From d1b97aae8e1dfc44f90049cf13854d84f9b05925 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:13:46 +1300 Subject: [PATCH 01/12] chore: Remove unused service-hook and reorg imports and jest config to use the one from @cartesi/services package. --- .../node-runners/NodeRunnerContainer.test.tsx | 6 +- apps/staking/__tests__/services/ens.test.ts | 146 ------------------ apps/staking/jest.setup.js | 12 ++ apps/staking/src/services/ens.ts | 80 ---------- 4 files changed, 15 insertions(+), 229 deletions(-) delete mode 100644 apps/staking/__tests__/services/ens.test.ts delete mode 100644 apps/staking/src/services/ens.ts diff --git a/apps/staking/__tests__/containers/node-runners/NodeRunnerContainer.test.tsx b/apps/staking/__tests__/containers/node-runners/NodeRunnerContainer.test.tsx index c43d85c7..311857e3 100644 --- a/apps/staking/__tests__/containers/node-runners/NodeRunnerContainer.test.tsx +++ b/apps/staking/__tests__/containers/node-runners/NodeRunnerContainer.test.tsx @@ -10,9 +10,9 @@ // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A // PARTICULAR PURPOSE. See the GNU General Public License for more details. +import { ENSEntry, useENS } from '@explorer/services'; import { UseWallet } from '@explorer/wallet/src/useWallet'; import { - act, findByText, fireEvent, getByRole, @@ -24,10 +24,10 @@ import { } from '@testing-library/react'; import { useFlag } from '@unleash/proxy-client-react'; import { NextRouter } from 'next/router'; +import { act } from 'react'; import { NodeRunnersContainer } from '../../../src/containers/node-runners/NodeRunnerContainer'; import { useUserNodes } from '../../../src/graphql/hooks/useNodes'; import useStakingPools from '../../../src/graphql/hooks/useStakingPools'; -import { ENSEntry, useENS } from '../../../src/services/ens'; import { useCartesiToken } from '../../../src/services/token'; import { useMessages } from '../../../src/utils/messages'; import { withChakraTheme } from '../../test-utilities'; @@ -40,7 +40,7 @@ import { } from '../mocks'; const useNodesMod = '../../../src/graphql/hooks/useNodes'; -const useENSMod = '../../../src/services/ens'; +const useENSMod = '@explorer/services'; jest.mock('../../../src/services/token', () => { return { diff --git a/apps/staking/__tests__/services/ens.test.ts b/apps/staking/__tests__/services/ens.test.ts deleted file mode 100644 index 7b78eeac..00000000 --- a/apps/staking/__tests__/services/ens.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (C) 2023 Cartesi Pte. Ltd. - -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. - -// This program is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -// PARTICULAR PURPOSE. See the GNU General Public License for more details. - -import { renderHook, waitFor } from '@testing-library/react'; -import { ethers } from 'ethers'; -import { useWallet } from '@explorer/wallet/src/useWallet'; -import { useENS } from '../../src/services/ens'; -import { Web3Provider } from '@ethersproject/providers'; -import { WalletConnectionContextProps } from '@explorer/wallet/src/definitions'; - -jest.mock('ethers'); - -jest.mock('@explorer/wallet/src/useWallet'); - -const address = '0x51937974a767da96dc1c3f9a7b07742e256f0ffe'; -const mockedGetAddress = ethers.utils.getAddress as jest.MockedFunction< - typeof ethers.utils.getAddress ->; -const mockUseWallet = useWallet as jest.MockedFunction; - -const walletData = { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - library: { - getSigner: jest.fn(), - } as unknown as Web3Provider, - account: address, - active: true, - activate: jest.fn(), - deactivate: jest.fn(), - chainId: 3, - network: { - ensAddress: '0xb00299b573a9deee20e6a242416188d1033e325f', - }, -}; - -describe('ens service', () => { - beforeEach(() => { - mockUseWallet.mockReturnValue( - walletData as unknown as WalletConnectionContextProps - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should early return address if an error has occurred', async () => { - mockedGetAddress.mockImplementation(() => { - throw new Error('Error ABC'); - }); - const { result } = renderHook(() => useENS(address)); - - await waitFor(() => { - expect(result.current.address).toBe(address); - expect(result.current.resolving).toBe(false); - }); - }); - - it('should early return address if network does not support ENS', async () => { - mockUseWallet.mockReturnValue({ - ...walletData, - library: { - ...walletData.library, - network: { - ensAddress: undefined, - }, - }, - } as unknown as WalletConnectionContextProps); - - const address = '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dc6'; - mockedGetAddress.mockImplementation(() => address); - const { result } = renderHook(() => useENS(address)); - - await waitFor(() => { - expect(result.current.address).toBe(address); - expect(result.current.resolving).toBe(false); - }); - }); - - it('should early return address if lookupAddress function returns an undefined name', async () => { - mockUseWallet.mockReturnValue({ - ...walletData, - library: { - lookupAddress: () => Promise.resolve(undefined), - network: { - ensAddress: 'some-ens-address', - }, - }, - } as unknown as WalletConnectionContextProps); - - const address = '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dc6'; - mockedGetAddress.mockImplementation(() => address); - const { result } = renderHook(() => useENS(address)); - - await waitFor(() => { - expect(result.current.address).toBe(address); - expect(result.current.resolving).toBe(false); - }); - }); - - it('should return expected ens data when reverse and forward lookup are successful', async () => { - const ensName = 'abc.xyz.com'; - const avatar = 'avatar-abc'; - const url = 'url-xyz'; - mockedGetAddress.mockImplementation(() => address); - mockUseWallet.mockReturnValue({ - ...walletData, - library: { - network: { - ensAddress: 'some-ens-address', - }, - lookupAddress: () => Promise.resolve(ensName), - getResolver: () => - Promise.resolve({ - getAddress: () => Promise.resolve(address), - getText: (type) => { - if (type === 'avatar') { - return Promise.resolve(avatar); - } - - return Promise.resolve(url); - }, - }), - }, - } as unknown as WalletConnectionContextProps); - - const { result } = renderHook(() => useENS(address)); - - await waitFor(() => { - expect(result.current.address).toBe(address); - expect(result.current.name).toBe(ensName); - expect(result.current.avatar).toBe(avatar); - expect(result.current.url).toBe(url); - expect(result.current.resolving).toBe(false); - }); - }); -}); diff --git a/apps/staking/jest.setup.js b/apps/staking/jest.setup.js index 13703410..a45a5938 100644 --- a/apps/staking/jest.setup.js +++ b/apps/staking/jest.setup.js @@ -3,6 +3,18 @@ import '@testing-library/jest-dom/extend-expect'; import 'jest-canvas-mock'; +jest.mock('@explorer/services', () => { + const original = jest.requireActual('@explorer/services'); + return { + __esModule: true, + ...original, + useENS: jest.fn().mockImplementation((address) => ({ + address, + resolving: false, + })), + }; +}); + Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation((query) => ({ diff --git a/apps/staking/src/services/ens.ts b/apps/staking/src/services/ens.ts deleted file mode 100644 index b018da6a..00000000 --- a/apps/staking/src/services/ens.ts +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2021 Cartesi Pte. Ltd. - -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. - -// This program is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -// PARTICULAR PURPOSE. See the GNU General Public License for more details. - -import { useEffect, useState } from 'react'; -import { ethers } from 'ethers'; -import { useWallet } from '@explorer/wallet'; - -export interface ENSEntry { - address: string; - name?: string; - avatar?: string; - url?: string; - resolving: boolean; -} - -/** - * This hook provides a easy way to display ENS information about a ETH address. - * @param address ETH address to be resolved - * @returns ENSEntry with the address, and name if address can be resolved to a name - */ -export const useENS = (address: string): ENSEntry => { - const { library } = useWallet(); - const [entry, setEntry] = useState({ address, resolving: true }); - useEffect(() => { - const resolve = async (address: string): Promise => { - // convert address to checksum address - try { - address = ethers.utils.getAddress(address); - } catch (e) { - console.warn(`error resolving address ${address}`); - return { address, resolving: false }; - } - - if (!library.network?.ensAddress) { - // network does not support ENS - return { address, resolving: false }; - } - - // do a reverse lookup - const name = await library.lookupAddress(address); - - console.log(`reverse lookup of ${address} resolved to ${name}`); - if (name) { - // name found, now do a forward lookup - const resolver = await library.getResolver(name); - const ethAddress = await resolver.getAddress(); - console.log( - `forward lookup of ${name} resolved to ${ethAddress}` - ); - - // we need to check if the forward resolution matches the reverse - if (ethAddress === address) { - const avatar = await resolver.getText('avatar'); - const url = await resolver.getText('url'); - console.log(`${name}: avatar(${avatar}) url(${url})`); - return { - address, - name, - avatar, - url, - resolving: false, - }; - } - } - return { address, resolving: false }; - }; - if (library) { - resolve(address).then(setEntry); - } - }, [address, library, library?.network]); - return entry; -}; From e4668ca6d2eb79b2fdae913837178929cedf0113 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:19:17 +1300 Subject: [PATCH 02/12] feat: Add drizzle kit and orm as new dependencies for staking. Map the env vars into turbo.json and ts declaration files. --- apps/staking/additional.d.ts | 28 ++ apps/staking/package.json | 11 +- package.json | 3 + turbo.json | 30 ++- yarn.lock | 502 ++++++++++++++++++++++++++++++++++- 5 files changed, 571 insertions(+), 3 deletions(-) diff --git a/apps/staking/additional.d.ts b/apps/staking/additional.d.ts index 622b9976..4292f50b 100644 --- a/apps/staking/additional.d.ts +++ b/apps/staking/additional.d.ts @@ -17,5 +17,33 @@ declare namespace NodeJS { * More info at {@link https://thegraph.com/explorer/subgraphs/5XqPmWe6gjyrJtFn9cLy237i4cWw2j9HcUJEXsP5qGtH?view=Query&chain=arbitrum-one} */ ENS_GRAPHQL_URL: string; + /** + * Storage to be used (e.g. ens information). It can have the following formats + * remote service: libsql://{database-name}-{company-name}.turso.io or + * local development: file:{path-to-your-local-file}.db + */ + TURSO_DATABASE_URL: string; + /** + * Only necessary when using the Turso platform. For development + * with local db it is not necessary. + */ + TURSO_AUTH_TOKEN?: string; + + /** + * A node to fetch mainnet blockchain information. e.g. Alchemy / Infura. + */ + HTTP_MAINNET_NODE_RPC: string; + + /** + * should be a number that set when ENS information about an address + * is considered staled. + */ + ENS_ENTRY_TTL: string; + + /** + * An authorization header value passed down by a CRON service calling + * functions under /api/cron/*. It can be any agreed value. + */ + CRON_SECRET: string; } } diff --git a/apps/staking/package.json b/apps/staking/package.json index 2887aeca..c3d4fc2f 100644 --- a/apps/staking/package.json +++ b/apps/staking/package.json @@ -12,7 +12,12 @@ "start": "next start", "storybook": "export NODE_OPTIONS=--openssl-legacy-provider && start-storybook -p 6006", "test": "dotenv -c test -- jest", - "test:ci": "dotenv -c test jest --runInBand --coverage" + "test:ci": "dotenv -c test jest --runInBand --coverage", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio", + "db:dev:migrate": "dotenv -c development -- yarn db:migrate", + "db:prod:migrate": "dotenv -c production -- yarn db:migrate" }, "dependencies": { "@apollo/client": "3.4.13", @@ -23,8 +28,10 @@ "@cartesi/util": "5.0.2", "@explorer/ui": "*", "@explorer/wallet": "*", + "@explorer/services": "*", "@fontsource/rubik": "4.5.0", "@fortawesome/fontawesome-free": "5.15.3", + "@libsql/client": "^0.14.0", "@metamask/onboarding": "1.0.1", "@types/humanize-duration": "3.25.1", "axios": "0.21.4", @@ -33,6 +40,7 @@ "cors": "2.8.5", "countdown": "2.6.0", "date-fns": "2.28.0", + "drizzle-orm": "^0.38.1", "eth-rpc-errors": "4.0.3", "ethers": "5.4.7", "graphql": "15.6.0", @@ -80,6 +88,7 @@ "babel-jest": "29.5.0", "babel-loader": "9.1.2", "chromatic": "5.10.2", + "drizzle-kit": "^0.30.0", "eslint": "7.32.0", "eslint-config-custom": "*", "eslint-config-prettier": "8.3.0", diff --git a/package.json b/package.json index a209a915..32793051 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "lint": "turbo run lint", "test": "dotenv -c test -- turbo run test", "test:ci": "dotenv -c test -- turbo run test:ci", + "db:generate": "turbo run db:generate", + "db:dev:migrate": "turbo run db:dev:migrate", + "db:prod:migrate": "turbo run db:prod:migrate", "build-storybook": "turbo run build-storybook", "storybook": "turbo run storybook", "format:check": "prettier --check \"**/*.{ts,tsx}\"", diff --git a/turbo.json b/turbo.json index 07c913e2..a38761d7 100644 --- a/turbo.json +++ b/turbo.json @@ -1,19 +1,47 @@ { "$schema": "https://turbo.build/schema.json", + "globalEnv": [ + "NEXT_PUBLIC_ENVIRONMENT", + "NEXT_PUBLIC_UNLEASH_PROXY_HOST", + "NEXT_PUBLIC_UNLEASH_PROXY_CLIENT_KEY", + "NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID", + "NEXT_PUBLIC_PROJECT_ID", + "NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID", + "NEXT_PUBLIC_MAINNET_GRAPHQL_URL", + "NEXT_PUBLIC_SEPOLIA_GRAPHQL_URL", + "ENS_GRAPHQL_URL", + "ENS_ENTRY_TTL", + "TURSO_DATABASE_URL", + "TURSO_AUTH_TOKEN", + "CRON_SECRET" + ], "pipeline": { "export": { "dependsOn": ["build"] }, "build": { - "dependsOn": ["^build"], + "dependsOn": ["^build", "db:prod:migrate"], "outputs": ["dist/**", ".next/**"] }, "lint": { "outputs": [] }, "dev": { + "dependsOn": ["db:dev:migrate"], "cache": false }, + "db:generate": { + "inputs": ["src/db/*.ts"] + }, + "db:migrate": { + "outputs": [] + }, + "db:dev:migrate": { + "outputs": [] + }, + "db:prod:migrate": { + "outputs": [] + }, "build-storybook": { "outputs": ["dist/**", "storybook-static/**"] }, diff --git a/yarn.lock b/yarn.lock index 7c3cb9c2..d39bf3fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2437,6 +2437,11 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@drizzle-team/brocli@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz#9757c006a43daaa6f45512e6cf5fabed36fb9da7" + integrity sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w== + "@emotion/babel-plugin@^11.11.0": version "11.12.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz#7b43debb250c313101b3f885eba634f1d723fcc2" @@ -2664,6 +2669,247 @@ dependencies: hash-test-vectors "^1.3.2" +"@esbuild-kit/core-utils@^3.3.2": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz#186b6598a5066f0413471d7c4d45828e399ba96c" + integrity sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ== + dependencies: + esbuild "~0.18.20" + source-map-support "^0.5.21" + +"@esbuild-kit/esm-loader@^2.5.5": + version "2.6.5" + resolved "https://registry.yarnpkg.com/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz#6eedee46095d7d13b1efc381e2211ed1c60e64ea" + integrity sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA== + dependencies: + "@esbuild-kit/core-utils" "^3.3.2" + get-tsconfig "^4.7.0" + +"@esbuild/aix-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" + integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== + +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" + integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== + +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" + integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== + +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + +"@esbuild/android-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" + integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== + +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + +"@esbuild/darwin-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" + integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== + +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/darwin-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" + integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== + +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" + integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== + +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/freebsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" + integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== + +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" + integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== + +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" + integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== + +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" + integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== + +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-loong64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" + integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== + +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-mips64el@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" + integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== + +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" + integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== + +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-riscv64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" + integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== + +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-s390x@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" + integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== + +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/linux-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" + integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== + +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/netbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" + integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== + +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/openbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" + integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== + +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/sunos-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" + integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== + +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" + integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== + +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" + integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== + +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + +"@esbuild/win32-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" + integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== + "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -4536,6 +4782,82 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@libsql/client@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@libsql/client/-/client-0.14.0.tgz#df30cfb21e84797398afdd268e153cf1594c68a1" + integrity sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q== + dependencies: + "@libsql/core" "^0.14.0" + "@libsql/hrana-client" "^0.7.0" + js-base64 "^3.7.5" + libsql "^0.4.4" + promise-limit "^2.7.0" + +"@libsql/core@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@libsql/core/-/core-0.14.0.tgz#bbdbdf89227f1325b79e49d00babace81a47bf23" + integrity sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q== + dependencies: + js-base64 "^3.7.5" + +"@libsql/darwin-arm64@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/darwin-arm64/-/darwin-arm64-0.4.7.tgz#070679b082ff88ad0a8c8fef94c6d0cc644e441f" + integrity sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg== + +"@libsql/darwin-x64@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/darwin-x64/-/darwin-x64-0.4.7.tgz#ee5cff8527777eea72e2ad9a4f1c41d6a5b6736e" + integrity sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA== + +"@libsql/hrana-client@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@libsql/hrana-client/-/hrana-client-0.7.0.tgz#c059d8106b9d40dd931217333710aff2ceb5216e" + integrity sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw== + dependencies: + "@libsql/isomorphic-fetch" "^0.3.1" + "@libsql/isomorphic-ws" "^0.1.5" + js-base64 "^3.7.5" + node-fetch "^3.3.2" + +"@libsql/isomorphic-fetch@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@libsql/isomorphic-fetch/-/isomorphic-fetch-0.3.1.tgz#42023816d5645a5a3f3a78bb3899bdc5814c7b88" + integrity sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw== + +"@libsql/isomorphic-ws@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@libsql/isomorphic-ws/-/isomorphic-ws-0.1.5.tgz#e2d1faf965ba0f3be9301fbf5640164d03c4e606" + integrity sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg== + dependencies: + "@types/ws" "^8.5.4" + ws "^8.13.0" + +"@libsql/linux-arm64-gnu@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/linux-arm64-gnu/-/linux-arm64-gnu-0.4.7.tgz#d1364f51b8c5a013822257de1a51525b602ac71d" + integrity sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA== + +"@libsql/linux-arm64-musl@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/linux-arm64-musl/-/linux-arm64-musl-0.4.7.tgz#14d9762853368e77e24774b306dea7c6ac2b4881" + integrity sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw== + +"@libsql/linux-x64-gnu@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/linux-x64-gnu/-/linux-x64-gnu-0.4.7.tgz#ae98f4311b45ffe6c6e31241b7d321cc24cf2922" + integrity sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ== + +"@libsql/linux-x64-musl@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/linux-x64-musl/-/linux-x64-musl-0.4.7.tgz#9340d4c46e77ae072968e7dc70710dedfd6ee4f4" + integrity sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA== + +"@libsql/win32-x64-msvc@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@libsql/win32-x64-msvc/-/win32-x64-msvc-0.4.7.tgz#b39cc6de4af797af736e9f2179002e8ebf4d2d4a" + integrity sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw== + "@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": version "1.1.1" resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz#64df34e2f12e68e78ac57e571d25ec07fa460ca9" @@ -4683,6 +5005,11 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@neon-rs/load@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@neon-rs/load/-/load-0.0.4.tgz#2a2a3292c6f1fef043f49886712d3c96a547532e" + integrity sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw== + "@next/env@14.2.2": version "14.2.2" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.2.tgz#6c36fe0b04a22ea78bd60a645ae77d53cd16d3ca" @@ -8287,6 +8614,13 @@ dependencies: "@types/node" "*" +"@types/ws@^8.5.4": + version "8.5.13" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.13.tgz#6414c280875e2691d0d1e080b05addbf5cb91e20" + integrity sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -12761,6 +13095,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + data-urls@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" @@ -13088,6 +13427,11 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g== +detect-libc@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" + integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -13330,6 +13674,21 @@ dotignore@~0.1.2: dependencies: minimatch "^3.0.4" +drizzle-kit@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/drizzle-kit/-/drizzle-kit-0.30.0.tgz#8ea3ff71f95c223c268c84e634b3f047b49b61bb" + integrity sha512-zAf0qg/BX2lV/Xip4igXrtbDv+Ub8S39U6qSOEGNvqcHrnmaQjS4mkkFZOUsgXRbuH56QUeWZxfnLHefrKCV5g== + dependencies: + "@drizzle-team/brocli" "^0.10.2" + "@esbuild-kit/esm-loader" "^2.5.5" + esbuild "^0.19.7" + esbuild-register "^3.5.0" + +drizzle-orm@^0.38.1: + version "0.38.1" + resolved "https://registry.yarnpkg.com/drizzle-orm/-/drizzle-orm-0.38.1.tgz#be534cabe8501719806aa98b0d79646a55216eaf" + integrity sha512-TpOzNrPGy7dl0/uP9vKD6ATzp9noJaRYhOYqtOCKxxwSmZqrPxN4SW5kWawVUIcbSd2lkbDCdZue+gtVNLmQsg== + duplexer3@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" @@ -13691,6 +14050,70 @@ es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" +esbuild-register@^3.5.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.6.0.tgz#cf270cfa677baebbc0010ac024b823cbf723a36d" + integrity sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg== + dependencies: + debug "^4.3.4" + +esbuild@^0.19.7: + version "0.19.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" + integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.19.12" + "@esbuild/android-arm" "0.19.12" + "@esbuild/android-arm64" "0.19.12" + "@esbuild/android-x64" "0.19.12" + "@esbuild/darwin-arm64" "0.19.12" + "@esbuild/darwin-x64" "0.19.12" + "@esbuild/freebsd-arm64" "0.19.12" + "@esbuild/freebsd-x64" "0.19.12" + "@esbuild/linux-arm" "0.19.12" + "@esbuild/linux-arm64" "0.19.12" + "@esbuild/linux-ia32" "0.19.12" + "@esbuild/linux-loong64" "0.19.12" + "@esbuild/linux-mips64el" "0.19.12" + "@esbuild/linux-ppc64" "0.19.12" + "@esbuild/linux-riscv64" "0.19.12" + "@esbuild/linux-s390x" "0.19.12" + "@esbuild/linux-x64" "0.19.12" + "@esbuild/netbsd-x64" "0.19.12" + "@esbuild/openbsd-x64" "0.19.12" + "@esbuild/sunos-x64" "0.19.12" + "@esbuild/win32-arm64" "0.19.12" + "@esbuild/win32-ia32" "0.19.12" + "@esbuild/win32-x64" "0.19.12" + +esbuild@~0.18.20: + version "0.18.20" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -15131,6 +15554,14 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + fetch-ponyfill@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/fetch-ponyfill/-/fetch-ponyfill-4.1.0.tgz#ae3ce5f732c645eab87e4ae8793414709b239893" @@ -15465,6 +15896,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -15779,6 +16217,13 @@ get-symbol-description@^1.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +get-tsconfig@^4.7.0: + version "4.8.1" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.1.tgz#8995eb391ae6e1638d251118c7b56de7eb425471" + integrity sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== + dependencies: + resolve-pkg-maps "^1.0.0" + get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" @@ -18347,6 +18792,11 @@ jquery@3.6.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== +js-base64@^3.7.5: + version "3.7.7" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79" + integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw== + js-sha3@0.5.7, js-sha3@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" @@ -18924,6 +19374,22 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +libsql@^0.4.4: + version "0.4.7" + resolved "https://registry.yarnpkg.com/libsql/-/libsql-0.4.7.tgz#dc80f94c58ac8146adc351e82f76dd12366fe051" + integrity sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw== + dependencies: + "@neon-rs/load" "^0.0.4" + detect-libc "2.0.2" + optionalDependencies: + "@libsql/darwin-arm64" "0.4.7" + "@libsql/darwin-x64" "0.4.7" + "@libsql/linux-arm64-gnu" "0.4.7" + "@libsql/linux-arm64-musl" "0.4.7" + "@libsql/linux-x64-gnu" "0.4.7" + "@libsql/linux-x64-musl" "0.4.7" + "@libsql/win32-x64-msvc" "0.4.7" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -20194,6 +20660,11 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2, node-fetch@2.6.7, node-fetch@^2.6.1, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" @@ -20206,6 +20677,15 @@ node-fetch@2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-fetch@~1.7.1: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" @@ -21483,6 +21963,11 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== +promise-limit@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/promise-limit/-/promise-limit-2.7.0.tgz#eb5737c33342a030eaeaecea9b3d3a93cb592b26" + integrity sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw== + promise-to-callback@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/promise-to-callback/-/promise-to-callback-1.0.0.tgz#5d2a749010bfb67d963598fcd3960746a68feef7" @@ -22610,6 +23095,11 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -23392,7 +23882,7 @@ source-map-support@^0.4.15: dependencies: source-map "^0.5.6" -source-map-support@^0.5.13, source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@~0.5.12, source-map-support@~0.5.20: +source-map-support@^0.5.13, source-map-support@^0.5.16, source-map-support@^0.5.19, source-map-support@^0.5.21, source-map-support@~0.5.12, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -25346,6 +25836,11 @@ web-namespaces@^1.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec" integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw== +web-streams-polyfill@^3.0.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + web3-bzz@1.2.11: version "1.2.11" resolved "https://registry.yarnpkg.com/web3-bzz/-/web3-bzz-1.2.11.tgz#41bc19a77444bd5365744596d778b811880f707f" @@ -26237,6 +26732,11 @@ ws@^8.11.0, ws@^8.2.3, ws@^8.5.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + x-default-browser@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/x-default-browser/-/x-default-browser-0.4.0.tgz#70cf0da85da7c0ab5cb0f15a897f2322a6bdd481" From 11ab3dfc322b17ddc81ece0686137ceab4ee8f55 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:21:42 +1300 Subject: [PATCH 03/12] feat: Add drizzle configuration, schema definitions, migrations and helpers to solve bulk update conflicts. --- apps/staking/drizzle.config.ts | 28 + .../db/helpers/conflictUpdateSetAllColumns.ts | 21 + .../db/migrations/0000_dizzy_mister_fear.sql | 10 + .../src/db/migrations/0001_empty_mystique.sql | 743 ++++++++++++++++++ .../src/db/migrations/meta/0000_snapshot.json | 79 ++ .../src/db/migrations/meta/0001_snapshot.json | 79 ++ .../src/db/migrations/meta/_journal.json | 20 + apps/staking/src/db/schemas.ts | 26 + 8 files changed, 1006 insertions(+) create mode 100644 apps/staking/drizzle.config.ts create mode 100644 apps/staking/src/db/helpers/conflictUpdateSetAllColumns.ts create mode 100644 apps/staking/src/db/migrations/0000_dizzy_mister_fear.sql create mode 100644 apps/staking/src/db/migrations/0001_empty_mystique.sql create mode 100644 apps/staking/src/db/migrations/meta/0000_snapshot.json create mode 100644 apps/staking/src/db/migrations/meta/0001_snapshot.json create mode 100644 apps/staking/src/db/migrations/meta/_journal.json create mode 100644 apps/staking/src/db/schemas.ts diff --git a/apps/staking/drizzle.config.ts b/apps/staking/drizzle.config.ts new file mode 100644 index 00000000..51bd99d9 --- /dev/null +++ b/apps/staking/drizzle.config.ts @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import type { Config } from 'drizzle-kit'; +import { join } from 'path'; + +const base = join('.', 'src', 'db'); + +const config: Config = { + schema: `${base}/schemas.ts`, + out: `${base}/migrations`, + dialect: 'turso', + dbCredentials: { + url: process.env.TURSO_DATABASE_URL, + authToken: process.env.TURSO_AUTH_TOKEN, + }, + casing: 'snake_case', +}; + +export default config; diff --git a/apps/staking/src/db/helpers/conflictUpdateSetAllColumns.ts b/apps/staking/src/db/helpers/conflictUpdateSetAllColumns.ts new file mode 100644 index 00000000..cd3ec4c9 --- /dev/null +++ b/apps/staking/src/db/helpers/conflictUpdateSetAllColumns.ts @@ -0,0 +1,21 @@ +import { getTableColumns, sql } from 'drizzle-orm'; +import { SQLiteTable, SQLiteUpdateSetSource } from 'drizzle-orm/sqlite-core'; +import { snakeCase } from 'lodash/fp'; + +export function conflictUpdateSetAllColumns( + table: TTable +): SQLiteUpdateSetSource { + const columns = getTableColumns(table); + const conflictUpdateSet = Object.entries(columns).reduce( + (acc, [columnName, columnInfo]) => { + if (!columnInfo.default) { + const dbColumnName = snakeCase(columnInfo.name); + acc[columnName] = sql.raw(`excluded.${dbColumnName}`); + } + return acc; + }, + {} + ) as SQLiteUpdateSetSource; + + return conflictUpdateSet; +} diff --git a/apps/staking/src/db/migrations/0000_dizzy_mister_fear.sql b/apps/staking/src/db/migrations/0000_dizzy_mister_fear.sql new file mode 100644 index 00000000..b76dbac3 --- /dev/null +++ b/apps/staking/src/db/migrations/0000_dizzy_mister_fear.sql @@ -0,0 +1,10 @@ +CREATE TABLE `address_ens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `address` text NOT NULL, + `has_ens` integer NOT NULL, + `updated_at` integer DEFAULT (unixepoch()), + `avatar_url` text, + `name` text +); +--> statement-breakpoint +CREATE UNIQUE INDEX `address_ens_address_unique` ON `address_ens` (`address`); \ No newline at end of file diff --git a/apps/staking/src/db/migrations/0001_empty_mystique.sql b/apps/staking/src/db/migrations/0001_empty_mystique.sql new file mode 100644 index 00000000..530f0f12 --- /dev/null +++ b/apps/staking/src/db/migrations/0001_empty_mystique.sql @@ -0,0 +1,743 @@ +INSERT INTO "address_ens" ("address", "has_ens", "avatar_url", "name") +VALUES ('0x07507f411411139d11d93319b039c9648dd1e285', 1, 'https://pbs.twimg.com/profile_images/1291912529350803456/sqgGZ8iY_400x400.png', 'ctsi.castrani.eth'), + ('0x0a47ef0b7bcd4f9ca549140144e2d0ea136ba4c6', 1, 'https://static.rockx.com/img/rockx_new_logo.png', 'rockx1.eth'), + ('0x0bbf70d1bfcf374cf88ab896a74444b4406b5a48', 1, null, 'pool.bluechain.eth'), + ('0x1ef1fcbe66974dbd2089d0d60dce94e4bf2c9617', 0, null, null), + ('0x2942aa4356783892c624125acfbbb80d29629a9d', 1, 'https://omnistake.s3.us-west-1.amazonaws.com/pool-logo/omnistakelogo.png', 'ctsi.omnistake.eth'), + ('0x2f9c790c37d675b459b500f823a52a7370b4b987', 1, 'https://colossus.digital/wp-content/uploads/2022/10/Colossus_ENS_Logo.jpg', 'ctsi.colossusdigital.eth'), + ('0x440d4bc1f4bf8468c370ffbefa471fdea0a1f838', 1, 'https://kosmosnode.io/wp-content/uploads/2021/07/Logo-Kosmos-cartesi2.png', 'ctsi.kosmosnode.eth'), + ('0x48381609a2f1bfe30b465e106bf8324342abe107', 1, 'https://blockmain.capital/Logo-Blockmain-capital-3.png', 'blockmain.eth'), + ('0x4e275bdb0db2b8e4ab1fdbde66f739113de3939d', 1, 'https://pbs.twimg.com/profile_images/1163440927978991616/TiXV-Jzq_400x400.jpg', 'ctsi.everstake-one.eth'), + ('0x5a2640b565a40b40b4fe9da7f33fe94a7673accb', 1, null, 'ctsi.delegated-staking.eth'), + ('0x7a3a5ee38cac45cefa05e71431c72896ef0de490', 0, null, null), + ('0x7ede8d738e13d9b9afc123703fd8ef396e9d1e0b', 1, 'https://res.cloudinary.com/crunchbase-production/image/upload/c_lpad,h_256,w_256,f_auto,q_auto:eco,dpr_1/ikhof9gwtwhesrob24um', 'mycointainer.eth'), + ('0x8ea35a47a3c5b4005775019c1af9483454e05fd0', 1, 'https://raw.githubusercontent.com/arichard5/moon/main/pngwing.com.png', 'yourctsimoon.eth'), + ('0x8f57ae7ddde63412509e71997b2438a3e6dcb763', 1, 'https://durable.network/images/logo.png', 'ctsi.durable.network'), + ('0x941eadfcdfd6e00110496cba505b7a7acdb991f6', 0, null, null), + ('0xa6d8228332c61c176534a9bbdb4fdb505ef5ed07', 1, 'https://i.imgur.com/yhZL09Q.png', 'poolctsi.eth'), + ('0xa7f050efa20ffa7d9831573af0ad31b0c59f0c93', 1, 'https://www.blockscope.net/blockscope_thumbnail_360_360.png', 'blockscope.eth'), + ('0xab7a6147cbe78e2be7ddf4d29bd56009846a56b4', 0, null, null), + ('0xaf921c4ac53354b2a915ff5b3b9771ed01a7e5ec', 1, 'https://bafybeig46ebe4wb3djeqqopogvvpnq6slwigdcxkxtu62seri32maixh6e.ipfs.infura-ipfs.io/', 'ctsi.bbwhales.eth'), + ('0xb84d5db46ecaaadeedc6da8e0d67c9fcb0873300', 1, 'https://i.ibb.co/SmLzf2Y/Excelsior-logo.png', 'excelsior-staking.eth'), + ('0xbb79d9c375e8ef9278103d1da0f4777eb6b2db12', 1, null, 'cartesi.hashkeycloud.eth'), + ('0xd0ca24400613f4c202006568410c5bb12be07b94', 1, 'https://www.hyperquanta.com/static/images/SmallLogo.png', 'ctsi.hq-pool.eth'), + ('0xd20816daae9b94015609b0a97a7e859e15d1cafc', 1, 'https://bitboycrypto.com/wp-content/uploads/2019/07/cropped-newlogo.png', 'bitboyctsi.eth'), + ('0xd424c94645ea33658ceae51b94072e06c85a48e1', 1, 'https://nystudio107-ems2qegf7x6qiqq.netdna-ssl.com/img/blog/_1200x675_crop_center-center_82_line/the-matrix.jpg.webp', 'ctsimatrix.eth'), + ('0xe3735698ac7c8ae0709c25899f244dcb7bfd14dc', 0, null, null), + ('0xe3f9cdc6464c5fd747877f0b5612636dee539a45', 0, null, null), + ('0xeac606152865d75a01dac7e6182007f6eea2d942', 0, null, null), + ('0xa774ffb2a2e28fa61c2adc32a03102db19f4cc54', 0, null, null), + ('0x22cd620226ea46ff812e21e4b213581c6c17c3ce', 0, null, null), + ('0xa9ba9f3496efceb524cfaf7d7ab2a343fff1ba56', 0, null, null), + ('0xd588a4ad819ee4dae02a2824719e127610d14e6d', 0, null, null), + ('0x355dbd28406170cff97a0b0b82210ba149b4eb70', 0, null, null), + ('0x40c17108b48335839126bc0a868b69350df62345', 0, null, null), + ('0xffe51b88c1d963109f0aca0f5399509bd8cafd52', 0, null, null), + ('0x075579b293af9b7cc2e97923f411acc5156df277', 0, null, null), + ('0xc8e94f26316d3753b11a64947ab02271a57dbb45', 0, null, null), + ('0x4ff2e8a4e74bc9376130eceec6d2c60256e83f82', 1, null, 'siannie.eth'), + ('0xe5bdbcc949da1c41df8a5e58136f9cd8059519cd', 0, null, null), + ('0x1e54f1839893eb7e5783eebbc2753af5315d42cc', 0, null, null), + ('0xc71c504d2a2938a173660ae71b3e97b563196bea', 0, null, null), + ('0xf977814e90da44bfa03b6295a0616a897441acec', 0, null, null), + ('0xb08ecd5b9fa33fba2cb27a6279d4391481b54ff7', 0, null, null), + ('0xd5bb24152217bea7a617525ddfa64ea3b41b9c0a', 0, null, null), + ('0x2209277976f5ab1b689b0e56d24ec7065631affe', 0, null, null), + ('0xa6d66a1576db85cbead9135d669c176a22a6fa9a', 0, null, null), + ('0xf77aca5af6148c0743693e2a46d6594b2c6bb569', 0, null, null), + ('0x32fcb62a5af7f327889658ba16cf83693a0ccca6', 0, null, null), + ('0xb6dec8886b0381b7e33d9775d914b80944c7acea', 0, null, null), + ('0xbf445f00225e3001e1f7af6679698af7a5896568', 0, null, null), + ('0xa6d890932264e79f6a086176eee54e1ceeeebeb8', 0, null, null), + ('0xc0751b35ff099828bd07d68be517bfce736dde38', 0, null, null), + ('0x89b2cbfaeee82e3f904c12e91700181c77419e39', 0, null, null), + ('0xc41efeeeedbe3c27d3287e73ec7f92adb76be427', 0, null, null), + ('0x6eba7169cbc1526dea91a52d37e0c13a95a2b56e', 0, null, null), + ('0xd786221947fe299ded1235032f020c977ade05b5', 0, null, null), + ('0x67b65b8100e52d0871482083db958148289aee34', 0, null, null), + ('0x2453430ee1722aff2c74b6ae5efeda40ec338544', 0, null, null), + ('0x9175e1cc3aa0b437272375de285b595605187622', 0, null, null), + ('0x0dd34c397384de8f21f463096a360a0419d476e1', 0, null, null), + ('0xb6fe18c8e22b633964714ff9382f893abe6e9fee', 1, null, 'aromatdoma.eth'), + ('0x6cae65fa9069119cb96ea634374d9e170f7fe967', 0, null, null), + ('0xb363112ba7ed93a221efb405496e4fa3be119d81', 0, null, null), + ('0xdeb3d637783fe892dab8e143c8d646a7567463f8', 0, null, null), + ('0x649885135c6ed14390c61ac4294dd56476532310', 0, null, null), + ('0xb95053d2a087066772f891d441c30060b12d552a', 0, null, null), + ('0xbd2ce651d8b7c55054872c980fa8f90553123c7f', 0, null, null), + ('0x9cae86e3dd7c14b57bd93efdd47c12dfca91aa3a', 0, null, null), + ('0x38c484ef4cd1879bfc2ca4f31f9dc02113b98f52', 0, null, null), + ('0xbb8a430025e7280655314c30d87f630d6719e0f9', 0, null, null), + ('0x436d607d8603643a67898ecade80ef2e9a952474', 1, null, 'web3rx.eth'), + ('0x0997b4126862cd2f17c810e291161ac7d68b439b', 0, null, null), + ('0xb8fd9b300c2819546013dd45c238ab6fa1eadba0', 0, null, null), + ('0x20f3887ac9b1a22bb4f32527f0fc27ad503b6c66', 0, null, null), + ('0x8b6a8903c8ac057af4db7df4057170faf91f2a85', 0, null, null), + ('0x618928927f71e258c2608bf0cba0bfcf34d48222', 0, null, null), + ('0x3aceefea1643a93ff8c6b1863182720d5f6027c5', 0, null, null), + ('0xba668c1fe35f50ee2f9f8a3460d9f75ffe344cda', 0, null, null), + ('0x83c6323fb8cafe4d58b7e7beb8f7d2cdb5fd46d9', 0, null, null), + ('0xeca023e03127205dca2f196b8b32bdd748203587', 0, null, null), + ('0x2505a3f0a59102e9818bdc29c077827db4c6ce30', 0, null, null), + ('0x4f63ea777716d6d3270512326a67549177394c6d', 0, null, null), + ('0x926a79a517694d722ab04b41bdfb9f861c389269', 0, null, null), + ('0x8e193f5e6ab744f6671fd9ac2356fde169a116ac', 1, 'eip155:1/erc721:0x60f80121c31a0d46b5279700f9df786054aa5ee5/265548', 'teague.eth'), + ('0xfc6d41b326198269bba82f29f2d7ea4b5ea1ea6c', 0, null, null), + ('0xb7bda1df534709ddfc825e35f00f8558bc9ea730', 0, null, null), + ('0x4b375eafcc256de2b9282a5125246a316289cec6', 0, null, null), + ('0x514dbbc8e34beead4225783710bceb2626d2e886', 1, 'https://euc.li/eigendude.eth', 'eigendude.eth'), + ('0x5b079a2b4c37a9ebde4834dabe95f4805d8c24fb', 0, null, null), + ('0x877e621f7a43164ec2d951c4da54927c53e25c75', 1, null, 'brunobarcella.eth'), + ('0xe113e7b1e8ecd07973ea40978aed520241d17f27', 1, null, 'embl3m.eth'), + ('0x632bb7b2db84da4c6ede9b22827e5e6fa350cd50', 0, null, null), + ('0x31e1590903ed7bbd0677d36c2fed34640fb44a3c', 0, null, null), + ('0xc97e11fcff2e2a370c6af376abcdaa0045e31391', 0, null, null), + ('0x21c64e00a9c9b4b844e2644f0613b824c67a7fba', 0, null, null), + ('0xebfbe1190f78352562668d107dd9f2497f9abb9d', 0, null, null), + ('0xdf8431e9dbec93e1a8da5d4f5b0ae27671aa9bcd', 0, null, null), + ('0xc87ff5635ad4996120f21f810a285bcdf06c9e90', 0, null, null), + ('0x8e73bd6bbf8eaedbf34cd738477b63291ed4adf0', 0, null, null), + ('0x5cffe165940e362ef15678044970d39ec1a0c0f8', 0, null, null), + ('0x4eea4510c6adc7c5cdc433857dac16a647cfbfdf', 0, null, null), + ('0xe29edf295746d9bff056626ed739d9c2667732c4', 0, null, null), + ('0x10b7e261821c07b9e656c2ce2df35d5fe5377c3d', 0, null, null), + ('0x457b4a9828899fd6bdb2b624b31a196dde4731d4', 0, null, null), + ('0x7b5fcc99ce4ce2a3e788801835e2ef4b770ca6e2', 0, null, null), + ('0x3283267103c0a7a36c4f6e4345c2db01e8317421', 0, null, null), + ('0x8ac348423b2af6dca711c1a080aede5f9792cb4d', 0, null, null), + ('0x953a189307ae98e3616473c306d3a6570cc90839', 0, null, null), + ('0xbfda0c35ab3c8e030c0075987756fa923330b6e3', 0, null, null), + ('0x0e3843a33072db8f97ed2ea32fb9122a0b0b17e9', 0, null, null), + ('0x5b5b37886b6113cdafd04ee41aee3f0261eb2323', 0, null, null), + ('0x128184e5ac5f52f0ea3634ac0dc2f63a4bf07795', 0, null, null), + ('0x737454cd34d13856f0b4ca3048e1f4d4f2235034', 0, null, null), + ('0xf261fbc4f4ac0f9a0c24f236444b0843bb00aac3', 0, null, null), + ('0x106a143f26ddec7612be177706b96f257000715b', 0, null, null), + ('0x696f3bf923a6c33c54be2c4ba2bcc95ecff573ce', 0, null, null), + ('0xd27a20a18496ae3200358e569b107d62a1e3f463', 1, 'https://ens.xyz/tuler.eth', 'tuler.eth'), + ('0xfa7b9174984c0abe040564e3e5df2e409cb0b61b', 0, null, null), + ('0xf01c75d0c5a260cd6537c072f9ce75325bf255e5', 0, null, null), + ('0xd2c2ecc5b40dff5979275b416f6e6f0f253ddf0d', 0, null, null), + ('0xb975ceb96ae54460f4afe3ceac97010e907a84d5', 0, null, null), + ('0xd5ec4f9af604ed6ee7f9d81de12fec7197c234b3', 0, null, null), + ('0xc05834f8560adaacd2d652f4f28a391dedfecf49', 0, null, null), + ('0x8dba03f9b5018effc8d7620ef235760383c17f8f', 0, null, null), + ('0xa7c107e0c3a743a5bfeebf2d927bfe94aca3699e', 0, null, null), + ('0x2308baa3100bbb2a94233eafbdcb8f6b8d593308', 0, null, null), + ('0x5a1f9d794a9639dc5b4629b56cf3312a51f43e62', 0, null, null), + ('0x9c10a969d423e991910e3bf44ba10bd7c1626550', 0, null, null), + ('0x42b3a5053afed46859bfc85dc8ce170a7b361602', 0, null, null), + ('0x87b6c1e90087d6a0fc392e5250a6203eb09e1b54', 0, null, null), + ('0x3fc1f081c9aacf19c913552a96b5f336fda52561', 0, null, null), + ('0x814494f38c0d9b679e34c0b7fcd93c04c2e0eb60', 0, null, null), + ('0x35ec43ee73302b849876e248b7cb5b90aa5b38bd', 0, null, null), + ('0x05d22e6e638a97e1777d571810fe9fc802841810', 0, null, null), + ('0x1f45e55c43b546115318a8d14d5b153ce40ad1ee', 0, null, null), + ('0x799a09f56920c7dd668ea59082a8863b62df4f2b', 0, null, null), + ('0xb72ed8401892466ea8af528c1af1d0524bc5e105', 1, null, 'bmf-capital.eth'), + ('0xe1a44bd41b6d2dea1c00373514eeaebc7361c9e6', 0, null, null), + ('0x8ddd05c04eed7ad151bc72696c3e483368bf9c91', 1, null, 'iamno1.eth'), + ('0x92f0f8f755b878abcc7f56bf259fe7a0f9c5d7e3', 0, null, null), + ('0x544a40955ba1c7e56e161a59e1319e3313c25251', 1, 'https://i.seadn.io/gae/an91hOZ3jd3l5rVgsqC-TJjB-MhCK5X9mqof3RygBxi1rDIMbZlo8minkaJNWnm8lywCE1idjEzmhzK94WLeyoWk49uvd_hhrK-m', 'lissstern.eth'), + ('0x32d9f89294430bbe6305f48ad3fd8c09f5b5b077', 0, null, null), + ('0x9093e30dcbfd92cdd1df661e9b12b5a8349f4da4', 0, null, null), + ('0x14374f34cc39d74eff3505b418b97e2b3e2afd4c', 0, null, null), + ('0x1690cc38a1d068ea0d2233cb7fedc049e50535aa', 0, null, null), + ('0xd298a4a23f95cc37e723277564fb62a0361da709', 0, null, null), + ('0xb8e45a6276599a6d7c2b1476611ffc8e86ab2cd6', 0, null, null), + ('0xfb529f55f4a39d9a336cd1a15b489f07679f4932', 0, null, null), + ('0x598231a0172ed433a65ee1e1d781c29c9f148bb4', 0, null, null), + ('0x2a22f9eaeeee2854838354c58c9725be7cd87453', 0, null, null), + ('0xc55cf647c1142ad47342bcd132cbf05f07110e3f', 0, null, null), + ('0x605934f48603a45d58ecdbafb4af1309d038eeda', 0, null, null), + ('0x1db826386c15e3316a80fbefcf57e392251392fd', 0, null, null), + ('0x1fab34460301e1d2055a4aba818dcd623e5537a7', 0, null, null), + ('0x5ea1943b14ec725016169a94bb704708b6000545', 0, null, null), + ('0xea71fda6c17dfb3637e41a127c96dcd393b1269c', 0, null, null), + ('0x73037097a165700ac71f91600ededb915b562bed', 1, null, 'marktur.eth'), + ('0x4928f15079d02789fa5720067553b64fa12f2e5c', 0, null, null), + ('0x5ac6a9f84be3990a3df5c93451b4544434cca6e2', 0, null, null), + ('0xefeae10e3744b9a66d449628d60ff3c2ca26ab15', 0, null, null), + ('0x82e939ce51a5510b183ac0b5922127613c3709ec', 1, null, 'karmentruong.eth'), + ('0x073844732318c6b2abb9aa3d47eef3a6121cf437', 0, null, null), + ('0x35c96a322d672d4198667700a250774689eef2a6', 0, null, null), + ('0xcacf945685ad5bdbae72a372291bb8da2df81fea', 0, null, null), + ('0x4e8dd13b7f8a07a69d935b0895fca8eb49b27de0', 0, null, null), + ('0x0b47fef7cb50ad5abbc10265152aacc3b4b5e6d1', 0, null, null), + ('0xb18195a890514b203ce6e7566477280cc515a1dc', 0, null, null), + ('0x3e5e40ba1706a0d487a0014d9b0140c785e49553', 0, null, null), + ('0xa7eb9c932d5a6e19e927d2356c8dc5d9e12ded88', 0, null, null), + ('0x2da14d486ae663347e2e69a0189c0ae36248f681', 0, null, null), + ('0xc4801fda5ef3dc8e86f7259ddad52983afeb19ef', 0, null, null), + ('0x6f0e517c0b5a813a6a5b79818b5bb284d9a712eb', 0, null, null), + ('0x97d29b3a41f3a9486b2bf5e0484a7c231d409260', 0, null, null), + ('0xc1b80b5169151a6145a25c9f1dc522b5b3621953', 0, null, null), + ('0x9386d9970a7a0870cdc9318fafc5a1eb453279aa', 0, null, null), + ('0xbc6e766b82ea3edf96a7807dcae446ebbcbb9af5', 0, null, null), + ('0xc2902f595444770de165e47f2db399bb7d14a634', 0, null, null), + ('0x175fb20b9b5bcf80a0167d403405e6f3a074cfa0', 0, null, null), + ('0xd37ea7332776a6a97903b645f218163659e6ae75', 0, null, null), + ('0x17c63868e3ab7da20adcf8c27d4ee46fdec1c325', 0, null, null), + ('0xcdbf0b2f9e658aeacf76c4465a539647bb12d779', 0, null, null), + ('0x9e5828f0eebd4376ba54c41760ce392a812fb3e3', 0, null, null), + ('0x2202c1ff921191fc8f6992e016f9ce3588da8c06', 0, null, null), + ('0x416b68c682996f3a54f9c8e4074464383e6dc78c', 0, null, null), + ('0x1ce8941d3bd09784cd31ec13d066f5bb75924724', 0, null, null), + ('0xaa459531ffb679b5e0a3910b6e28d2b20e577968', 0, null, null), + ('0x7265abd33cd136f911a1f4d504744c80116294b7', 0, null, null), + ('0x2cbfb6e19ec316c46ae2a777c8fffe91bdcd334e', 0, null, null), + ('0x1614f31d8e2c4b78911e378444a553aad164d744', 0, null, null), + ('0x7047f53cc39073089a02ba87f8e46559b877daae', 0, null, null), + ('0xfe26b0812ede6c2c9d4a80a6a1a43680a71cd275', 0, null, null), + ('0xfaedc46c097e9fe7c27c86d14855297fb49ad0c2', 0, null, null), + ('0x7a4e1ca7f1d56dbb19265e3f825ccc861f2ce018', 0, null, null), + ('0x9f8a5e14b1aa2cf1884911a23240e307ba96df0b', 0, null, null), + ('0x6ab74a1358630634f2433ebec1eef676f0f32303', 0, null, null), + ('0x9b855bbf9c1f602d07e68a2237c7b54c3fd42645', 0, null, null), + ('0x3c0e36dbd9cf249ed8bbbb93d6cd4b5e318455ad', 0, null, null), + ('0xefaac9ee91488ed9240cb0268a4ea40547fab5f7', 0, null, null), + ('0xc16168e3f3d6ec1724cc15cc190ff9e233e76613', 0, null, null), + ('0x796b82dbdf7ee969521a55de6bf128ce67999364', 0, null, null), + ('0xcd1b23339407b11c8628b29cd7a16124dd73b722', 0, null, null), + ('0x27b61bae54b622540289b90b35f4bcdc34f081d7', 0, null, null), + ('0xeb147d8ad8b50176dcb9a1cf9931f25662268442', 0, null, null), + ('0x7c1d5b264932ad350a37f69def6284e3676203f0', 0, null, null), + ('0x20e9e0da8191ae9ec382191cc75cf9d37c57eecf', 0, null, null), + ('0x41b72ebc7d6c9f54f8405964897184b0ec9b9814', 0, null, null), + ('0xddec2683d394d51c2afdf3c9f93fea6db9a6547a', 0, null, null), + ('0x952ff8ecc09e16d426873afe59da9d0576aa994a', 0, null, null), + ('0x306ba6827e7c6fb137ec8efda7220ad4c8a3cf06', 0, null, null), + ('0x3d1b1e40382fedf81208742b13aa3d237b30e0da', 0, null, null), + ('0x44f76c0d4b1826f3265ddd847cead78ca39248eb', 0, null, null), + ('0xd7470828734ac7cf2a28bcd9a77308b8899c99e8', 0, null, null), + ('0x4e883f4bd207b2b9cb6d32a67288fc02f4c305e5', 0, null, null), + ('0x6dd121d67ac13467d8cf594bbcaa9f126293b1a0', 0, null, null), + ('0xfd719bbf85a8b8c0c6b16946ef1e3c8c93ac3b66', 1, 'https://euc.li/twape.eth', 'twape.eth'), + ('0x769893a94e66297943b728a52bee02afbdd5e4cb', 0, null, null), + ('0x9c064f87ba71a0606578d7a470e68a734e6f43b6', 0, null, null), + ('0x58f23602c784aaae44090c845b5d12421913321d', 0, null, null), + ('0xb092651a4a4cef165d47f824411790202e6b0502', 0, null, null), + ('0x81ebe8ee7b51741fd5dad31f6987e626a9bb8111', 1, null, 'gov.hellenstans.eth'), + ('0xfafe19575c164b87c8268f21c8f6bf29e42c6385', 0, null, null), + ('0x2a4dcad6e9715439a6ce4b0217ec0a32d9326424', 0, null, null), + ('0x7609bb972d80de8de6d5a546a026500f5290af7b', 0, null, null), + ('0x38b4968b568480d97f3e218213a89a2a29336c4e', 0, null, null), + ('0x543572fd96daf365a97edbce0606da4c80f84912', 0, null, null), + ('0x13e7c5fbd317bd465cfd960d7c82c4a4bac4f209', 0, null, null), + ('0x74ad56fa24d1163cfcab69f20a6016b6ef9c325d', 0, null, null), + ('0xec8e5c7b2e715439aa0b613649b0465248905df3', 0, null, null), + ('0x621b5224abe44bc5d940ae3505ff89071e699374', 0, null, null), + ('0x7bf4f71449079adecafd29d8ba50844c6de48a20', 0, null, null), + ('0xedac4dc925dc94c734a304db480a353b61af4b6c', 0, null, null), + ('0x3434f0bc0fca2d72fbe84a29d5d6cae29e18bf72', 0, null, null), + ('0x54c1a32bcbe78b6fc65178cdd565f23b4d5bb132', 0, null, null), + ('0x473c3552e15ca2d7ebbb6ba0af9bcc478c8a2b72', 0, null, null), + ('0x527a3ab8f1ff9172fd7d380863c54edec60bd41d', 1, null, 'theblackpill.eth'), + ('0x6ffc916dbda4467b544305972b0784024ec56b3f', 0, null, null), + ('0x5a333c5aaed62aa473e404169f9f8f989be5dbfd', 0, null, null), + ('0xf1926418c680f3eb7f31230b46aeca191fc57103', 0, null, null), + ('0x706568707620a44a2463ace5edac48fdc864e9c5', 0, null, null), + ('0x4a7a1e42554c7780e328b67cd488ecff00d9f63d', 1, null, 'ogk.eth'), + ('0x5a36c6cc556ad30ff67410df4a72de2df21a166e', 0, null, null), + ('0xce40067bfdcc18ba120e85a14ebeece3958a24c6', 0, null, null), + ('0xde5fea952e4c8f3b2c339d2e30a9b4e0d286fe16', 0, null, null), + ('0xfab07adad054b367adb10315de31b48a3ded69c8', 0, null, null), + ('0x1bc86a67ec3779edc31417f88f267d55c66d251c', 0, null, null), + ('0xe37d90c250d137d5288cb330a83882e1fa37579e', 0, null, null), + ('0xb4a517211b391ef798ef133eab0f699ccbb3430c', 0, null, null), + ('0x6859c31d8b03d6ba27ec739b3e658ab53199c014', 0, null, null), + ('0xe1a52730911ac5ee4e0ab6e51de1121587fefac8', 0, null, null), + ('0xe103e5f8e4f3b1b5b5ca817220015c8fa882786d', 0, null, null), + ('0x56403cf17ece514c1736d49348fd5b860f67d205', 0, null, null), + ('0x57117ad843d9220d39f166577254061d5a60a6d1', 0, null, null), + ('0x62f32793e460306c6432443f7f0f03f4153eaf7b', 0, null, null), + ('0xd68b0f192de0c4852806102ab571f131a26b3101', 1, null, 'brunocrescimus.eth'), + ('0x4204a7bd44959f90bd47331cffc20230da53c220', 0, null, null), + ('0x77d04705f14b847185d0ac92e726f333caaf2b39', 0, null, null), + ('0xdcfbb7e2ebd82f4bc082304cc8179fbbea729e9a', 0, null, null), + ('0xfcbdc53cd040a059ec477d4442cfc839949bed90', 0, null, null), + ('0x856b63349fb6c818ea7cd7305483ae0ef6956f6c', 1, 'eip155:1/erc721:0xe4d22f2ac0fab6baaa32146727732e496752e3cd/1', 'awbvious.eth'), + ('0xba2d183d78a7b87ad599f7d7cbe05f17c7dd5098', 0, null, null), + ('0xcccd6e5c48e614d8a9694c83c85cabbb6279a056', 0, null, null), + ('0xc1016197380ceb796be44aee4286748560185213', 0, null, null), + ('0xa20df2ad034138d247bfac04fb3564bdfecc90f4', 0, null, null), + ('0x78999155c4454087100619f4004509b88e6db590', 0, null, null), + ('0xea35cbd55602396934817a3c886934e781f7673d', 0, null, null), + ('0x668905d364092afe091045fcd243687b6a82c2cc', 0, null, null), + ('0xbfbab6e93095dd75e2305dd40f84c04ebdc263d9', 0, null, null), + ('0x298dcca37cd7561e29b0704acb55a4d06169a962', 0, null, null), + ('0x2ced9043c8b06f12dd052f5b78bdb08bc450332c', 0, null, null), + ('0xddf6aade2ec59d8143c48a13ce55d928f02f6dbe', 0, null, null), + ('0x81172af0746014ca5127a93f7ea818cbfb1ffca4', 0, null, null), + ('0x8d38e4e4ea6d1df6996ccbbf2bb839c0821690d9', 0, null, null), + ('0xaf6235f8ff237aa7bbddbf210bb242ea3249f10d', 0, null, null), + ('0xc288fd9cbaa2354ab66cba9504fd2b4491388b05', 0, null, null), + ('0xf81471f7a96a1bc863c7675587c0e1b538780473', 0, null, null), + ('0x3ea1d011e4cb0103b67dab59ab2bba0d1ce81b0d', 0, null, null), + ('0x5df4142236abd324ea9d2c46f214f601d26d9dac', 0, null, null), + ('0x1f34a60d08fcf60756633fcff1e23786305f5731', 0, null, null), + ('0x8b194fca1641dd1a2e1f62f6b91cadd51d576aa2', 0, null, null), + ('0xf03c33473291e9a0247cd1a70de048554986a707', 0, null, null), + ('0xc093ec9735d470948778f2f334f5099f49a97242', 0, null, null), + ('0x642692c3643496f22c483e63037e8c9b2388a15d', 0, null, null), + ('0x0d277979cba73e3233d239b7a44740ec84d1c2cf', 0, null, null), + ('0xfa13bfc5e4193726ffcb32cad9ccf57cf6a5b99c', 0, null, null), + ('0xc851ed6f7b9e0b0bc20998c9fd6a133fe978ac1b', 0, null, null), + ('0xc3a6a1ec1e5cedb047a2778c8ed1b0aa6e5be082', 0, null, null), + ('0x1eb97bfcfadce491290596e23566f30b33d06430', 0, null, null), + ('0xa64ba55010fb4d362714c9c43ab5edc0d911d2d6', 0, null, null), + ('0x23e730e46284c706a511b026d514b04f111d0cb9', 0, null, null), + ('0x363f0f4fe58bc543944caca68f22ebd398e50fa9', 0, null, null), + ('0x34d35d8846a071bcbd011f728b0d3f52fa375b07', 0, null, null), + ('0x0356f06f0a7c77bf7c1dc546c706a7483f931b65', 0, null, null), + ('0x75cf884a608457d323472c13a341eb754db4c111', 0, null, null), + ('0x3a26954693ae7e359fa34bfe2f8d8f31312d7539', 1, null, 'lukas-preuss.eth'), + ('0x8ed96d596cfc60cd505748657dfb5478e072ac0d', 0, null, null), + ('0x85f10c4a7ab9663f227eb7ec9506619212f4ff8c', 0, null, null), + ('0x4af926a219e09fe5dd1ee16d7039d82ecf979148', 0, null, null), + ('0x707450d5b545fae1432031899e2a19e2b36274bf', 0, null, null), + ('0x0971508fd715704b3abf6a7f11c45c1ddabaf73f', 1, null, 'protonpete.eth'), + ('0xdccc5f3bc8330cdcc01faec924baa1b6585e1319', 0, null, null), + ('0x46fcd6367ddb602ca2127e90fe0df01728a6c951', 0, null, null), + ('0xbe4fe8998327980566f9c586b3a881cc407dfb60', 0, null, null), + ('0xd662b9b1aa394f7e5f671f500bfcc1582b3e1c1a', 0, null, null), + ('0x618b7fb0686b1bc7cb7a5b5f00430bd46670ad1e', 0, null, null), + ('0x8bf68b377dd4eb5296724a496bab40fc18a54488', 0, null, null), + ('0x8b5ba346b25dd55a900d34ed8e24909dda5ee695', 0, null, null), + ('0xfd12fb55426ed04641ef1d7178d1b6e43815c6c4', 0, null, null), + ('0xa17be5b1af2325ba8257f1e7f8961b44d5055cfd', 0, null, null), + ('0x19bc97469dcfb7c9bc2fa46e7327cd2153359e8b', 0, null, null), + ('0xa543c6fd3cfd5c1733885934a5063c9ce46b526e', 0, null, null), + ('0x80795b7b2d2aa0d3b10c665215d1a2e2442f0889', 0, null, null), + ('0x54cdd27399ecb738e670b3fe078f3419e1ffc7f7', 0, null, null), + ('0xf37841f3511abe49e416740682eb4bad403e7a14', 0, null, null), + ('0xbe0df005fb9b5a7fd6f5a04b815f1282dbd617fd', 0, null, null), + ('0x92d04bbd1bcd2fd8e6f3113c22f47370705671ca', 0, null, null), + ('0xc5b3c384a9f6ef0ff9f37016d5126cc5fe18069c', 0, null, null), + ('0xe961935288b7c6d9735b46e6bdf6d960f967e7e0', 0, null, null), + ('0xa9aa9bc2d2bee16e5149fd5635e324df2fb8a7dd', 0, null, null), + ('0xaddf9bbdf42c66932e2cf37bcbd0e8be329bcc8e', 1, null, 'tensix.eth'), + ('0x5b2199ec24988cb49594c3c54430d8764c64af32', 0, null, null), + ('0xc4ba6ce34450e6e3a800669a6adbc4b5992734d6', 0, null, null), + ('0x742b6de8cb16327b68c83e1d6433174db89fd2e0', 0, null, null), + ('0x31304a15d2b0bbd05d4089ce9e6167a400751aca', 0, null, null), + ('0x61dbf2d453b4aaaf9c5e2a2653756c59c9a114b2', 0, null, null), + ('0xe1fde9ab0c643778eb27a1cf821dabd23bdfb70e', 0, null, null), + ('0xbc722215f8696e02e5557c5848d7fd14d076609c', 0, null, null), + ('0xed1e25dbcc74d2570701e3ab5a6df16d42f4bdfe', 0, null, null), + ('0x1ab14304f342cd5001e52d3cca0f8d04530e1115', 0, null, null), + ('0x0cf5fd9a042bc72a7e8af3a4eb4e2d376890264c', 0, null, null), + ('0xa58f5b9f56ee8ff8c06fec57390c0927f95f88fa', 0, null, null), + ('0x7a3b154eb8d8aac303fff1c669c80b57f35f5237', 0, null, null), + ('0xd5533ba8c34af2ddc035ba5d5a034f9962b33118', 0, null, null), + ('0x6586e6d49ce53f4ef95247d7cc7e60caf05e9b15', 0, null, null), + ('0x61689f5c9295870866806a89f853c3978879fa29', 0, null, null), + ('0xa1e8aec1f7e5c1f46c998cf3965a1982d815e28e', 0, null, null), + ('0xf00b5338810f97da3aebf91771afd6239ca6ab4e', 0, null, null), + ('0x676a3415fe9bb5577933fe42f9e2d9e567d7780b', 0, null, null), + ('0xc81b9dfce4fedf2fae001c2e2a2a5c60ad84c537', 0, null, null), + ('0xf9465cbdef16a7e0700698009fb0f410fd52e0c5', 0, null, null), + ('0xd57f48d0777a0fc928a73e51e9c85105b4078a4f', 0, null, null), + ('0x5897983033e0c43db23c768e43b14f66e1084954', 0, null, null), + ('0x9d8d7e4e328912d00e5dea3678dd7e96be915903', 0, null, null), + ('0xe26be241b7d09a68e7c0d186d6aa20966c5f7f62', 0, null, null), + ('0x54d8082929c694ae0736b140fc3c403e4092a6bd', 0, null, null), + ('0xf78f886444ab19e77510d01d06d0bb198e748fc1', 0, null, null), + ('0x96cc2da8c79663579a25bdc807c9990447939dd4', 0, null, null), + ('0xe1c67fda681438dc85a6b4046cefe5027ec391fb', 0, null, null), + ('0xcf6e7983381c4c2d2575ff9263671aceac6cec1c', 0, null, null), + ('0xe1b057b519317482117624469ccf4cce6d22e68c', 0, null, null), + ('0xd0638071ffcbd796410a7433fb61fbcfd9bd11b3', 0, null, null), + ('0xe84f8a9790df6c4d55e95a6bc1b5df0523bb6459', 0, null, null), + ('0x8e55e432db0a05e8a1de9389ca043112967e5c97', 0, null, null), + ('0x49c2c779dcf34609f37742f913fedfddc8ae7409', 0, null, null), + ('0xbc6359d32889743b9838580692e554f47b777e07', 0, null, null), + ('0xc5467a50929365a58d51db14d5459bb8d07516b0', 0, null, null), + ('0xbb0187b3d3da48faec9c0dd264973a79c834165a', 0, null, null), + ('0x5215af5105f79cb9a29ff3135871f357b51a66a1', 1, null, 'sarahtulin.eth'), + ('0xb12953aff186b57c81b36db1c915135837a30b94', 0, null, null), + ('0x0d05d736db2b9923cdf3fe967ea8b4e17da94c5e', 0, null, null), + ('0x616f3e30d762f4792538b3cfae4a313021478533', 0, null, null), + ('0x34324b7a8b271e46b0f3c28f7d1be7f4c0dba7a6', 0, null, null), + ('0x0876dac118dc190d908bc6d530e851069b9b686c', 0, null, null), + ('0x4c4744784437a3d9d7c11f5477e16b1ecb7b56e5', 0, null, null), + ('0x3d4b2941ca2acbc6e7586e4c6160e4a0d891088a', 0, null, null), + ('0x7eb0069fa949c4bf77246ee745b983132d390d95', 0, null, null), + ('0xb5ba4a130f9e30036d1c1db11a8913caf3acdeba', 0, null, null), + ('0xc5e50fcdc7b75080334e0332c37d0d0bc3441d2f', 0, null, null), + ('0x9bce9180c44f6e6403d8c043eb3f0ab91fb78278', 0, null, null), + ('0xfa841628f04b1350c6c0ed5dd435b2e4c3526ea3', 0, null, null), + ('0xef1555a5b28fe857998b546eb6bc04c8fc1a23c5', 0, null, null), + ('0xaf0de9616735ed16e25f0c08829d5e37068dd375', 1, null, 'slavni.eth'), + ('0xfc8d3b0a2d18286d2bfa1e9ade288512ba095ec6', 0, null, null), + ('0x6773382992995ca6bb0b2429dcd385d045dd1e2c', 0, null, null), + ('0x61ba46a9032b0e6f376bed3ea32d66fc8545c4d8', 0, null, null), + ('0x09fec9c95681b8186f09deed526630258c46b41e', 0, null, null), + ('0xf2aea72e738dac877aac220394ff3addd73bb3a5', 0, null, null), + ('0xc5e4e9ab93fd061baba4f335b409e33674a2e1ab', 0, null, null), + ('0xd55a4ce318fd3282d69a2cc7adbe33c1c1cb271b', 1, null, 'jamestown2009.eth'), + ('0xb2abd0a37bafaf4ace8f9e5e8797f07882bd03e2', 0, null, null), + ('0x57d9f09eeff8877a9f94aff75ce582b85dc56f7c', 0, null, null), + ('0xb0fed7bfafc832dfaf664f0ab6d3c8c536fcb455', 0, null, null), + ('0xc83e9ebbb9dfb4acc515ceb3e2f694a2f28f5075', 0, null, null), + ('0xcbe442a4edba6ebac4b276b43755aaca0af92d9d', 0, null, null), + ('0xc2b755e8efdb1b8c30dfcdd0f46a330b089da822', 0, null, null), + ('0xf9453826c08782715f5e614d224622cbde0bc978', 0, null, null), + ('0xeb30b18d22648ddbe9318fb1e0a4d669916fb179', 0, null, null), + ('0x6f3348d5e3f9370132f705923207a8d5bc9dabd3', 0, null, null), + ('0x3fcda596066c74a02501731f11ffdf3cea786e5a', 0, null, null), + ('0x672efc9a54495073e20f30dbb5fc75b37159430c', 0, null, null), + ('0xaecb945c0187aa0b78380ed274b2a8795956ce1e', 0, null, null), + ('0x93ba4aee496a71936fcf5239600ae47e8c936bed', 0, null, null), + ('0x99aa7a0bb20a5d48688c20be72698a45a388a653', 0, null, null), + ('0x2a0ba4209fce31ef4e1f980a2154c6f5e25da5f6', 0, null, null), + ('0x537cab5aede3d773300fb13c07f5c0e0fd121d7f', 0, null, null), + ('0x29f82d09c2afd12f3c10ee49cd713331f4a7228e', 0, null, null), + ('0x4f0c9ba189fd87ef75c713f8d99519f9c82115a0', 0, null, null), + ('0x0fdeb94de783e6addda9197aa18cf6adec5ce919', 1, null, 'bagsarepacked.eth'), + ('0xff17b13fde1b3738b473d24ca640379f8e163738', 1, null, 'blurprotocol.eth'), + ('0x5c524fb730569715fcfabc4c05293793f69a3a33', 0, null, null), + ('0x55139ed41c01b249f2285b629850805d59cc89c1', 0, null, null), + ('0xb3f5eb38d617be12260d22bf963d40836b9c8aab', 0, null, null), + ('0x531811a057a1dc20ca492cd68716da11c26004c7', 0, null, null), + ('0x8ede88e2218e362a1f3cdf41c436d91182b9fc7a', 0, null, null), + ('0x4624f295080d69bcfa68984984294c3258815375', 0, null, null), + ('0x0fe1621efdac9b80fe2957f9ac9aa346b1589e51', 0, null, null), + ('0x625855a5beddbf9ac009f521e68a063ab097bd73', 0, null, null), + ('0xaeca2aafd6385fd99034055f5190be744bfe6d52', 0, null, null), + ('0x24a117f08ae5b464848883131edb2bc46e9d44ff', 0, null, null), + ('0x3512ed667d93e81b13e5d9770347dedbfc4ffd3c', 0, null, null), + ('0x1b9d7087da196c9bde8861694901eef94f738abb', 0, null, null), + ('0x746b47670d2979a5b1d0763131503feabd03722f', 0, null, null), + ('0xf5793f0affadbf3421a775d4e101f169c8abdc8a', 1, null, '3pm.eth'), + ('0x9e1372563e3860dff47f65e82bbe14cbd8b503a8', 0, null, null), + ('0x565f3324e4e65713f0cbd8cc842784dca869ebbc', 0, null, null), + ('0xbb1fb1b1bafb60ada6c355532d582008b48f3960', 0, null, null), + ('0x0848db7cb495e7b9ada1d4dc972b9a526d014d84', 0, null, null), + ('0x9aed09e3a14ede77608c47fcddf5c19d5e080783', 1, null, 'depros.eth'), + ('0x9c859818b03b3fefb3243b968be41d47c4132770', 1, null, 'euphon.eth'), + ('0x4f2222879d0f2e35dbf0d9e35a228953b71181dd', 0, null, null), + ('0x8ffd971a186ea42287cad53aa15c771d1f66d1ee', 0, null, null), + ('0xbf560153bf00efcf37e83df01a4c9afd2cb091cd', 0, null, null), + ('0xe67b4a05168c7701aae0db52c6b11d3fc5359dfc', 0, null, null), + ('0xc88747107f84353808125c7d208dc2f09c532651', 0, null, null), + ('0x7c8de4e5931485db36f755682b84648267d003ec', 0, null, null), + ('0x9fdb286d321137fdfa2a20293507401ad826204c', 0, null, null), + ('0xceeeb572595ade8bc7e8758c1cc42d2e93da05df', 0, null, null), + ('0x527efbb2d9743f707781b949e6dc9abff59eabf0', 0, null, null), + ('0x8fda86ddb38f9f1d2ea6c54f163315d8ffc1d478', 0, null, null), + ('0x11092cabb32536589d11442b01554a4edf35a862', 0, null, null), + ('0x1cd289b1b059540840dc173d47190f8b2491fa5e', 0, null, null), + ('0xfbc0b92b45e0cae69f9a665aea0b5929cb015403', 0, null, null), + ('0xf70fe258d00c13765b6aeb32136393db1e203af2', 0, null, null), + ('0xf8bb93597d1d4748e4fddb0bb8e7b6011bc3c689', 0, null, null), + ('0xb3c3dbf05f70878945f97bd8e081866dc883e0f4', 0, null, null), + ('0xe687e40fc109080e359f5cb77df128cef442189a', 0, null, null), + ('0xf28a78466531f89c45db1b79465318319b5cdc0c', 0, null, null), + ('0xf2775f3900b514b508e2d2e17093bf6de383152d', 0, null, null), + ('0xea8bed1acee9bcc23744fb1b559b0b61fe6ead96', 0, null, null), + ('0x514106071abfafe4465ffbaa7dca0bf2cdc88b13', 0, null, null), + ('0xa2748f8b2c00a085168b12bf4a3a6387d74b7838', 0, null, null), + ('0x763436617aef4051de1aefe0805f220da84fdad2', 0, null, null), + ('0x4a7a90a94d6a66f87d7bfff1db0d44cf61e43e91', 0, null, null), + ('0x2e244b71aa91d829436ca309b63088d15fabea5e', 0, null, null), + ('0xe8cf0b110bf1cd9b0e74e8d6852eeb96c699100f', 0, null, null), + ('0x4d30c73ca651ed15bb402ca9f14ab83fc236250c', 0, null, null), + ('0xdf2503116a1dc9af476e1efd099b432b35331d25', 0, null, null), + ('0x692782fc3e405c89b9b423183993b7c72cab3470', 0, null, null), + ('0x62c08fb380f4e66700d1b9b62ba2e010167665ed', 0, null, null), + ('0x77bf7375c5a582ef42da7f923576be20c1282c59', 0, null, null), + ('0x743791d60df6c504cb2b7f8b9ecc18671bc1326e', 0, null, null), + ('0x826e66992dd4faf25a1cac2122f7bd903967bc3f', 0, null, null), + ('0x47854123ea0a6d5f6cc015b75eaac531952ab31f', 0, null, null), + ('0xd3f2f944ef8f468219fcbb649e3f8f41155c7f5c', 0, null, null), + ('0x4bd5d05d50af4dc36a42cae72a64c6e542ad4dfa', 0, null, null), + ('0xb7d81dac2fd5eebbc6017df92a2f2a81a4622c6e', 0, null, null), + ('0x648feba5382524c4e38b858dcd56c69ec9b4b351', 0, null, null), + ('0xb12a91e7d965e1fbdb26dac928b71cbc3393eace', 0, null, null), + ('0x8c70805eaba444037adb46da94fd74af637cdf4b', 0, null, null), + ('0x0fb29a71ed97a5ea9c708fc3866497c59dcbaa71', 0, null, null), + ('0xa4b70dba84704c093d274957489a73b176d23952', 1, null, 'justinbookman.eth'), + ('0x1a268a88b586b475ff4bd86882bf2f9d79875964', 0, null, null), + ('0x58f63a60ff5ff3cd7d2ac7efe0b9d4436fd4351c', 0, null, null), + ('0x149214bda0676c3c528b60c35b015605f8af053c', 0, null, null), + ('0xd501991019c71c1127aa5c653a61a7b5e6aa3ae5', 0, null, null), + ('0xc568793aa6e769efed7f553858f79211203bc234', 0, null, null), + ('0xf5905fa1ffc5640d6294a9da5f1efbd15cf92864', 0, null, null), + ('0x8bc52147b8807330d93e1944b4228e2c7bb498a5', 0, null, null), + ('0x19e727541ae70f9a9998146e927c6f9960ef7da9', 0, null, null), + ('0x4009178081f25b04cb9d13732b57f423627d7bc7', 0, null, null), + ('0x00032550944ee9b1a050796fc9074011b2f2c28f', 0, null, null), + ('0x9543746e414926d1695ec001bc7b76277895490f', 1, null, 'applemacbook.eth'), + ('0xeaed02439c0c17a2dda901637e2df6d0d5fa55ef', 0, null, null), + ('0xbec540198d5f27731024a809704df38fec0d9f98', 0, null, null), + ('0xb7324214728c4c35b27d08ecd03fcbf5bf52ab45', 0, null, null), + ('0xe215c0c28c0d1d97d9e90faeb2a43478c8f105f5', 0, null, null), + ('0xeda4edb617bf8dd7736ee2088c7dd40549a60cb0', 0, null, null), + ('0x93dd37541bee2501ffca0fdebf1e27ce97c38cbf', 0, null, null), + ('0x79e9b4efc1b40bd349d1f812e91b1524d98fce29', 0, null, null), + ('0x48f2091be8c1dd38830fb76cb8467c0ecacdbf0d', 0, null, null), + ('0x0ef81fc447590513792cabe5afc7a952f266e826', 1, null, 'mazzilli.eth'), + ('0x9cd455d3657c0ddf4c78ed7250d3f9b8c79203d9', 0, null, null), + ('0xe461e164c6c76377feb65e7b1320099c91aced94', 0, null, null), + ('0xa88aaa2def74aa9d325e25659a17a40b9c9539a9', 0, null, null), + ('0x19b45503881af00be3fc650ddba66ebcaa391ccd', 0, null, null), + ('0x792951e051b70e3798a035539930f440093acc79', 0, null, null), + ('0x753337bc3922db4e2d74b955e2bd82fe2b85be16', 0, null, null), + ('0x0757c45357306643dd86f0aab174036ed2081da4', 0, null, null), + ('0x217cd9f0015eea1f2897e74963cc4e9825888076', 0, null, null), + ('0xe316ae2de156ec1b8ea410bc41895c732dc7d64b', 0, null, null), + ('0xe03bd1123708e4fb29de61c6cabf5d29532f7fd3', 0, null, null), + ('0xf03f48ba3a4ebde7220630bf4df9b9f055f15f74', 0, null, null), + ('0xeef784f34aa98f6e580435cc0b9b59d9e94fd711', 0, null, null), + ('0xe02bb063c2b366a6ae82aef8768123b2035bc01e', 0, null, null), + ('0xdaa62a29ff68fb923d7c828e88ad75a6f9ad1529', 0, null, null), + ('0xdf1aafbdf9394e15dbcdbca0068abdabb6da3359', 0, null, null), + ('0xd8ff8648a09da514856bcc45a89ee54d78076728', 0, null, null), + ('0xe96995006efc68f5eb9f9e46aba26e9f1e3e967e', 1, 'eip155:1/erc721:0x884ba86faa29745b6c40b7098567a393e91335cf/332', 'web3mineral.eth'), + ('0xd10db8dd4a07447896c8b049aea2fbd0172d4a37', 0, null, null), + ('0xd8d9c5eff7c746fa0c8b1138f477436928b91a99', 0, null, null), + ('0xc79a376d58d68dc739a78dc23f4850dbefd23496', 0, null, null), + ('0xcf789f69b6ff311d6681a1716d6be4400a9224bb', 0, null, null), + ('0xc7458ca5ca82b9947bedfd41295c661dc3851b1e', 0, null, null), + ('0xc3f693f468d799bbaacae9173e3c1a13d7c7751f', 0, null, null), + ('0xc3f824c4be1181a082a1427b31a559fd17a61a81', 0, null, null), + ('0xc7cbc2b8b6ead19bde69590b0cd06d7ccc602736', 1, 'https://euc.li/jayjolly.eth', 'jayjolly.eth'), + ('0xbe093163023769a88949de9a8c7d3d60ae272b38', 0, null, null), + ('0xc080e234dba2545e93872ce6e96e683a4e29046b', 1, null, 'bigds.eth'), + ('0x6dc0f8a5e5ec573e255fe979abe4c54fd007aed6', 0, null, null), + ('0x327904bc7bb3aa2a3a9867927fc6773a7bf0d45c', 0, null, null), + ('0x01161f540adf1f7aa5b09bb479bfb94112def37e', 0, null, null), + ('0x2679856f917004c41a9191e650f3fce5cb2eb0bf', 0, null, null), + ('0xa009dcb2d780976c629502f3a8a0a5bc90c490f1', 0, null, null), + ('0x247c92e10796da3113882b98c919a33651f0ed89', 0, null, null), + ('0x1e46fb33a271f19dc93050f32cc9fb30f25a7f5a', 0, null, null), + ('0x60247492f1538ed4520e61ae41ca2a8447592ff5', 0, null, null), + ('0x22f2caff4bbd9a3e3ffc935639e372a38bfd6654', 0, null, null), + ('0xdbc055afa60b7a8516275f16ea14664dd8486bac', 0, null, null), + ('0x0eacf70ab0b22cae824eb0a2c3fe71ac8c3404d9', 0, null, null), + ('0x7c126807095b228b3534c5ab937500d5829175fc', 0, null, null), + ('0x2aefdacfc632a5f862b3e072111ef6d356e28986', 0, null, null), + ('0xc8b61af5e9f8118546b0634f4249f77aa6b43e66', 0, null, null), + ('0x1908a3232eed9186b4a5b666075711d2db0200e5', 0, null, null), + ('0x06bd3d22abd1f41402dea307531ebee24cdd7f06', 0, null, null), + ('0x5caec7ac41c1db9884a5cd83be5c7dccd11f4def', 0, null, null), + ('0xa3607a6aa36a7e134881f6db4360081d3148336c', 0, null, null), + ('0xed993ee2d7fdcc4898fa0b1799a7348e26313710', 0, null, null), + ('0xc7acf6545e848a5e2e54033897292df4b05d9451', 0, null, null), + ('0x533491726ee661e790f948bdfe0ed41a13002ba2', 0, null, null), + ('0x1bcddb2aae6f2accb3c5a3c4ef6b260abedf71b1', 0, null, null), + ('0xafc114a21ac519fdf0a5122b80934dee1110963d', 0, null, null), + ('0x41ce7e1a34c84d251838a41edd53b4e89b02cad2', 0, null, null), + ('0xfb6a3a5bffc0f026beb0eabe0b50899eb63590ca', 0, null, null), + ('0xefd4d9f51aa5f137cba17c33edb5166d938a0e53', 0, null, null), + ('0x1f0853a4d28aa25a81dfd97089421f5bc4ea1015', 0, null, null), + ('0xa1345efa53fce0456113fa17a36fa740c158ef85', 0, null, null), + ('0x4e79b7d76bb8b0ec624de532ce1a5e1a7f00d228', 0, null, null), + ('0x0e85852e8ae0c5afedc78090eca5f6a360fce4f5', 0, null, null), + ('0x537befe4eafeb4c2bdcd739cda38e2b3e24cc795', 0, null, null), + ('0xa6581ff408b89a71b8fa86b2043d40de0713ee4d', 0, null, null), + ('0xe868b8c09fd2c8c3f74d54f34f7325ab1c94c71f', 0, null, null), + ('0xa0b3ca9267107c0a08077e4206605041c384fd01', 0, null, null), + ('0xc2fd73fe73c163d3e3a395762016cfe8292caf79', 0, null, null), + ('0x653609ae7eb3a6f5ba30a0546f0593cf8ec0a85b', 0, null, null), + ('0xb534eda46d40224b9aa96f70bd908e9b4dfae11f', 0, null, null), + ('0x45cf34ef1716936e3fd420e3d927efbd78a9b1bb', 1, 'https://ens.xyz/0xbokonon.eth', '0xbokonon.eth'), + ('0x05d8db91f5dc4e47483e0f7e62cdb96fd94eac35', 1, null, 'cryptocuban.eth'), + ('0x6d0bbe84eba47434a0004fc65797b87ef1c913b7', 0, null, null), + ('0x8cb8184f44016e8dfea6dcd429ab7530565f836d', 0, null, null), + ('0x3a73f9b32fe84fb0b1c3b23739a84adf6db1d46a', 0, null, null), + ('0xb9e34ab79c29f182f4ff40399d6e1885abe23067', 0, null, null), + ('0xe6007ee5076792d6b30b88eb4d8f44d2724fd954', 0, null, null), + ('0x3d99021cc2480dc1288338696538786c45ce3af6', 0, null, null), + ('0x37337a521aa3b3b895fdb1e33ccc560f4de24b25', 0, null, null), + ('0xfc763fc9e21be7bcce44275805f2a1fc8dcf2564', 0, null, null), + ('0x47c7901b7eb9db736da98b957ca3a7f8e6c984e1', 0, null, null), + ('0x5f75b53f8ca3fbbbc51d06ec7b0edeae0aaf4c50', 0, null, null), + ('0x3c9eeb2c5d74a79a0e65ca4e595891d76e171cac', 0, null, null), + ('0x3158c1f5f3dcb85f2902dfaad7883444af4ba000', 0, null, null), + ('0x3b975d0c078ef4573a675a291c57b7c4bba32a1f', 1, 'https://euc.li/edenboost.eth', 'edenboost.eth'), + ('0xa015972b35498fa91f14f445c0cc1c0335607c03', 0, null, null), + ('0x0b2c98aa37f11691ac2919d55fed8570a66185c9', 0, null, null), + ('0xc862ccbac277531af05f0e2999f42e173fe193f5', 0, null, null), + ('0x0264727fcb63f2cc6f3e92e5688a946959dcdfb3', 0, null, null), + ('0xf95fdb86dcf295124493c50ce01024b08d57e3b9', 0, null, null), + ('0x9a8d8bfb7ff6fd39d078aaa4e0dce8f94b1773e8', 0, null, null), + ('0xe33a480412c86500939014c00f7add7522675dec', 0, null, null), + ('0x281f24764c3fcf1e787753a663f7585c17b67609', 0, null, null), + ('0x12d5ed341da214f90296f2c6e61b4c9a0c8479e5', 0, null, null), + ('0x78d4bb599964955e51ea4f5c767772f60d791f50', 0, null, null), + ('0xd361981613f3e77b3ee3c17693cbbf08850d1cae', 0, null, null), + ('0x3c7449fafc5b5c1e8768d89c3ba5f1b9075d6c3d', 0, null, null), + ('0x2926c372107d9ded122d2c28bb0c98baaed0754d', 0, null, null), + ('0xbf3c977e607b6f73317b05c0d570feb1fcf2183d', 0, null, null), + ('0x0a69838fe023eebb61319f166244c60050f22a94', 0, null, null), + ('0xbf986e4fccc87e6ac2887979f8b0304bfa584305', 0, null, null), + ('0x40697b5367e417b97c9853622644fb8aa023c539', 1, null, 'zachgeglein.eth'), + ('0x8aae9750a48da5240a19f16c929a390eb1f23350', 0, null, null), + ('0x244980d84e077765f167cd0d64c5c74645c3824f', 0, null, null), + ('0x1446a668ba67f905cc95404c7f2360042a7f2b20', 0, null, null), + ('0xbe4dafc2d31ccdcd6fec93156503d09cd5a1cef6', 0, null, null), + ('0xe7d04176c22de94999626dba9d4eed5add47184e', 0, null, null), + ('0x1b95b8a82966615a084397932bcbf514e2be6f20', 0, null, null), + ('0xbb4d7848382715673d485b5a623be83d94a93880', 0, null, null), + ('0x193abfd94a4ffe60afafb7743b8439ec90684d11', 0, null, null), + ('0x404f7b8b95f9b57d1ac463777f22ad8c37c12e89', 0, null, null), + ('0xcef121a3ff830af4ec84085ec3aed8fe2ae4d88f', 0, null, null), + ('0xa33111db0f411a1b2a279bd448a0f4c618724085', 0, null, null), + ('0x0d936250edd40f83b80d49c3799c591f7130e4c9', 0, null, null), + ('0xa3a57e49acd7286fa24e6d403d2dc71a5e01e341', 0, null, null), + ('0x2808b8d10619e1ff6cc8df15381ca5c1352150f7', 0, null, null), + ('0x083cd9e6875e84ec6618afef95c847c098f06515', 0, null, null), + ('0xb4ad5ccd105bb652216e8a5a6528ca7b04969887', 0, null, null), + ('0x9e9bbd60b1f2a177d11280636ffe4892a89a5912', 1, null, 'flubdub.eth'), + ('0xc42a038e423aba5116f33ecb9dcb5cf933ec8fc3', 0, null, null), + ('0xd66a1f743b415dd034cc68eb444b6dd8cb6d6112', 0, null, null), + ('0xba37cdc1d2755382479826692073fc1bfcba2af4', 0, null, null), + ('0x2fa2138478a4fda31d9f7e15b5e3907cdf064433', 0, null, null), + ('0x31f0b3c4e84f111dd5eb76eadeceb236d191bce4', 0, null, null), + ('0x8a14416efab582c8482f7fe4efe47a3791f6d0f5', 0, null, null), + ('0x6dc609113ec34108ad7a3235b8772befc2ab2cbb', 0, null, null), + ('0xab15033649df6fe91c2c59293e5ae4e6c2e75dd8', 0, null, null), + ('0x2fbb26d4b22758d9ecf11d04f1f2d5cb9c3e2926', 0, null, null), + ('0x69dedb7456038123fdf10bcc9b0b64d52424b764', 0, null, null), + ('0x14b46ced7c91bd5b57e566fe2bc0ad64a04775a4', 0, null, null), + ('0x1f4ee74c7d5b3cd0f085183fc14c1f90e15f21ab', 0, null, null), + ('0x3671861e805f45cbda4a715402dfe301d465b90e', 0, null, null), + ('0xf5eaf9a0086067933b056946cb768d3d78033d8f', 0, null, null), + ('0x3f286c1796fd88a59fae248da7db7afe7489ba78', 0, null, null), + ('0xe72fafd6859b6e496e3c380be3ab99472086ce17', 0, null, null), + ('0x30dc29f4f37330e59612d11ba04c3cab9ab5a3f3', 0, null, null), + ('0x64623240360b6707f979f667e6137bb100b2cfef', 0, null, null), + ('0x09f44fe6ad938bae7495e0b4939f83b4bff0feb8', 0, null, null), + ('0xde56307c503872b9cdb52440fe139ad56906f903', 0, null, null), + ('0x0fc96663592e92e25832d6f5485eb63866cc020a', 0, null, null), + ('0x15306a6caafbdaf9210fe07f5f5d866c12aeb288', 0, null, null), + ('0x3937bba560bf50c7a6d3958e8d5e8460e647a29a', 0, null, null), + ('0x483424f8b15abac898dbd00ca9ba514f2c3bcc1e', 0, null, null), + ('0xa20d50ac1e3ac665ba16fb5e7c673a0f62fbe112', 0, null, null), + ('0x9b6c0c5767172db35c862aa4187372b7acea75e3', 0, null, null), + ('0x3d8e5d25181a371d7d0b81d73b5813b40e857b13', 1, null, 'jewelspersecond.eth'), + ('0xb95c0f5263858a18c69065391a4484fd93571e4b', 0, null, null), + ('0x18dd861aeef62710261483dde92ae06f3c78f4f0', 0, null, null), + ('0x64580109e45548cf6cff5dda923f52b2ed03bae5', 0, null, null), + ('0x3ce59ab649a1242d7c49731fef41a834b5a7978a', 0, null, null), + ('0x83e35ad204a7436893c73503c77fd113c318f913', 0, null, null), + ('0x3bde07a4ba78ab56f7a991a2893978b061fa8e84', 0, null, null), + ('0x22de51295ec1d7f853eb3516697c3ead96cf6514', 0, null, null), + ('0x806db6c5cbdaa7dee05c001863fae510a5fe6871', 0, null, null), + ('0xc66e19c252e620213d634fb94ffc93c4c9fbc6bf', 0, null, null), + ('0x0821a3fbb55e0452632d303162d5a0e49f9d43b7', 0, null, null), + ('0x305efe618ce64a799044b3e19277be6d05109e4d', 0, null, null), + ('0xc155efbeb10ad1debedcd4f05dc0e669e5ba7e25', 1, 'https://euc.li/db4336.eth', 'db4336.eth'), + ('0xe8a741fa0ad19252d5c0244c441018eb20816972', 0, null, null), + ('0x234252dc1c014e1d68fc6bf6f04c319c530e87e5', 0, null, null), + ('0x39325d041c6df9b2db18fb6c5ae1007dc1bcd0ae', 0, null, null), + ('0x4c29d66a39c2c0e72ae7d23ab2b66817c9af6b9d', 0, null, null), + ('0x9144bd72d685b0b75a200996ebab6f58d8d77c82', 0, null, null), + ('0xa397eb4360826e175314923b97d67e9075b8d2fc', 0, null, null), + ('0xc4afd7f927ff1fc16bcdcabd4b094304905a2b4a', 0, null, null), + ('0x70ba7916970f7a822903c7a2b46e03eaf6dc88ac', 0, null, null), + ('0xefa642f446ef2412dfa2e85cb2299d32c866175d', 0, null, null), + ('0x8a7b731fa2d0081e91bb214fba53199fb425eca7', 0, null, null), + ('0x043729b2cec8f20c77bfb8b2c596f44a47676222', 0, null, null), + ('0x22ea9223b1b3bf6c78e234669ba1f199ea5e4381', 0, null, null), + ('0xc9e439a8fd8273d3ed80378233d7a3a6a77e006d', 0, null, null), + ('0xf249b54d9bdc58906516e3094caf9ac3daaa9999', 0, null, null), + ('0xafe7bae6826bf9ccffe2450b352ffbe2f41d5b20', 0, null, null), + ('0xfba48807e041791950489be9dd7b884723fce2a1', 0, null, null), + ('0x4dff362a8f9dc12ad17ff6da94540dbfa21f88d1', 0, null, null), + ('0x13bc311b10653f454d3b624e5422ac1745c7d3e7', 1, null, 'warningtones.eth'), + ('0x14f13f150a810e70a168bf377a452b9c741a0641', 0, null, null), + ('0x2cc99afaf23aa95f03c4aa2989881aa1bf5f6ba9', 0, null, null), + ('0xbf791cdadc1b015c92a2cec4b54c96d0efb484ef', 0, null, null), + ('0x5795dc26a4f4ac25084eb721c9f32812e2038294', 0, null, null), + ('0x4f7e2244cf7f66d219f39597933fac37275fcd88', 0, null, null), + ('0x8950fa87a542ee0c0c3c81ceb76b0c65ceb74a62', 0, null, null), + ('0x76e6a066190219dd929d7d5d7031fc90c4516b65', 0, null, null), + ('0x794580aeb3f98ffda6b9d5c86e5d4b957ec43374', 0, null, null), + ('0x4f1e9675aaf8eff4c98b59ee2b757e200ff7fb09', 0, null, null), + ('0xd2ca607dadde5d84bd0dce658eb32c93905312ce', 0, null, null), + ('0x13dabad6a1a25e3ca2f08f01a86a5ec39f1c1a8f', 0, null, null), + ('0x93a9df3ad128466aa1ee8af677f954afd557842a', 0, null, null), + ('0x5d2ab76cf8991a3e5c6c1e39ca5dd34ae5c64405', 0, null, null), + ('0x4bbc9a770465c97dd03b4e4d1fe97a172ae95bb0', 0, null, null), + ('0xda2d9bc8805e744dc9dd0a3ba6033d31a5a5dced', 0, null, null), + ('0x0f21870baff9bff3e3b85ac8c15024c236d161fe', 1, 'https://euc.li/cryptocholic.eth', 'cryptocholic.eth'), + ('0xa0c01bd98602c1c0f5656a316bd08830a8fcd5ad', 0, null, null), + ('0x9791944d740355272e6db5d46b0d121fdbdfe0ab', 0, null, null), + ('0x5162754d2b9d273acf6444325732eec7a2a2b5e2', 0, null, null), + ('0x266307e80607cd65511c01bfbd0de779b9fda7f2', 0, null, null), + ('0xd9861b7c2196b458a04b69de9a2f6a7b02aeb65a', 1, null, 'mevclothing.eth'), + ('0x9377c61866b597d948a98a3c8cc199e2b908f445', 0, null, null), + ('0x9436881d38022403c3ac7123a6ddffcf28d53479', 0, null, null), + ('0xbfbba1515bb284cf4cccd91ea6a4b1c3b35ad821', 0, null, null), + ('0xac8d3891f0f6dbd6ad2de0e8fbd5a638d744ab7c', 0, null, null), + ('0x6ee25671aa43c7e9153d19a1a839ccbbbe65d1ec', 1, null, 'e-mike.eth'), + ('0x5563cd544ff6194825d6d8cee7082cf3b82f6495', 0, null, null), + ('0x857e2b2d430bc14bf6136c1a7e3f28de137df8b9', 0, null, null), + ('0xbd25c9d40bb5c1dd9e9ec05793ef68c1832a5225', 0, null, null), + ('0x211d0db4903a21b2069964f03a764f651881a959', 0, null, null), + ('0x42726b0570174227679521e48cddf454357c8553', 1, 'https://euc.li/respired.eth', 'respired.eth'), + ('0xd1134771cf976d428b0305436147904394adc0f4', 0, null, null), + ('0x51ff8f7307124765872d5cc36a2239610931c3cf', 0, null, null), + ('0x3e604b0c39e38ab3babd79883efe6d0a266d6d85', 1, null, 'jacedodai.eth'), + ('0x375cf2da53f9db8d666b63ab923df1e3509d44ed', 0, null, null), + ('0x30e3784b28332b7cb268988f9e81eeff9e3bf2dc', 1, null, 'parastrate.eth'), + ('0xefb1af6ed095dd9977c1323147e0f1c5b657dec5', 0, null, null), + ('0x8a14d7e2f705ddf57e4fa1e7b5e2e47189eeef51', 0, null, null), + ('0xd79df3a138e33eeffe77ffcc238f7f79dc36e8e9', 0, null, null), + ('0x21cf5754f10ca01edd0f28bcf9d2617ce55ac510', 0, null, null), + ('0x9283fc67acc9684481c5f3fd86a7ed803d278d4d', 0, null, null), + ('0xa0764a9cd88e75a67fd1c02138738ff7282f51ef', 0, null, null), + ('0x182a5f2135bb1a037e806da0afeeb379930a08b7', 0, null, null), + ('0x69abe223244ced141f81019f253cd01c1f81ebcb', 0, null, null), + ('0xf2b4409a1ee503448e52da4d44a448f398145772', 0, null, null), + ('0x1cc501fd638b7adba943177fc156c437d86b00db', 0, null, null), + ('0x1c89d8dcc9730be0ee03badafaa2bc1378a29202', 0, null, null), + ('0x27557565434b453cad7f49bd2ad0af41f0b869d8', 0, null, null), + ('0x07332f210c694c99579cb234389ce370ef5012f1', 0, null, null), + ('0x8b93f0e0a5dfb07d34ae001a01ec18cac92e2d52', 0, null, null), + ('0x7f767233b40bf991fa5317768674ad21bbdca54b', 0, null, null), + ('0x1bfa059bce1e96487d8c7e5f204cc0b34e5dc3da', 0, null, null), + ('0xb972e341a442297f811cdb2dc3537941bc2e7c1d', 0, null, null), + ('0xcf7df61d1cdf6bfbf8dfd3474be362e12e1b1c28', 0, null, null), + ('0x51c1a3476bc4f84d3541587420f68871e4df7057', 0, null, null), + ('0x4bd30da63e580d4d3db9ea327cdc91c071b2dd97', 0, null, null), + ('0xecc4ee1390f083951b486ed1a2d2096d7e65c132', 0, null, null), + ('0xada1fa671651d335998c6a0de336a78f5b49ad3f', 0, null, null), + ('0x85f0af7ed77bd03ad9d4d4ae08fe1a8bae74bf6c', 1, null, 'dimedizzi.eth'), + ('0x7212f026aee238c35dbc203a2cd2d626f7d3ae97', 0, null, null), + ('0x403ba5b29326196e87c3642afafd46c13843e3dc', 0, null, null), + ('0xa630a8368c317f6ab23c225597b4f98748a9f941', 0, null, null), + ('0x77f8d2af77e79cb14a33dc39f340b31d0d6ae5ee', 0, null, null), + ('0x2d7b3f8db2dfad609c27356eeadd07a05928e300', 0, null, null), + ('0x2a68ddd44b1ab6da3d31d4933f69887722ba2d2f', 0, null, null), + ('0x62fa85d7f83bbf69f8aab6eb7b0bfeb03122f399', 0, null, null), + ('0x38430336153468dcf36af5cea7d6bc472425633a', 1, 'ipfs://bafkreifvzf23wrw7q6bzmz7345foiuj3gcr2a2rhonz5qimafwpapikkmu', 'edwardtay.eth'), + ('0x5e7335383b45b19f1f400b2f5b32299006cd2233', 0, null, null), + ('0x78b5ed208352f8f00e875c4f0377cd264c9b07b1', 0, null, null), + ('0x7561eee782743198800907850fe2751e574e7181', 0, null, null), + ('0x36affe344dbe397c0928f4c4dfd0299ffe75067e', 0, null, null), + ('0xfb687ea32cabf54a48235b5724f72532b45fef80', 0, null, null), + ('0x568663cb8669de044cd97f5e169df439b45a8650', 0, null, null), + ('0x6077c70c03b3cfb7d73f3ea5d9c472c01a538291', 0, null, null), + ('0x48b58f0317956e24253d8a4e9121b686ba559eed', 0, null, null), + ('0x080196390194cac5165729a131403ac429b5cf01', 1, null, 'st3v3n.eth'), + ('0xd48bb125982a8af33c78e842c7cb8431904dcbd2', 1, null, 'goblincave.eth'), + ('0xe91122574acc00a07028c29f90e346a9a35416f2', 1, null, 'leetvault.eth'), + ('0x2a85b0798e81ee50349d222e8d4f20cc7d3d108f', 0, null, null), + ('0x1e2aaf59f027c71fd047bbfe7e0ebe06a9b18a04', 0, null, null), + ('0x3a08ca9e3a394c7b947b9ca56493e38176f3428c', 0, null, null), + ('0x3a2bdd605f4b56b4f0ddb8da72434a8cac3ffae6', 0, null, null), + ('0xb578d1eb9998d5c0c186f993bec59342c7376afb', 0, null, null), + ('0xd086045e1219fed9bf4663990963fa51109b38f2', 0, null, null), + ('0x145dcfb47920325d5bb44d239cb9fd0dcada8bc7', 1, null, 'metkayina.eth'), + ('0x6de4832d7e8bcbf5074f52ead3d37c4b9524f926', 0, null, null), + ('0x5b0a02c4d37f8be2d4773250ba5dcb725496714f', 0, null, null), + ('0xe5504e8109e032bc5d961e4f3c358705104e198e', 0, null, null), + ('0x413540c18e8e23bf8aa8a6c8214071a8eafb2c89', 0, null, null), + ('0x8920a5404bc7f13a90e26d1d8ef083e2b8bfa739', 0, null, null), + ('0x3d645730e8b199f59c9a655920def45a042ffe68', 0, null, null), + ('0xa4166ada3884e5d98e83ded352ed0b77ba91de89', 0, null, null), + ('0xb1e7cdda419b03b54b2b769ab710572c9aa839c3', 0, null, null); \ No newline at end of file diff --git a/apps/staking/src/db/migrations/meta/0000_snapshot.json b/apps/staking/src/db/migrations/meta/0000_snapshot.json new file mode 100644 index 00000000..02634b10 --- /dev/null +++ b/apps/staking/src/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,79 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5732bbcb-16cc-4de5-971b-2e6b9d823fcc", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "address_ens": { + "name": "address_ens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_ens": { + "name": "has_ens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(unixepoch())" + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "address_ens_address_unique": { + "name": "address_ens_address_unique", + "columns": [ + "address" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/staking/src/db/migrations/meta/0001_snapshot.json b/apps/staking/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..f55bbab4 --- /dev/null +++ b/apps/staking/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,79 @@ +{ + "id": "6b72abf4-3d0f-4af3-a4dd-81ace2edfa08", + "prevId": "5732bbcb-16cc-4de5-971b-2e6b9d823fcc", + "version": "6", + "dialect": "sqlite", + "tables": { + "address_ens": { + "name": "address_ens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "has_ens": { + "name": "has_ens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(unixepoch())" + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "address_ens_address_unique": { + "name": "address_ens_address_unique", + "columns": [ + "address" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/apps/staking/src/db/migrations/meta/_journal.json b/apps/staking/src/db/migrations/meta/_journal.json new file mode 100644 index 00000000..20244b6b --- /dev/null +++ b/apps/staking/src/db/migrations/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1734554591621, + "tag": "0000_dizzy_mister_fear", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1734554613898, + "tag": "0001_empty_mystique", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/apps/staking/src/db/schemas.ts b/apps/staking/src/db/schemas.ts new file mode 100644 index 00000000..d1ae991d --- /dev/null +++ b/apps/staking/src/db/schemas.ts @@ -0,0 +1,26 @@ +import { sql } from 'drizzle-orm'; +import { integer, sqliteTable as table, text } from 'drizzle-orm/sqlite-core'; + +export const addressEnsTable = table( + 'address_ens', + { + id: integer({ mode: 'number' }).primaryKey({ autoIncrement: true }), + address: text().notNull().unique(), + hasEns: integer({ mode: 'boolean' }).notNull(), + updatedAt: integer({ mode: 'number' }) + .default(sql`(unixepoch())`) + .$onUpdateFn(() => sql`(unixepoch())`), + avatarUrl: text(), + name: text(), + }, + () => [] +); + +export type SelectAddressENS = typeof addressEnsTable.$inferSelect; +export type InsertAddressENS = Pick & + Partial>; +export type UpdateAddressENS = Pick< + SelectAddressENS, + 'id' | 'address' | 'hasEns' +> & + Partial>; From 0ae38e746789ab0b2384d0f1ce2d45223bc5667e Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:26:15 +1300 Subject: [PATCH 04/12] feat: Add backend only services to deal with address-ens information. Tests included. --- .../services/server/AddressENSService.test.ts | 252 ++++++++++++++++++ .../services/server/allowedMethods.test.ts | 37 +++ .../services/server/handleResponse.test.ts | 94 +++++++ .../__tests__/services/server/utils.test.ts | 80 ++++++ .../src/services/server/allowedMethods.ts | 28 ++ .../server/ens/AddressENSRepository.ts | 104 ++++++++ .../services/server/ens/AddressENSService.ts | 125 +++++++++ .../src/services/server/ens/functions.ts | 160 +++++++++++ apps/staking/src/services/server/ens/types.ts | 36 +++ .../src/services/server/handleResponse.ts | 39 +++ apps/staking/src/services/server/turso.ts | 27 ++ apps/staking/src/services/server/types.ts | 26 ++ apps/staking/src/services/server/utils.ts | 48 ++++ 13 files changed, 1056 insertions(+) create mode 100644 apps/staking/__tests__/services/server/AddressENSService.test.ts create mode 100644 apps/staking/__tests__/services/server/allowedMethods.test.ts create mode 100644 apps/staking/__tests__/services/server/handleResponse.test.ts create mode 100644 apps/staking/__tests__/services/server/utils.test.ts create mode 100644 apps/staking/src/services/server/allowedMethods.ts create mode 100644 apps/staking/src/services/server/ens/AddressENSRepository.ts create mode 100644 apps/staking/src/services/server/ens/AddressENSService.ts create mode 100644 apps/staking/src/services/server/ens/functions.ts create mode 100644 apps/staking/src/services/server/ens/types.ts create mode 100644 apps/staking/src/services/server/handleResponse.ts create mode 100644 apps/staking/src/services/server/turso.ts create mode 100644 apps/staking/src/services/server/types.ts create mode 100644 apps/staking/src/services/server/utils.ts diff --git a/apps/staking/__tests__/services/server/AddressENSService.test.ts b/apps/staking/__tests__/services/server/AddressENSService.test.ts new file mode 100644 index 00000000..0feec5f9 --- /dev/null +++ b/apps/staking/__tests__/services/server/AddressENSService.test.ts @@ -0,0 +1,252 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { SelectAddressENS } from '../../../src/db/schemas'; +import { default as Repository } from '../../../src/services/server/ens/AddressENSRepository'; +import AddressENSService from '../../../src/services/server/ens/AddressENSService'; +import { getENSData } from '../../../src/services/server/ens/functions'; +import { isCartesiUser } from '../../../src/services/server/utils'; + +jest.mock('../../../src/services/server/ens/functions', () => { + return { + getENSData: jest.fn(), + }; +}); +jest.mock('../../../src/services/server/utils', () => { + return { + isCartesiUser: jest.fn(), + }; +}); + +jest.mock('../../../src/services/server/ens/AddressENSRepository', () => { + return { + __esModule: true, + default: { + getAll: jest.fn(), + get: jest.fn(), + getAllStaleEntries: jest.fn(), + create: jest.fn(), + update: jest.fn(), + updateBulk: jest.fn(), + }, + }; +}); + +const getENSDataMock = jest.mocked(getENSData); +const isCartesiUserMock = jest.mocked(isCartesiUser); +const repositoryMock = jest.mocked(Repository); +const address = '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dc6'; +const validEntry: SelectAddressENS = { + id: 1, + address, + avatarUrl: 'https://some-host/image.png', + hasEns: true, + name: 'enzo.eth', + updatedAt: 100000, +}; + +describe('Address ENS Service', () => { + const errorLogSpy = jest + .spyOn(console, 'error') + // eslint-disable-next-line @typescript-eslint/no-empty-function + .mockImplementation(() => {}); + + beforeEach(() => { + // defaults to failure + // eslint-disable-next-line @typescript-eslint/no-empty-function + jest.spyOn(console, 'info').mockImplementation(() => {}); + getENSDataMock.mockResolvedValue([]); + isCartesiUserMock.mockResolvedValue(false); + repositoryMock.get.mockResolvedValue(null); + repositoryMock.getAll.mockResolvedValue([]); + repositoryMock.getAllStaleEntries.mockResolvedValue([]); + repositoryMock.create.mockImplementation((obj) => { + return Promise.resolve({ + id: 1, + address: obj.address, + avatarUrl: obj.avatarUrl, + hasEns: obj.hasEns, + name: obj.name, + updatedAt: 10000, + }); + }); + repositoryMock.update.mockImplementation((obj) => { + return Promise.resolve({ + id: obj.id, + name: obj.name, + address: obj.address, + hasEns: obj.hasEns, + avatarUrl: obj.avatarUrl, + updatedAt: 20000, + }); + }); + + repositoryMock.updateBulk.mockImplementation((entries) => { + const updated = entries.map((e) => { + return { ...e, updatedAt: 30000 }; + }); + + return Promise.resolve(updated as SelectAddressENS[]); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getEntry method', () => { + it('should quick return the entry when it is found', async () => { + repositoryMock.get.mockResolvedValue(validEntry); + const result = await AddressENSService.getEntry(address, 1); + + expect(result).toEqual({ + ok: true, + data: { + id: 1, + hasEns: true, + avatarUrl: 'https://some-host/image.png', + name: 'enzo.eth', + address: '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dc6', + }, + }); + }); + + it('should fail the service request when the address is not in the Cartesi Ecosystem', async () => { + const result = await AddressENSService.getEntry(address, 1); + expect(result).toEqual({ ok: false, error: 'not_an_user' }); + }); + + it('should return an new entry when not available in the repository', async () => { + const newAddress = '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dd7'; + isCartesiUserMock.mockResolvedValue(true); + getENSDataMock.mockResolvedValue([ + { address: newAddress, hasEns: false }, + ]); + + const result = await AddressENSService.getEntry(newAddress, 1); + + expect(result).toEqual({ + ok: true, + data: { + address: '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dd7', + avatarUrl: undefined, + hasEns: false, + id: 1, + name: undefined, + }, + }); + }); + + it('should handle runtime errors and return the result to the callee', async () => { + isCartesiUserMock.mockRejectedValue( + new Error('GraphQL out of service') + ); + + const result = await AddressENSService.getEntry(address, 1); + expect(result).toEqual({ ok: false, error: 'unexpected' }); + expect(errorLogSpy).toHaveBeenCalledWith( + new Error('GraphQL out of service') + ); + }); + }); + + describe('listAll method', () => { + it('should return all the available entries including only allowed fields', async () => { + const entries = [ + { ...validEntry }, + { ...validEntry, id: 2 }, + { ...validEntry, id: 3 }, + ]; + + repositoryMock.getAll.mockResolvedValue(entries); + + const result = await AddressENSService.listAll(); + + expect(result).toEqual({ + ok: true, + data: entries.map((e) => ({ ...e, updatedAt: undefined })), + }); + }); + + it('should handle any internal error gracefully', async () => { + repositoryMock.getAll.mockRejectedValue( + new Error('Connection pipe is broken') + ); + + const result = await AddressENSService.listAll(); + + expect(result).toEqual({ ok: false, error: 'unexpected' }); + expect(errorLogSpy).toHaveBeenCalledWith( + new Error('Connection pipe is broken') + ); + }); + }); + + describe('refreshEntries method', () => { + it('should return ok and not call ENS check internals when there are no stale entries', async () => { + const result = await AddressENSService.refreshEntries(); + + expect(result).toEqual({ + ok: true, + data: { success: true, count: 0 }, + }); + expect(getENSDataMock).not.toHaveBeenCalled(); + }); + + it('should check all the stale entries and update with fresh result from sources', async () => { + const staleEntry = { + id: validEntry.id, + address: validEntry.address, + hasEns: validEntry.hasEns, + }; + + const staleEntries = [ + { ...staleEntry }, + { ...staleEntry, id: 2 }, + { ...staleEntry, id: 3 }, + ]; + + const freshEntries = [ + { + ...staleEntry, + name: validEntry.name, + avatarUrl: validEntry.avatarUrl, + }, + { ...staleEntry, id: 2, name: 'dev.enzo.eth' }, + { ...staleEntry, id: 3, name: 'qa.enzo.eth' }, + ]; + + repositoryMock.getAllStaleEntries.mockResolvedValue(staleEntries); + getENSDataMock.mockResolvedValue(freshEntries); + + const result = await AddressENSService.refreshEntries(); + + expect(getENSDataMock).toHaveBeenCalledTimes(1); + expect(repositoryMock.updateBulk).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + ok: true, + data: { success: true, count: 3 }, + }); + }); + + it('should handle any runtime internal errors gracefully', async () => { + repositoryMock.getAllStaleEntries.mockRejectedValue( + new Error('Connection closed') + ); + + const result = await AddressENSService.refreshEntries(); + + expect(result).toEqual({ ok: false, error: 'unexpected' }); + expect(errorLogSpy).toHaveBeenCalledWith( + new Error('Connection closed') + ); + }); + }); +}); diff --git a/apps/staking/__tests__/services/server/allowedMethods.test.ts b/apps/staking/__tests__/services/server/allowedMethods.test.ts new file mode 100644 index 00000000..ee0b0906 --- /dev/null +++ b/apps/staking/__tests__/services/server/allowedMethods.test.ts @@ -0,0 +1,37 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. +import { NextApiRequest } from 'next'; +import { allowedMethodBuilder } from '../../../src/services/server/allowedMethods'; + +const requestMockBuilder = (obj: Partial) => { + const req: jest.Mocked = { + ...obj, + } as unknown as jest.Mocked; + + return req; +}; + +describe('Allowed Method Builder', () => { + it('should return false when a method is not allowed', () => { + const isMethodAllowed = allowedMethodBuilder({ methods: ['GET'] }); + const req = requestMockBuilder({ method: 'POST' }); + expect(isMethodAllowed(req)).toEqual(false); + }); + + it('should return true if the request method is one of the allowed methods listed', () => { + const isMethodAllowed = allowedMethodBuilder({ + methods: ['PUT', 'DELETE'], + }); + + const req = requestMockBuilder({ method: 'DELETE' }); + + expect(isMethodAllowed(req)).toEqual(true); + }); +}); diff --git a/apps/staking/__tests__/services/server/handleResponse.test.ts b/apps/staking/__tests__/services/server/handleResponse.test.ts new file mode 100644 index 00000000..09d3ec21 --- /dev/null +++ b/apps/staking/__tests__/services/server/handleResponse.test.ts @@ -0,0 +1,94 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. +import { NextApiResponse } from 'next'; +import handleResponse from '../../../src/services/server/handleResponse'; + +const responseMockBuilder = (mocks: any) => { + const mock: jest.Mocked = { + status(value: number) { + mocks.status(value); + return this; + }, + send(value: any) { + mocks.send(value); + return this; + }, + json(obj: Object) { + mocks.json(obj); + return this; + }, + } as unknown as jest.Mocked; + return mock; +}; + +describe('Response Handler', () => { + const status = jest.fn(); + const json = jest.fn(); + const send = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return 200 with data body when service result is ok', () => { + const res = responseMockBuilder({ status, json }); + + handleResponse({ ok: true, data: { message: 'hello' } }, res); + + expect(status).toHaveBeenCalledWith(200); + expect(json).toHaveBeenCalledWith({ data: { message: 'hello' } }); + }); + + it('should be able to change the status code for service results that are ok', () => { + const res = responseMockBuilder({ status, json }); + + handleResponse( + { ok: true, data: { message: 'Entry created!' } }, + res, + 201 + ); + + expect(status).toHaveBeenCalledWith(201); + expect(json).toHaveBeenCalledWith({ + data: { message: 'Entry created!' }, + }); + }); + + it('should return 401 when the service result error is related to unauthorized access', () => { + const res = responseMockBuilder({ status, send }); + handleResponse({ ok: false, error: 'unauthorized' }, res); + expect(status).toHaveBeenCalledWith(401); + expect(send).toHaveBeenCalledWith('Unauthorized'); + }); + + it('should return 405 when service result is related to not-allowed method calls', () => { + const res = responseMockBuilder({ status, send }); + handleResponse({ ok: false, error: 'method_not_allowed' }, res); + expect(status).toHaveBeenCalledWith(405); + expect(send).toHaveBeenCalledWith('Method not allowed'); + }); + + it('should return 404 when service result error is related to a non-user', () => { + const res = responseMockBuilder({ status, send }); + handleResponse({ ok: false, error: 'not_an_user' }, res); + expect(status).toHaveBeenCalledWith(404); + expect(send).toHaveBeenCalledWith('Not found'); + }); + + it('should return 500 any other error due to runtime errors', () => { + const res = responseMockBuilder({ status, json }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + handleResponse({ ok: false, error: 'DB-failure' }, res); + + expect(status).toHaveBeenCalledWith(500); + expect(json).toHaveBeenCalledWith({ message: 'Something went wrong.' }); + }); +}); diff --git a/apps/staking/__tests__/services/server/utils.test.ts b/apps/staking/__tests__/services/server/utils.test.ts new file mode 100644 index 00000000..e0b0b14f --- /dev/null +++ b/apps/staking/__tests__/services/server/utils.test.ts @@ -0,0 +1,80 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. +import { ApolloClient, NormalizedCacheObject } from '@apollo/client'; +import { createApollo } from '../../../src/services/apollo'; +import { isCartesiUser } from '../../../src/services/server/utils'; + +jest.mock('../../../src/services/apollo'); + +const createApolloMock = jest.mocked(createApollo, { shallow: true }); + +describe('Server utils', () => { + const queryMock = jest.fn(); + const address = '0x07b41c2b437e69dd1523bf1cff5de63ad9bb3dc6'; + const mainnet = 1; + + beforeEach(() => { + queryMock.mockResolvedValue({ + data: { + user: null, + poolUser: null, + node: null, + }, + }); + + // bare bones mock with only what is required + createApolloMock.mockReturnValue({ + query: queryMock, + } as unknown as ApolloClient); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should return false when an address is not part of Cartesi ecosystem', async () => { + const result = await isCartesiUser(address, mainnet); + expect(result).toEqual(false); + }); + + it('should return true when an address is belongs to a pool-user', async () => { + queryMock.mockResolvedValue({ + data: { + poolUser: { id: address }, + }, + }); + + const result = await isCartesiUser(address, mainnet); + + expect(result).toEqual(true); + }); + + it('should return true when an address is an user', async () => { + queryMock.mockResolvedValue({ + data: { + user: { id: address }, + }, + }); + + const result = await isCartesiUser(address, mainnet); + expect(result).toEqual(true); + }); + + it('should return true when an address is a node', async () => { + queryMock.mockResolvedValue({ + data: { + node: { id: address }, + }, + }); + + const result = await isCartesiUser(address, mainnet); + expect(result).toEqual(true); + }); +}); diff --git a/apps/staking/src/services/server/allowedMethods.ts b/apps/staking/src/services/server/allowedMethods.ts new file mode 100644 index 00000000..ba3b1624 --- /dev/null +++ b/apps/staking/src/services/server/allowedMethods.ts @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { NextApiRequest } from 'next'; + +type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD'; + +type AllowedMethodsParams = { + methods: Methods[]; +}; + +type Builder = ( + param: AllowedMethodsParams +) => (req: NextApiRequest) => boolean; + +export const allowedMethodBuilder: Builder = ({ methods }) => { + return (req: NextApiRequest) => { + return methods.includes(req.method as Methods); + }; +}; diff --git a/apps/staking/src/services/server/ens/AddressENSRepository.ts b/apps/staking/src/services/server/ens/AddressENSRepository.ts new file mode 100644 index 00000000..6c03aa57 --- /dev/null +++ b/apps/staking/src/services/server/ens/AddressENSRepository.ts @@ -0,0 +1,104 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { eq, sql } from 'drizzle-orm'; +import { conflictUpdateSetAllColumns } from '../../../db/helpers/conflictUpdateSetAllColumns'; +import { + addressEnsTable, + InsertAddressENS, + UpdateAddressENS, +} from '../../../db/schemas'; +import dbClient from '../turso'; +import { AddressEns } from './types'; + +class AddressENSRepository { + static async getAll() { + console.time('AddressENSRepository.getAll'); + const list = await dbClient.select().from(addressEnsTable); + console.timeEnd('AddressENSRepository.getAll'); + return list; + } + + static async get(address: string) { + console.time(`AddressENSRepository.get(${address})`); + const [addressEns] = await dbClient + .select() + .from(addressEnsTable) + .where(eq(addressEnsTable.address, address.toLowerCase())); + + console.timeEnd(`AddressENSRepository.get(${address})`); + + return addressEns; + } + + static async getAllStaleEntries(ttlInSeconds: number) { + console.time('AddressENSRepository.getAllStaleEntries'); + const result = await dbClient + .select({ + id: addressEnsTable.id, + address: addressEnsTable.address, + hasEns: addressEnsTable.hasEns, + }) + .from(addressEnsTable) + .where( + sql`${addressEnsTable.updatedAt} < (unixepoch() - ${ttlInSeconds})` + ); + + console.timeEnd('AddressENSRepository.getAllStaleEntries'); + + return result; + } + + static async create(obj: InsertAddressENS) { + console.time(`AddressENSRepository.create(${obj.address})`); + const [created] = await dbClient + .insert(addressEnsTable) + .values(obj) + .returning(); + + console.timeEnd(`AddressENSRepository.create(${obj.address})`); + return created; + } + + static async update(obj: UpdateAddressENS) { + console.time(`AddressENSRepository.update(${obj.address})`); + const [updated] = await dbClient + .insert(addressEnsTable) + .values(obj) + .onConflictDoUpdate({ + target: addressEnsTable.id, + set: conflictUpdateSetAllColumns(addressEnsTable), + }) + .returning(); + + console.timeEnd(`AddressENSRepository.update(${obj.address})`); + + return updated; + } + + static async updateBulk(values: AddressEns[]) { + console.time(`AddressENSRepository.updateBulk(${values.length})`); + const bulkUpdate = await dbClient + .insert(addressEnsTable) + .values(values) + .onConflictDoUpdate({ + target: addressEnsTable.id, + set: conflictUpdateSetAllColumns(addressEnsTable), + }) + .returning(); + + console.timeEnd(`AddressENSRepository.updateBulk(${values.length})`); + + return bulkUpdate; + } +} + +export default AddressENSRepository; diff --git a/apps/staking/src/services/server/ens/AddressENSService.ts b/apps/staking/src/services/server/ens/AddressENSService.ts new file mode 100644 index 00000000..0c0207b3 --- /dev/null +++ b/apps/staking/src/services/server/ens/AddressENSService.ts @@ -0,0 +1,125 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { defaultTo, pick } from 'lodash/fp'; +import { SelectAddressENS } from '../../../db/schemas'; +import { isCartesiUser } from '../utils'; +import { ServiceResult } from './../types'; +import { default as Repository } from './AddressENSRepository'; +import { getENSData } from './functions'; +import { AddressEns } from './types'; + +const ALLOWED_FIELDS = [ + 'id', + 'address', + 'hasEns', + 'avatarUrl', + 'name', +] as const; + +const prepareReturn = (data: SelectAddressENS) => { + return pick(ALLOWED_FIELDS, data) as AddressEns; +}; + +const defaultTTL = 7 * 24 * 60 * 60; +const ENS_ENTRY_TTL = defaultTo( + defaultTTL, + parseInt(process.env.ENS_ENTRY_TTL ?? '') +); + +export default class AddressENSService { + static async getEntry( + address: string, + chainId: number + ): Promise> { + try { + const entry = await Repository.get(address); + + if (entry) return { ok: true, data: prepareReturn(entry) }; + + const isUser = await isCartesiUser(address, chainId); + + if (!isUser) { + return { + ok: false, + error: 'not_an_user', + }; + } + + const [ensData] = await getENSData([{ address }]); + + const createdEntry = await Repository.create({ + ...ensData, + }); + + return { + ok: true, + data: prepareReturn(createdEntry), + }; + } catch (error) { + console.error(error); + return { + ok: false, + error: 'unexpected', + }; + } + } + + static async listAll(): Promise> { + try { + const entries = await Repository.getAll(); + return { + ok: true, + data: entries.map(prepareReturn), + }; + } catch (error) { + console.error(error); + + return { + ok: false, + error: 'unexpected', + }; + } + } + + static async refreshEntries(): Promise< + ServiceResult<{ success: boolean; count: number }> + > { + try { + const staleList = await Repository.getAllStaleEntries( + ENS_ENTRY_TTL + ); + if (staleList.length === 0) + return { ok: true, data: { success: true, count: 0 } }; + console.info(`(Total stale entries): ${staleList.length}`); + const refreshedList = await getENSData(staleList); + + const updatedEntries = await Repository.updateBulk( + refreshedList as AddressEns[] + ); + + return { + ok: true, + data: { + success: true, + count: updatedEntries.length, + }, + }; + } catch (error) { + console.error(error); + + return { + ok: false, + error: 'unexpected', + }; + } + } +} diff --git a/apps/staking/src/services/server/ens/functions.ts b/apps/staking/src/services/server/ens/functions.ts new file mode 100644 index 00000000..fa12bcb3 --- /dev/null +++ b/apps/staking/src/services/server/ens/functions.ts @@ -0,0 +1,160 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { Network } from '@explorer/utils'; +import { ethers } from 'ethers'; +import { + DOMAINS, + GetEnsDomainsQuery, +} from '../../../graphql/queries/ensDomains'; +import ensClient from './../../../services/apolloENSClient'; +import { ENSAddressData, Entry, QueriedDomain } from './types'; + +const httpNodeRpc = + process.env.HTTP_MAINNET_NODE_RPC ?? 'https://cloudflare-eth.com'; + +const buildRpcClient = (nodeRpcEndpoint: string, network: number) => { + const provider = new ethers.providers.JsonRpcProvider( + nodeRpcEndpoint, + network + ); + + return provider; +}; + +const provider = buildRpcClient(httpNodeRpc, Network.MAINNET); + +const ACTION_NAME = { + addAvatarUrl: 'ADD_AVATAR_URL', + getDomains: 'GET_DOMAINS', + addENSName: 'ADD_ENS_NAME', + getAvatarUrl: 'GET_AVATAR_URL', +} as const; + +const getAvatarUrl = (name: string): Promise => { + return provider + .getResolver(name) + .then((resolver) => { + return resolver.getText('avatar'); + }) + .catch((error) => { + console.error( + `${ACTION_NAME.getAvatarUrl}: (${name}) => Fail to get avatar.\nReason: ${error.message}` + ); + return null; + }); +}; + +const addAvatarUrl = async ( + ensAddressDataList: ENSAddressData[] +): Promise => { + const timeLabel = `${ACTION_NAME.addAvatarUrl}(${ensAddressDataList.length})`; + console.time(timeLabel); + const listP = ensAddressDataList.map((ensAddressData) => { + if ( + !ensAddressData.hasEns || + (ensAddressData.hasEns && !ensAddressData.name) + ) { + return Promise.resolve(ensAddressData); + } + + return getAvatarUrl(ensAddressData.name) + .then((ensAvatar) => { + console.info( + `${ACTION_NAME.addAvatarUrl}: (${ensAddressData.name}) => avatar(${ensAvatar})` + ); + ensAddressData.avatarUrl = ensAvatar; + return ensAddressData; + }) + .catch((reason: any) => { + console.error( + `${ACTION_NAME}: (Errored) ${ensAddressData.address} - reason (${reason.message})` + ); + return ensAddressData; + }); + }); + + const result = await Promise.all(listP); + console.timeEnd(timeLabel); + + return result; +}; + +const getDomains = async (addresses: string[]) => { + const timeLabel = `${ACTION_NAME.getDomains}(${addresses.length})`; + console.time(timeLabel); + const result = await ensClient + .query({ + query: DOMAINS, + variables: { + first: addresses.length, + where: { resolvedAddress_in: addresses }, + orderBy: 'createdAt', + orderDirection: 'asc', + }, + }) + .catch((reason: any) => { + console.error(reason); + return { + data: { + domains: [], + }, + }; + }); + + const domains = (result.data.domains ?? []) as QueriedDomain[]; + console.info( + `${ACTION_NAME.getDomains}: (Number of addresses): ${addresses.length}` + ); + console.info( + `${ACTION_NAME.getDomains}: (Domains found): ${domains.length}` + ); + + console.timeEnd(timeLabel); + return domains; +}; + +const getDomainsByAddress = (domains: QueriedDomain[]) => { + return domains.reduce((prev, curr) => { + const address = curr.resolvedAddress?.id ?? ''; + return { + ...prev, + [address]: curr, + }; + }, {} as Record); +}; + +const addENSName = async (entries: Entry[]): Promise => { + const timeLabel = `${ACTION_NAME.addENSName}(${entries.length})`; + console.time(timeLabel); + if (!entries || (entries && entries.length === 0)) return []; + + const addresses = entries.map((e) => e.address); + const domains = await getDomains(addresses); + const domainsByAddress = getDomainsByAddress(domains); + + const result = entries.map((entry) => { + const ensInfo = domainsByAddress[entry.address]; + return { + ...entry, + hasEns: ensInfo !== undefined, + name: ensInfo?.name || ensInfo?.labelName, + }; + }); + + console.timeEnd(timeLabel); + + return result; +}; + +export const getENSData = async (entries: Entry[]) => { + return addENSName(entries).then(addAvatarUrl); +}; diff --git a/apps/staking/src/services/server/ens/types.ts b/apps/staking/src/services/server/ens/types.ts new file mode 100644 index 00000000..46639dee --- /dev/null +++ b/apps/staking/src/services/server/ens/types.ts @@ -0,0 +1,36 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { GetEnsDomainsQuery } from '../../../graphql/queries/ensDomains'; + +export type AddressEns = { + id: number; + address: string; + hasEns: boolean; + name?: string; + avatarUrl?: string; +}; + +export type StaleEntry = Pick; + +export type Entry = { + id?: number; + address: string; +}; + +export type QueriedDomain = GetEnsDomainsQuery['domains'][number]; +export type ENSAddressData = { + id?: number; + hasEns: boolean; + address: string; + name?: string | null; + avatarUrl?: string | null; +}; diff --git a/apps/staking/src/services/server/handleResponse.ts b/apps/staking/src/services/server/handleResponse.ts new file mode 100644 index 00000000..ccca16bb --- /dev/null +++ b/apps/staking/src/services/server/handleResponse.ts @@ -0,0 +1,39 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { NextApiResponse } from 'next'; +import { ServiceError, ServiceResult } from './types'; + +const handleResponse = ( + result: ServiceResult, + res: NextApiResponse, + statusCode = 200 +) => { + if (result.ok === true) + return res.status(statusCode).json({ data: result.data }); + + handleError(result.error, res); +}; + +const handleError = (error: ServiceError, res: NextApiResponse) => { + switch (error) { + case 'unauthorized': + return res.status(401).send('Unauthorized'); + case 'method_not_allowed': + return res.status(405).send('Method not allowed'); + case 'not_an_user': + return res.status(404).send('Not found'); + default: + return res.status(500).json({ message: 'Something went wrong.' }); + } +}; + +export default handleResponse; diff --git a/apps/staking/src/services/server/turso.ts b/apps/staking/src/services/server/turso.ts new file mode 100644 index 00000000..7d2576e9 --- /dev/null +++ b/apps/staking/src/services/server/turso.ts @@ -0,0 +1,27 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { Config, createClient } from '@libsql/client'; +import { drizzle } from 'drizzle-orm/libsql'; + +const url = process.env.TURSO_DATABASE_URL; +const authToken = process.env.TURSO_AUTH_TOKEN; + +const config: Config = { + url, + authToken, +}; + +const turso = createClient(config); + +const dbClient = drizzle(turso, { casing: 'snake_case' }); + +export default dbClient; diff --git a/apps/staking/src/services/server/types.ts b/apps/staking/src/services/server/types.ts new file mode 100644 index 00000000..bd105ece --- /dev/null +++ b/apps/staking/src/services/server/types.ts @@ -0,0 +1,26 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +export type ServiceError = + | 'not_an_user' + | 'method_not_allowed' + | 'unauthorized' + | 'unexpected'; + +export type ServiceResult = + | { + ok: true; + data: TData; + } + | { + ok: false; + error: ServiceError; + }; diff --git a/apps/staking/src/services/server/utils.ts b/apps/staking/src/services/server/utils.ts new file mode 100644 index 00000000..b522b2c2 --- /dev/null +++ b/apps/staking/src/services/server/utils.ts @@ -0,0 +1,48 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { networks } from '@explorer/utils'; +import { isNil } from 'lodash/fp'; +import { + IS_CARTESI_USER_QUERY, + IsCartesiUserQuery, + userQueryVars, +} from '../../graphql/queries'; +import { createApollo } from '../apollo'; + +export const getChainId = (chainName: string) => + parseInt( + Object.keys(networks).find( + (key) => networks[key] == chainName.toLowerCase() + ) + ) || 1; + +export async function isCartesiUser(address: string, chainId: number) { + const client = createApollo(chainId); + const { data } = await client.query< + IsCartesiUserQuery, + typeof userQueryVars + >({ + query: IS_CARTESI_USER_QUERY, + variables: { + id: address, + }, + }); + const isUser = + !isNil(data.user) || !isNil(data.poolUser) || !isNil(data.node); + + if (!isUser) { + console.info( + `IS_CARTESI_USER: ${address} is not an user on ${chainId}` + ); + } + return isUser; +} From 72e10299685d9d2cbd5062896ee2fa5b13af547b Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:26:54 +1300 Subject: [PATCH 05/12] feat: Add and update graphQL queries to support the server services. --- .../staking/src/graphql/queries/ensDomains.ts | 12 ++++++++++ .../src/graphql/queries/stakingPools.ts | 4 ++++ apps/staking/src/graphql/queries/user.ts | 22 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/apps/staking/src/graphql/queries/ensDomains.ts b/apps/staking/src/graphql/queries/ensDomains.ts index 897dc438..2ba93682 100644 --- a/apps/staking/src/graphql/queries/ensDomains.ts +++ b/apps/staking/src/graphql/queries/ensDomains.ts @@ -34,3 +34,15 @@ export const DOMAINS = gql` } } `; + +export type GetEnsDomainsQuery = { + __typename?: 'Query'; + domains: Array<{ + __typename?: 'Domain'; + id: string; + name?: string | null; + labelName?: string | null; + createdAt: any; + resolvedAddress?: { __typename?: 'Account'; id: string } | null; + }>; +}; diff --git a/apps/staking/src/graphql/queries/stakingPools.ts b/apps/staking/src/graphql/queries/stakingPools.ts index fdaeea10..962fb321 100644 --- a/apps/staking/src/graphql/queries/stakingPools.ts +++ b/apps/staking/src/graphql/queries/stakingPools.ts @@ -94,3 +94,7 @@ export const STAKING_POOLS_IDS = gql` } } `; + +export interface StakingPoolsIdsQuery { + stakingPools: { id: string }[]; +} diff --git a/apps/staking/src/graphql/queries/user.ts b/apps/staking/src/graphql/queries/user.ts index 32665b35..45bb68f8 100644 --- a/apps/staking/src/graphql/queries/user.ts +++ b/apps/staking/src/graphql/queries/user.ts @@ -27,6 +27,28 @@ export const USER = gql` } `; +export const IS_CARTESI_USER_QUERY = gql` + query user($id: ID!) { + user(id: $id) { + id + } + + poolUser(id: $id) { + id + } + + node(id: $id) { + id + } + } +`; + +export interface IsCartesiUserQuery { + user: { id: string } | null; + poolUser: { id: string } | null; + node: { id: string } | null; +} + export const userQueryVars = { id: '0', }; From 1c518d4a4a273fd0357f01f718e8e361d0aef212 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:27:38 +1300 Subject: [PATCH 06/12] feat: Add new API endpoints to provide Address ENS information. --- .../src/pages/api/[chain]/ens/[address].ts | 40 +++++++++++++++++++ .../src/pages/api/[chain]/ens/index.ts | 28 +++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 apps/staking/src/pages/api/[chain]/ens/[address].ts create mode 100644 apps/staking/src/pages/api/[chain]/ens/index.ts diff --git a/apps/staking/src/pages/api/[chain]/ens/[address].ts b/apps/staking/src/pages/api/[chain]/ens/[address].ts new file mode 100644 index 00000000..0d292a38 --- /dev/null +++ b/apps/staking/src/pages/api/[chain]/ens/[address].ts @@ -0,0 +1,40 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { ethers } from 'ethers'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { allowedMethodBuilder } from '../../../../services/server/allowedMethods'; +import AddressENSService from '../../../../services/server/ens/AddressENSService'; +import handleResponse from '../../../../services/server/handleResponse'; +import { getChainId } from '../../../../services/server/utils'; + +const isMethodAllowed = allowedMethodBuilder({ methods: ['GET', 'HEAD'] }); + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (isMethodAllowed(req)) { + const chainName = (req.query.chain ?? '') as string; + const address = (req.query?.address ?? '') as string; + + if (!ethers.utils.isAddress(address)) + return res + .status(422) + .json({ message: `${address} is not a valid address format.` }); + + const chainId = getChainId(chainName); + const result = await AddressENSService.getEntry(address, chainId); + + handleResponse(result, res); + } else { + handleResponse({ ok: false, error: 'method_not_allowed' }, res); + } +}; + +export default handler; diff --git a/apps/staking/src/pages/api/[chain]/ens/index.ts b/apps/staking/src/pages/api/[chain]/ens/index.ts new file mode 100644 index 00000000..b87a2db8 --- /dev/null +++ b/apps/staking/src/pages/api/[chain]/ens/index.ts @@ -0,0 +1,28 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { NextApiRequest, NextApiResponse } from 'next'; +import { allowedMethodBuilder } from '../../../../services/server/allowedMethods'; +import AddressENSService from '../../../../services/server/ens/AddressENSService'; +import handleResponse from '../../../../services/server/handleResponse'; + +const isMethodAllowed = allowedMethodBuilder({ methods: ['GET', 'HEAD'] }); + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (isMethodAllowed(req)) { + const result = await AddressENSService.listAll(); + handleResponse(result, res); + } else { + handleResponse({ ok: false, error: 'method_not_allowed' }, res); + } +}; + +export default handler; From 7bdf3de6699caf6cdcbf773f4f57b9f25e5230f4 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:29:38 +1300 Subject: [PATCH 07/12] feat: Add a new protected endpoint to refresh stale ENS data. Include Vercel Cron service configuration. --- .../src/pages/api/cron/ens-update-stale.ts | 37 +++++++++++++++++++ apps/staking/vercel.json | 9 +++++ 2 files changed, 46 insertions(+) create mode 100644 apps/staking/src/pages/api/cron/ens-update-stale.ts create mode 100644 apps/staking/vercel.json diff --git a/apps/staking/src/pages/api/cron/ens-update-stale.ts b/apps/staking/src/pages/api/cron/ens-update-stale.ts new file mode 100644 index 00000000..48f2fd69 --- /dev/null +++ b/apps/staking/src/pages/api/cron/ens-update-stale.ts @@ -0,0 +1,37 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { NextApiRequest, NextApiResponse } from 'next'; +import { allowedMethodBuilder } from '../../../services/server/allowedMethods'; +import AddressENSService from '../../../services/server/ens/AddressENSService'; +import handleResponse from '../../../services/server/handleResponse'; + +const isMethodAllowed = allowedMethodBuilder({ methods: ['GET'] }); +const cronSecret = process.env.CRON_SECRET; + +const isAuthorized = (req: NextApiRequest) => + req.headers.authorization === `Bearer ${cronSecret}`; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + if (!isAuthorized(req)) + return handleResponse({ ok: false, error: 'unauthorized' }, res); + + if (isMethodAllowed(req)) { + console.time('ens-update-stale'); + const result = await AddressENSService.refreshEntries(); + console.timeEnd('ens-update-stale'); + handleResponse(result, res); + } else { + handleResponse({ ok: false, error: 'method_not_allowed' }, res); + } +}; + +export default handler; diff --git a/apps/staking/vercel.json b/apps/staking/vercel.json new file mode 100644 index 00000000..85dff63a --- /dev/null +++ b/apps/staking/vercel.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "crons": [ + { + "path": "/api/cron/ens-update-stale", + "schedule": "0 0 * * *" + } + ] +} From 36849a0b277e08748c4280f1c67d1b1072c1e2a5 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:42:12 +1300 Subject: [PATCH 08/12] refactor: Replace the layer one fetch of ENS data with our new cached ENS service, including logic to request data on missing addresses. --- packages/services/src/ens.ts | 81 ---------------- packages/services/src/ens.tsx | 168 ++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 81 deletions(-) delete mode 100644 packages/services/src/ens.ts create mode 100644 packages/services/src/ens.tsx diff --git a/packages/services/src/ens.ts b/packages/services/src/ens.ts deleted file mode 100644 index d39c8761..00000000 --- a/packages/services/src/ens.ts +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (C) 2021 Cartesi Pte. Ltd. - -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. - -// This program is distributed in the hope that it will be useful, but WITHOUT ANY -// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -// PARTICULAR PURPOSE. See the GNU General Public License for more details. - -import { useEffect, useState } from 'react'; -import { ethers } from 'ethers'; -import { useWallet } from '@explorer/wallet'; - -export interface ENSEntry { - address: string; - name?: string; - avatar?: string; - url?: string; - resolving: boolean; -} - -/** - * This hook provides a easy way to display ENS information about a ETH address. - * @param address ETH address to be resolved - * @returns ENSEntry with the address, and name if address can be resolved to a name - */ -export const useENS = (address: string): ENSEntry => { - const { library } = useWallet(); - const [entry, setEntry] = useState({ address, resolving: true }); - useEffect(() => { - if (library) { - const resolve = async (address: string): Promise => { - // convert address to checksum address - try { - address = ethers.utils.getAddress(address); - } catch (e) { - console.warn(`error resolving address ${address}`); - return { address, resolving: false }; - } - - if (!library.network?.ensAddress) { - // network does not support ENS - return { address, resolving: false }; - } - - // do a reverse lookup - const name = await library.lookupAddress(address); - - console.log(`reverse lookup of ${address} resolved to ${name}`); - if (name) { - // name found, now do a forward lookup - const resolver = await library.getResolver(name); - const ethAddress = await resolver?.getAddress(); - console.log( - `forward lookup of ${name} resolved to ${ethAddress}` - ); - - // we need to check if the forward resolution matches the reverse - if (ethAddress === address) { - const avatar = await resolver?.getText('avatar'); - const url = await resolver?.getText('url'); - console.log(`${name}: avatar(${avatar}) url(${url})`); - return { - address, - name, - avatar, - url, - resolving: false, - }; - } - } - return { address, resolving: false }; - }; - - resolve(address).then(setEntry); - } - }, [address, library, library?.network]); - return entry; -}; diff --git a/packages/services/src/ens.tsx b/packages/services/src/ens.tsx new file mode 100644 index 00000000..2d7e9df2 --- /dev/null +++ b/packages/services/src/ens.tsx @@ -0,0 +1,168 @@ +// Copyright (C) 2024 Cartesi Pte. Ltd. + +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. + +// This program is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +// PARTICULAR PURPOSE. See the GNU General Public License for more details. + +import { networks } from '@explorer/utils'; +import { useWallet } from '@explorer/wallet'; +import { ethers } from 'ethers'; +import { + createContext, + FC, + ReactNode, + useContext, + useEffect, + useState, +} from 'react'; + +interface AddressEns { + id: number; + address: string; + hasEns: boolean; + name?: string; + avatarUrl?: string; +} + +interface ENSData extends AddressEns { + resolving: boolean; +} + +type ENSCacheData = Map; +const ENSDataContext = createContext(new Map()); + +type GetENSInfoResult = Promise; +type GetENSInfoSuccessResponse = { data: AddressEns }; +type GetENSInfoError = { + status: number; + errorMessage: string; +}; + +type NetworkId = keyof typeof networks; + +const getENSInfo = (address: string, chainId: number): GetENSInfoResult => { + const chainName = networks[chainId as NetworkId] ?? 'mainnet'; + const host = location.origin; + const endpoint = `${host}/api/${chainName}/ens/${address}`; + + return fetch(endpoint).then(async (response) => { + if (response.status === 200) { + const { data } = + (await response.json()) as GetENSInfoSuccessResponse; + return data; + } + + const text = await response.text(); + const status = response.status; + const errorMessage = `${status}: ${text ?? response.statusText}`; + + return Promise.reject({ status, errorMessage }); + }); +}; + +const useCachedEntry = (address: string) => { + const { chainId } = useWallet(); + const cachedData = useContext(ENSDataContext); + const [entry, setEntry] = useState>({ + resolving: true, + address, + }); + + useEffect(() => { + const isValidAddress = ethers.utils.isAddress(address ?? ''); + + if (!isValidAddress) { + const entry = { address, hasEns: false, resolving: false }; + setEntry(entry); + return; + } + + const entry = cachedData.get(address); + + if (entry) { + setEntry(entry); + } else { + const tempEntry = { + address, + resolving: true, + hasEns: false, + id: 0, + }; + + cachedData.set(address, tempEntry); + + getENSInfo(address, chainId ?? 1) + .then((ensData) => { + const newEntry = { ...ensData, resolving: false }; + cachedData.set(ensData.address, newEntry); + setEntry(newEntry); + }) + .catch((error: GetENSInfoError) => { + console.error(error.errorMessage); + const newEntry = { + id: -1, + address, + hasEns: false, + resolving: false, + }; + cachedData.set(address, newEntry); + setEntry(newEntry); + }); + } + }, [cachedData, address, chainId]); + + return entry; +}; + +export const ENSDataProvider: FC<{ + children: ReactNode; + value: AddressEns[]; +}> = ({ children, value }) => { + const cachedData = useContext(ENSDataContext); + + useEffect(() => { + if (value) { + console.count('ens-cache-provider-rolling'); + value.forEach((ensDataEntry) => { + cachedData.set(ensDataEntry.address, { + ...ensDataEntry, + resolving: false, + }); + }); + } + }, [cachedData, value]); + + return ( + + {children} + + ); +}; + +export interface ENSEntry { + address: string; + name?: string; + avatar?: string; + url?: string; + resolving: boolean; +} + +/** + * This hook provides a easy way to display ENS information about a ETH address. + * @param address ETH address to be resolved + * @returns ENSEntry with the address, and name if address can be resolved to a name + */ +export const useENS = (address: string): ENSEntry => { + const cachedEnsEntry = useCachedEntry(address); + return { + address: cachedEnsEntry?.address ?? address, + resolving: cachedEnsEntry?.resolving ?? false, + avatar: cachedEnsEntry?.avatarUrl, + name: cachedEnsEntry?.name, + }; +}; From 697776591cb67f02e3057b20fc402f291a3f19e0 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:50:00 +1300 Subject: [PATCH 09/12] test: Add dotenv to run tests locally without problem. --- packages/ui/.env | 1 + packages/ui/jest.setup.js | 11 +++++++++++ packages/ui/package.json | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/ui/.env diff --git a/packages/ui/.env b/packages/ui/.env new file mode 100644 index 00000000..442b2900 --- /dev/null +++ b/packages/ui/.env @@ -0,0 +1 @@ +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=WC_PROJECT_ID \ No newline at end of file diff --git a/packages/ui/jest.setup.js b/packages/ui/jest.setup.js index 13703410..3915818c 100644 --- a/packages/ui/jest.setup.js +++ b/packages/ui/jest.setup.js @@ -3,6 +3,17 @@ import '@testing-library/jest-dom/extend-expect'; import 'jest-canvas-mock'; +jest.mock('@explorer/services', () => { + const original = jest.requireActual('@explorer/services'); + return { + __esModule: true, + ...original, + useENS: jest.fn().mockImplementation((address) => ({ + address, + resolving: false, + })), + }; +}); Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation((query) => ({ diff --git a/packages/ui/package.json b/packages/ui/package.json index c91bedf8..d1fc2580 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -6,7 +6,7 @@ "license": "MIT", "scripts": { "storybook": "start-storybook -p 6007", - "test": "jest", + "test": "dotenv -- jest", "test:ci": "jest --runInBand --coverage", "lint": "TIMING=1 eslint --ext .tsx,.ts src/" }, From 483258d612ca4200149747c5c1c412d25ff6ae5a Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 17:53:10 +1300 Subject: [PATCH 10/12] feat: Changes in the frontend to load the ENS data only on first app load. --- apps/staking/src/pages/_app.tsx | 42 ++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/staking/src/pages/_app.tsx b/apps/staking/src/pages/_app.tsx index 8db87052..7edd74e0 100644 --- a/apps/staking/src/pages/_app.tsx +++ b/apps/staking/src/pages/_app.tsx @@ -13,13 +13,15 @@ import { ChakraProvider } from '@chakra-ui/react'; import { theme } from '@explorer/ui'; import { AppProps } from 'next/app'; import dynamic from 'next/dynamic'; -import { FC, useEffect, ReactNode } from 'react'; +import { FC, ReactNode, useEffect, useState } from 'react'; import TagManager from 'react-gtm-module'; import ApolloContainer from '../components/ApolloContainer'; +import { ENSDataProvider } from '@explorer/services'; import { Fonts } from '@explorer/ui'; -import { GA4TrackerProvider } from '../contexts/ga4Tracker'; import PageHead from '../components/PageHead'; +import { GA4TrackerProvider } from '../contexts/ga4Tracker'; +import { AddressEns } from '../services/server/ens/types'; type ComponentType = FC<{ children: ReactNode }>; @@ -31,16 +33,36 @@ const Web3Container = dynamic(() => import('../components/Web3Container'), { ssr: false, }) as ComponentType; +const getENSCachedData = () => { + const host = location.origin; + const endpoint = `${host}/api/mainnet/ens`; + return fetch(endpoint) + .then((response) => response.json()) + .catch((reason) => { + console.error( + `Fetching ENS cached data failed.\nReason: ${reason.message}` + ); + return { data: [] }; + }); +}; + const App = ({ Component, pageProps, }: AppProps & { Component: ComponentType }) => { + const [ensData, setEnsData] = useState([]); + useEffect(() => { if (process.env.NODE_ENV === 'production') { TagManager.initialize({ gtmId: process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID, }); } + + (async () => { + const { data }: { data: AddressEns[] } = await getENSCachedData(); + setEnsData(data); + })(); }, []); return ( @@ -51,13 +73,15 @@ const App = ({ /> - - - - - - - + + + + + + + + + ); From 127463434012d81f26316a15bd5cba6657c31315 Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 18:20:17 +1300 Subject: [PATCH 11/12] chore: Update Environment files with new properties including comments. --- apps/staking/.env.development | 11 ++++++++++- apps/staking/.env.production | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/staking/.env.development b/apps/staking/.env.development index a8bc36dd..39af03c9 100644 --- a/apps/staking/.env.development +++ b/apps/staking/.env.development @@ -6,4 +6,13 @@ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=YOUR_WALLETCONNECT_PROJECT_ID NEXT_PUBLIC_MAINNET_GRAPHQL_URL=YOUR_MAINNET_SUBGRAPH_GRAPHQL_ENDPOINT NEXT_PUBLIC_SEPOLIA_GRAPHQL_URL=YOUR_SEPOLIA_SUBGRAPH_GRAPHQL_ENDPOINT # SERVER SIDE -ENS_GRAPHQL_URL=YOUR_SUBGRAPH_ENS_GRAPHQL_ENDPOINT \ No newline at end of file +ENS_GRAPHQL_URL=YOUR_SUBGRAPH_ENS_GRAPHQL_ENDPOINT +# A ethereum mainnet RPC-NODE. Below is a free endpoint (rate-limiting may cause troubles) +HTTP_MAINNET_NODE_RPC=https://cloudflare-eth.com +# Time in seconds to define when a ENS entry is stale. +ENS_ENTRY_TTL=86400 +# The Auth token is only necessary in case you're using a paid service e.g Turso +# TURSO_AUTH_TOKEN=YOUR_TOKEN_HERE_IF_NECESSARY +TURSO_DATABASE_URL="file:local.db" +# For calling the /api/cron/ens-update-ens endpoint +CRON_SECRET="dummy-secret" \ No newline at end of file diff --git a/apps/staking/.env.production b/apps/staking/.env.production index 5a6875e4..4e782364 100644 --- a/apps/staking/.env.production +++ b/apps/staking/.env.production @@ -8,3 +8,8 @@ NEXT_PUBLIC_MAINNET_GRAPHQL_URL=YOUR_MAINNET_SUBGRAPH_GRAPHQL_ENDPOINT NEXT_PUBLIC_SEPOLIA_GRAPHQL_URL=YOUR_SEPOLIA_SUBGRAPH_GRAPHQL_ENDPOINT # SERVER SIDE ENS_GRAPHQL_URL=YOUR_SUBGRAPH_ENS_GRAPHQL_ENDPOINT +HTTP_MAINNET_NODE_RPC=YOUR_PAID_MAINNET_RPC_NODE +ENS_ENTRY_TTL=DEFINE_TIME_IN_SECONDS +TURSO_DATABASE_URL=YOUR_TURSO_OR_OTHER_LIBSQL_ENDPOINT_SERVICE +TURSO_AUTH_TOKEN=YOUR_LIBSQL_SERVICE_AUTH_TOKEN +CRON_SECRET=A_STRONG_SECRET_FOR_CALLING_PROTECTED_CRON_APIS \ No newline at end of file From f8d2fa36efd7d1ab68a56287c6edb177e95aa2ae Mon Sep 17 00:00:00 2001 From: Bruno Menezes Date: Thu, 19 Dec 2024 18:30:21 +1300 Subject: [PATCH 12/12] doc(explorer): Add changelog entry. Add information for next explorer release v3.9.0 --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab2cdfe..a477c016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.9.0] - 2024-12-20 + +- Improve how the ENS address information is provided if available through the UI. The new approach considerably improves efficiency in terms of on-chain reads. + ## [3.8.6] - 2024-12-05 - Fix a UI bug in the block's page search box. The text is now more visible when using light mode. @@ -392,7 +396,8 @@ Staking Pools - First release -[unreleased]: https://github.com/cartesi/explorer/compare/v3.8.6...HEAD +[unreleased]: https://github.com/cartesi/explorer/compare/v3.9.0...HEAD +[3.9.0]: https://github.com/cartesi/explorer/compare/v3.9.0...v3.8.6 [3.8.6]: https://github.com/cartesi/explorer/compare/v3.8.6...v3.8.5 [3.8.5]: https://github.com/cartesi/explorer/compare/v3.8.5...v3.8.4 [3.8.4]: https://github.com/cartesi/explorer/compare/v3.8.4...v3.8.3