diff --git a/.env.example b/.env.example index a53d0ec34..3096f9a36 100644 --- a/.env.example +++ b/.env.example @@ -9,4 +9,8 @@ PINATA_SUBMARINE_KEY=UNKNOWN LOOPRING_API_KEY=UNKNOWN # Other environment variables -SESSION_SECRET=complex_password_at_least_32_characters_long \ No newline at end of file +SESSION_SECRET=complex_password_at_least_32_characters_long + +# Supabase +NEXT_PUBLIC_SUPABASE_URL=https://YOUR_DB.supabase.co +NEXT_PUBLIC_SUPABASE_ANON=👁️👄👁️ \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9beea7730..0a621c03f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,6 +7,9 @@ on: jobs: main: runs-on: ubuntu-latest + env: + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON }} steps: - name: 🏗 Setup repo uses: actions/checkout@v3 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b5fa1601d..48c01776c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,6 +5,9 @@ on: jobs: main: runs-on: ubuntu-latest + env: + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON }} steps: - name: 🏗 Setup repo uses: actions/checkout@v3 diff --git a/docs/setup/4-RUNNING.md b/docs/setup/4-RUNNING.md index 4664a9be1..1842daa3f 100644 --- a/docs/setup/4-RUNNING.md +++ b/docs/setup/4-RUNNING.md @@ -21,7 +21,7 @@ You can access it by opening a browser and going to the following URL: \ Now your LoopGate is running on your local machine, it's time to check the `.env` file. -- Go to the following url: [http://localhost:3000/api/env-status](http://localhost:3000/api/env-status). +- Go to the following url: [http://localhost:3000/api/helpers/checkEnvStatus](http://localhost:3000/api/helpers/checkEnvStatus). This checks the secrets in your `.env` file: if these are misconfigured, LoopGate will not work. diff --git a/docs/setup/5-NETLIFY.md b/docs/setup/5-NETLIFY.md index cd0b5609b..6a204768e 100644 --- a/docs/setup/5-NETLIFY.md +++ b/docs/setup/5-NETLIFY.md @@ -30,7 +30,7 @@ However, there is still one important step: adding the environment secrets to Ne 1. Go to your project's the 'env' settings of your project's Netlify page: [https://app.netlify.com/sites/YOUR_PROJECT_NAME/settings/env](https://app.netlify.com/sites/YOUR_PROJECT_NAME/settings/env) 2. Click on 'Add a variable', then 'import from a .env file'. 3. Copy the contents of your `.env` file, and paste them in the input field. Click on 'Import variables'. -4. Once more, check the `/api/env-status` endpoint to see if all secrets are defined. Your site should be live soon at [https://YOUR_PROJECT_NAME.netlify.app/api/env-status](https://YOUR_PROJECT_NAME.netlify.app/api/env-status) +4. Once more, check the `/api/helpers/checkEnvStatus` endpoint to see if all secrets are defined. Your site should be live soon at [https://YOUR_PROJECT_NAME.netlify.app/api/helpers/checkEnvStatus](https://YOUR_PROJECT_NAME.netlify.app/api/helpers/checkEnvStatus) {% hint style="info" %} Your Netlify website will most likely have an auto-generated name like '[https://adjective-noun-12345.netlify.app](https://adjective-noun-12345.netlify.app)'. You can easily change the domain name in Netlify! diff --git a/env.d.ts b/env.d.ts index 5ec083175..98b75d54d 100644 --- a/env.d.ts +++ b/env.d.ts @@ -4,5 +4,9 @@ namespace NodeJS { PINATA_SUBMARINE_KEY: string; LOOPRING_API_KEY: string; SESSION_SECRET: string; + NEXT_PUBLIC_SUPABASE_ANON: string; + NEXT_PUBLIC_SUPABASE_URL: string; + NEXT_PUBLIC_LOGFLARE_API_KEY: string; + NEXT_PUBLIC_LOGFLARE_SOURCE_TOKEN: string; } } diff --git a/jest.config.js b/jest.config.js index baff22c45..c73d4f66a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,7 +19,7 @@ const customJestConfig = { // For example: moduleNameMapper: { - "@/(.*)$": "/src/$1", + "@/(.*)$": "/$1", }, testEnvironment: "jest-environment-jsdom", }; diff --git a/package-lock.json b/package-lock.json index b98455f27..e87219332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "loopgate", - "version": "0.2.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "loopgate", - "version": "0.2.1", + "version": "1.0.0", "license": "BSD-2-Clause", "dependencies": { "@heroicons/react": "^2.0.13", @@ -22,21 +22,30 @@ "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-tooltip": "^1.0.3", + "@supabase/supabase-js": "^2.11.0", "axios": "^1.2.3", "axios-rate-limit": "^1.3.0", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", "connectkit": "^1.1.1", + "crypto": "^1.0.1", + "date-fns": "^2.29.3", "ethers": "^5.7.2", "iron-session": "^6.3.1", "next": "^13.0.0", "pinata-submarine": "^0.1.6", + "pino": "^8.11.0", + "pino-logflare": "^0.3.12", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-fast-marquee": "^1.3.5", "react-hot-toast": "^2.4.0", + "react-jazzicon": "^1.0.4", + "react-spring": "^9.7.1", "siwe": "^2.1.3", "tailwind-merge": "^1.9.0", "tailwindcss-animate": "^1.0.5", + "uuid": "^9.0.0", "wagmi": "~0.10.0" }, "devDependencies": { @@ -46,12 +55,14 @@ "@types/node": "^17.0.31", "@types/react": "^18.0.9", "@types/react-dom": "^18.0.3", + "@types/uuid": "^9.0.1", "autoprefixer": "^10.4.13", "eslint": "^8.15.0", "eslint-config-next": "^12.1.6", "jest": "^29.4.2", "jest-environment-jsdom": "^29.4.2", "postcss": "^8.4.21", + "supabase": "^1.45.2", "tailwindcss": "^3.2.4", "typescript": "^4.6.4" } @@ -3100,6 +3111,137 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.1.tgz", + "integrity": "sha512-EX5KAD9y7sD43TnLeTNG1MgUVpuRO1YaSJRPawHNRgUWYfILge3s85anny4S4eTJGpdp5OoFV2kx9fsfeo0qsw==", + "dependencies": { + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.1.tgz", + "integrity": "sha512-8K9/FaRn5VvMa24mbwYxwkALnAAyMRdmQXrARZLcBW2vxLJ6uw9Cy3d06Z8M12kEqF2bDlccaCSDsn2bSz+Q4A==", + "dependencies": { + "@react-spring/animated": "~9.7.1", + "@react-spring/rafz": "~9.7.1", + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/konva": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-9.7.1.tgz", + "integrity": "sha512-74svXHtUJi6Tvk9mNLUV1/1WfU8MdWsTK6JUpvmJr/rUr8r3FdOokk22icbgEg6AjxCkIf5e2WFovCCHUSyS0w==", + "dependencies": { + "@react-spring/animated": "~9.7.1", + "@react-spring/core": "~9.7.1", + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "konva": ">=2.6", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-konva": "^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0" + } + }, + "node_modules/@react-spring/native": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/native/-/native-9.7.1.tgz", + "integrity": "sha512-dHWeH0UuE+Rxc3YZFLp8Aq0RBP07sdOgI7pLVG46OzkMRs2RtJeWJxB6UXIWAgcYDqWDk2REAPhLD3ItDl0tDQ==", + "dependencies": { + "@react-spring/animated": "~9.7.1", + "@react-spring/core": "~9.7.1", + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || >=17.0.0 || >=18.0.0", + "react-native": ">=0.58" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.1.tgz", + "integrity": "sha512-JSsrRfbEJvuE3w/uvU3mCTuWwpQcBXkwoW14lBgzK9XJhuxmscGo59AgJUpFkGOiGAVXFBGB+nEXtSinFsopgw==" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.1.tgz", + "integrity": "sha512-R2kZ+VOO6IBeIAYTIA3C1XZ0ZVg/dDP5FKtWaY8k5akMer9iqf5H9BU0jyt3Qtxn0qQY7whQdf6MTcWtKeaawg==", + "dependencies": { + "@react-spring/rafz": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/three": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.1.tgz", + "integrity": "sha512-5leUe0PDwIIw1M3GN3788zwTY4Ykyy+kNvQmg9+Hqs1DN3T8J1ovRTGwqWfGAu4ApTta9p5BH7SWNxxt3NO59Q==", + "dependencies": { + "@react-spring/animated": "~9.7.1", + "@react-spring/core": "~9.7.1", + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.1.tgz", + "integrity": "sha512-yBcyfKUeZv9wf/ZFrQszvhSPuDx6Py6yMJzpMnS+zxcZmhXPeOCKZSHwqrUz1WxvuRckUhlgb7eNI/x5e1e8CA==" + }, + "node_modules/@react-spring/web": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.1.tgz", + "integrity": "sha512-6uUE5MyKqdrJnIJqlDN/AXf3i8PjOQzUuT26nkpsYxUGOk7c+vZVPcfrExLSoKzTb9kF0i66DcqzO5fXz/Z1AA==", + "dependencies": { + "@react-spring/animated": "~9.7.1", + "@react-spring/core": "~9.7.1", + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/zdog": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.7.1.tgz", + "integrity": "sha512-FeDws+7ZSoi91TUjxKnq3xmdOW6fthmqky6zSPIZq1NomeyO7+xwbxjtu15IqoWG4DJ9pouVZDijvBQXUNl0Mw==", + "dependencies": { + "@react-spring/animated": "~9.7.1", + "@react-spring/core": "~9.7.1", + "@react-spring/shared": "~9.7.1", + "@react-spring/types": "~9.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-zdog": ">=1.0", + "zdog": ">=1.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz", @@ -3358,6 +3500,60 @@ "@stablelib/wipe": "^1.0.1" } }, + "node_modules/@supabase/functions-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.1.0.tgz", + "integrity": "sha512-vRziB+AqRXRaGHjEFHwBo0kuNDTuAxI7VUeqU24Fe86ISoD8YEQm0dGdpleJEcqgDGWaO6pxT1tfj1BRY5PwMg==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/gotrue-js": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.14.0.tgz", + "integrity": "sha512-FI6q4n4iZ2zrEt1BnBYYe8HQ1k9t5CpBcDQxVXa8PeMwygXpzR0AcdfAsZ5Yba42C8YsBA132ti01f+RINS3UQ==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.4.1.tgz", + "integrity": "sha512-aruqwV/aTggkM7OVv2JinCeXmRMKHJCZpkuS1nuoa0NgLw7g3NyILSyWOKYTBJ/PxE/zXtWsBhdxFzaaNz5uxg==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.7.0.tgz", + "integrity": "sha512-wg35ofiCpIemycmPZvvZk3jM9c9z8VvnPUBbSP9ZZN2vSOEJ9C7DZuLiiZMXsyNUzjVgIn62A1tN99T5+9O8Aw==", + "dependencies": { + "@types/phoenix": "^1.5.4", + "websocket": "^1.0.34" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.3.1.tgz", + "integrity": "sha512-BaPIvyvjuZW1V0CnfGKUZyzpBUXnsh0XD8eqTOYd+MdiGPmIPI0vtwnT4fAoK8mipp1vpcN62EVQaqeUnWXPtQ==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.11.0.tgz", + "integrity": "sha512-FkaPZjVx1oY4boS02kAoNhmdyy5hrezxGlY8lZdQwKvk7ee5NPiXzjjsGruc6JOS1QNeuWIUAw1L7uKVYI30dA==", + "dependencies": { + "@supabase/functions-js": "^2.1.0", + "@supabase/gotrue-js": "^2.12.0", + "@supabase/postgrest-js": "^1.1.1", + "@supabase/realtime-js": "^2.7.0", + "@supabase/storage-js": "^2.3.1", + "cross-fetch": "^3.1.5" + } + }, "node_modules/@swc/helpers": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz", @@ -3809,6 +4005,11 @@ "@types/koa": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.194", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", + "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -3827,6 +4028,11 @@ "@types/node": "*" } }, + "node_modules/@types/phoenix": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.5.tgz", + "integrity": "sha512-1eWWT19k0L4ZiTvdXjAvJ9KvW0B8SdiVftQmFPJGTEx78Q4PCSIQDpz+EfkFVR1N4U9gREjlW4JXL8YCIlY0bw==" + }, "node_modules/@types/prettier": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", @@ -3918,6 +4124,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz", "integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==" }, + "node_modules/@types/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", + "dev": true + }, "node_modules/@types/ws": { "version": "7.4.7", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", @@ -4425,6 +4637,75 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/logger/node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" + }, + "node_modules/@walletconnect/logger/node_modules/pino": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@walletconnect/logger/node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/@walletconnect/logger/node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + }, + "node_modules/@walletconnect/logger/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, + "node_modules/@walletconnect/logger/node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@walletconnect/logger/node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/@walletconnect/logger/node_modules/thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "dependencies": { + "real-require": "^0.1.0" + } + }, "node_modules/@walletconnect/logger/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -4845,6 +5126,51 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/sign-client/node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" + }, + "node_modules/@walletconnect/sign-client/node_modules/pino": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@walletconnect/sign-client/node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/@walletconnect/sign-client/node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + }, + "node_modules/@walletconnect/sign-client/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, "node_modules/@walletconnect/sign-client/node_modules/query-string": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", @@ -4862,6 +5188,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@walletconnect/sign-client/node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@walletconnect/sign-client/node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/@walletconnect/sign-client/node_modules/thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "dependencies": { + "real-require": "^0.1.0" + } + }, "node_modules/@walletconnect/sign-client/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -4998,6 +5348,51 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/universal-provider/node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" + }, + "node_modules/@walletconnect/universal-provider/node_modules/pino": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + }, + "node_modules/@walletconnect/universal-provider/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, "node_modules/@walletconnect/universal-provider/node_modules/query-string": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz", @@ -5015,13 +5410,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@walletconnect/universal-provider/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "node_modules/@walletconnect/universal-provider/node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "engines": { + "node": ">= 12.13.0" + } }, - "node_modules/@walletconnect/utils": { - "version": "1.8.0", + "node_modules/@walletconnect/universal-provider/node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "dependencies": { + "real-require": "^0.1.0" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@walletconnect/utils": { + "version": "1.8.0", "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-1.8.0.tgz", "integrity": "sha512-zExzp8Mj1YiAIBfKNm5u622oNw44WOESzo6hj+Q3apSMIb0Jph9X3GDIdbZmvVZsNPxWDL7uodKgZcCInZv2vA==", "dependencies": { @@ -5110,6 +5529,17 @@ } } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", @@ -5731,11 +6161,30 @@ } ] }, + "node_modules/batch2": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/batch2/-/batch2-1.0.6.tgz", + "integrity": "sha512-xZsZx73HfBcoUMITZwqRF+gO5RGx5Sf+hZjmxoRuu8xpYM003aSDM7uwKKbmATJG1Kuc5Rs0kck/0ALpOnue0w==", + "dependencies": { + "through2": "^3.0.1" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bigint-buffer": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", @@ -5748,6 +6197,42 @@ "node": ">= 10.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, + "node_modules/bin-links": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.1.tgz", + "integrity": "sha512-bmFEM39CyX336ZGGRsGPlc6jZHriIoHacOQcTt72MktIjpPhZoP4te2jOyUXF3BLILmJ8aNLncoPVeIIFlrDeA==", + "dev": true, + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/bin-links/node_modules/write-file-atomic": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.0.tgz", + "integrity": "sha512-R7NYMnHSlV42K54lwY9lvW6MnSm1HSJqZL3xiSgi9E7//FYaI74r2G0rd+/X6VAMkHEdzxQaU5HUOXWUz5kA/w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5954,7 +6439,6 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", "hasInstallScript": true, - "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -6137,6 +6621,15 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", @@ -6259,6 +6752,15 @@ "node": ">=6" } }, + "node_modules/cmd-shim": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.1.tgz", + "integrity": "sha512-S9iI9y0nKR4hwEQsVWpyxld/6kRfGepGfzff83FcaiEBpmvlbA2nnGe7Cylgrx2f/p1P5S5wpRm9oL8z1PbS3Q==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6441,6 +6943,12 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/css-color-keywords": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", @@ -6507,12 +7015,30 @@ "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", "dev": true }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", @@ -6561,6 +7087,18 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", + "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -6593,8 +7131,7 @@ "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" }, "node_modules/decode-uri-component": { "version": "0.2.2", @@ -7080,6 +7617,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -7093,6 +7654,15 @@ "es6-promise": "^4.0.3" } }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -7879,6 +8449,14 @@ "npm": ">=3" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -7949,6 +8527,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", @@ -7991,6 +8582,11 @@ "node": ">= 6" } }, + "node_modules/fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -8039,6 +8635,29 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8111,6 +8730,11 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/flatstr": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", + "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" + }, "node_modules/flatted": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", @@ -8165,6 +8789,18 @@ "node": ">= 14.17" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/fraction.js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", @@ -8206,6 +8842,30 @@ "tslib": "^2.1.0" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -9398,6 +10058,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==" }, + "node_modules/jayson/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/jest": { "version": "29.4.2", "resolved": "https://registry.npmjs.org/jest/-/jest-29.4.2.tgz", @@ -10467,6 +11135,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/logflare-transport-core": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/logflare-transport-core/-/logflare-transport-core-0.2.5.tgz", + "integrity": "sha512-CufxlLL81hAnJgaO8UL4AkeeyUHwrwEhcjI+w0VJY/5U7NaA5nwc+pyTXrTXH76UxHttauegGIjX2FfeA2XoQg==", + "dependencies": { + "@types/lodash": "^4.14.153", + "axios": "^0.21.1", + "big-integer": "^1.6.48", + "bignumber.js": "^9.0.0", + "decimal.js": "^10.2.0", + "lodash": "^4.17.15" + } + }, + "node_modules/logflare-transport-core/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10589,6 +11278,11 @@ "node": ">= 8" } }, + "node_modules/mersenne-twister": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mersenne-twister/-/mersenne-twister-1.1.0.tgz", + "integrity": "sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==" + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -10681,6 +11375,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.5.tgz", + "integrity": "sha512-+yQl7SX3bIT83Lhb4BVorMAHVuqsskxRdlmO9kTpyukp8vsm2Sn/fUOV9xlnG8/a5JsypJzap21lz/y3FBMJ8Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -11169,6 +11909,11 @@ } } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "node_modules/next/node_modules/postcss": { "version": "8.4.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", @@ -11197,6 +11942,25 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.6.8", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", @@ -11265,6 +12029,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.0.tgz", + "integrity": "sha512-g+DPQSkusnk7HYXr75NtzkIP4+N81i3RPsGFidF3DzHd9MT9wWngmqoeg/fnHFz5MNdtG4w03s+QnhewSLTT2Q==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -11414,9 +12187,9 @@ } }, "node_modules/on-exit-leak-free": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", - "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" }, "node_modules/once": { "version": "1.4.0", @@ -11678,39 +12451,137 @@ } }, "node_modules/pino": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-7.11.0.tgz", - "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", + "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", "dependencies": { "atomic-sleep": "^1.0.0", - "fast-redact": "^3.0.0", - "on-exit-leak-free": "^0.2.0", - "pino-abstract-transport": "v0.5.0", - "pino-std-serializers": "^4.0.0", - "process-warning": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "real-require": "^0.1.0", - "safe-stable-stringify": "^2.1.0", - "sonic-boom": "^2.2.1", - "thread-stream": "^0.15.1" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", - "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", "dependencies": { - "duplexify": "^4.1.2", + "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", + "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-logflare": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/pino-logflare/-/pino-logflare-0.3.12.tgz", + "integrity": "sha512-+b61yGXEwrT3vDWg3YiX7qwBGEoNswwoNbaaHjiD08nXG1PPug2+Z3HM7E2eEWJ9ZTHksFy7svhul9gy/BTJ0Q==", + "dependencies": { + "axios": "^0.21.1", + "batch2": "^1.0.6", + "commander": "^5.0.0", + "fast-json-parse": "^1.0.3", + "lodash": "^4.17.15", + "logflare-transport-core": "^0.2.5", + "pino": "^6.3.2", + "pumpify": "^2.0.1", + "split2": "^3.1.1", + "stream-browserify": "^3.0.0", + "through2": "^3.0.1" + }, + "bin": { + "pino-logflare": "dist/cli.js" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/pino-logflare/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/pino-logflare/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pino-logflare/node_modules/pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "dependencies": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-logflare/node_modules/pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "node_modules/pino-logflare/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + }, + "node_modules/pino-logflare/node_modules/sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, + "node_modules/pino-logflare/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/pino-std-serializers": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", - "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz", + "integrity": "sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==" }, "node_modules/pirates": { "version": "4.0.5", @@ -11989,9 +12860,9 @@ } }, "node_modules/process-warning": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-1.0.0.tgz", - "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", + "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" }, "node_modules/prompts": { "version": "2.4.2", @@ -12033,6 +12904,25 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "dependencies": { + "duplexify": "^4.1.1", + "inherits": "^2.0.3", + "pump": "^3.0.0" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -12177,6 +13067,15 @@ "react": "^18.2.0" } }, + "node_modules/react-fast-marquee": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/react-fast-marquee/-/react-fast-marquee-1.3.5.tgz", + "integrity": "sha512-eOqLoz4iVVBvi2wN/web8hd2XX9y2Z6CYR7g++7nTVHlTOXBtqyARQJ9rYNpbp179hAzloMx0yBFAo8LpNYmKQ==", + "peerDependencies": { + "react": ">= 16.8.0 || 18.0.0", + "react-dom": ">= 16.8.0 || 18.0.0" + } + }, "node_modules/react-hot-toast": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.0.tgz", @@ -12197,6 +13096,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-jazzicon": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-jazzicon/-/react-jazzicon-1.0.4.tgz", + "integrity": "sha512-/3kWv5vtAhI18GBFoqjpxRTtL+EImuB73PAC02r/zJQ6E+PAUmoBx8edYvTCIYHwS01uFf6N3elTDqSrVPwg4w==", + "dependencies": { + "mersenne-twister": "^1.1.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", @@ -12242,6 +13153,23 @@ } } }, + "node_modules/react-spring": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-9.7.1.tgz", + "integrity": "sha512-o2+r2DNQDVEuefiz33ZF76DPd/gLq3kbdObJmllGF2IUfv2W6x+ZP0gR97QYCSR4QLbmOl1mPKUBbI+FJdys2Q==", + "dependencies": { + "@react-spring/core": "~9.7.1", + "@react-spring/konva": "~9.7.1", + "@react-spring/native": "~9.7.1", + "@react-spring/three": "~9.7.1", + "@react-spring/web": "~9.7.1", + "@react-spring/zdog": "~9.7.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -12303,6 +13231,15 @@ "node": ">=0.10.0" } }, + "node_modules/read-cmd-shim": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -12328,9 +13265,9 @@ } }, "node_modules/real-require": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", - "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", "engines": { "node": ">= 12.13.0" } @@ -12556,6 +13493,14 @@ "utf-8-validate": "^5.0.2" } }, + "node_modules/rpc-websockets/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/rpc-websockets/node_modules/ws": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.0.tgz", @@ -12835,9 +13780,9 @@ } }, "node_modules/sonic-boom": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz", - "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", + "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -12878,9 +13823,9 @@ } }, "node_modules/split2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", - "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "engines": { "node": ">= 10.x" } @@ -13189,6 +14134,39 @@ } } }, + "node_modules/supabase": { + "version": "1.45.2", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-1.45.2.tgz", + "integrity": "sha512-M9Mk8+pj6F5sTGS8m0lDlOQ5WUgI6TWQBL8wnSobq834ZwImUgn6731NDoetsUd2GFsG7DFffPU+HzgLWbnqkQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bin-links": "^4.0.1", + "node-fetch": "^3.2.10", + "tar": "6.1.13" + }, + "bin": { + "supabase": "bin/supabase" + } + }, + "node_modules/supabase/node_modules/node-fetch": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz", + "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/superstruct": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", @@ -13282,6 +14260,23 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -13308,11 +14303,11 @@ "dev": true }, "node_modules/thread-stream": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz", - "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", + "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", "dependencies": { - "real-require": "^0.1.0" + "real-require": "^0.2.0" } }, "node_modules/through": { @@ -13320,6 +14315,15 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "node_modules/through2": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "2 || 3" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -13458,6 +14462,11 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -13667,7 +14676,6 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", "hasInstallScript": true, - "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -13693,9 +14701,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } @@ -13805,6 +14813,15 @@ "makeerror": "1.0.12" } }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/webcrypto-core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.5.tgz", @@ -13822,6 +14839,35 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, + "node_modules/websocket": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", + "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.50", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -14015,6 +15061,14 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "engines": { + "node": ">=0.10.32" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index 0b2e848e5..4da848196 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopgate", - "version": "0.2.1", + "version": "1.0.0", "description": "Easily Token-Gate Content using Loopring Layer-2 NFTs and Pinata", "author": "Geel <@0xGeel>", "license": "BSD-2-Clause", @@ -13,7 +13,8 @@ "start": "next start", "lint": "next lint", "test": "jest", - "test:watch": "jest --watch" + "test:watch": "jest --watch", + "supabase:types": "npx supabase gen types typescript --project-id wyskjtopiuiwkkqclyho --schema public > src/utils/supabase/types.ts" }, "dependencies": { "@heroicons/react": "^2.0.13", @@ -29,21 +30,30 @@ "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-tooltip": "^1.0.3", + "@supabase/supabase-js": "^2.11.0", "axios": "^1.2.3", "axios-rate-limit": "^1.3.0", "class-variance-authority": "^0.4.0", "clsx": "^1.2.1", "connectkit": "^1.1.1", + "crypto": "^1.0.1", + "date-fns": "^2.29.3", "ethers": "^5.7.2", "iron-session": "^6.3.1", "next": "^13.0.0", "pinata-submarine": "^0.1.6", + "pino": "^8.11.0", + "pino-logflare": "^0.3.12", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-fast-marquee": "^1.3.5", "react-hot-toast": "^2.4.0", + "react-jazzicon": "^1.0.4", + "react-spring": "^9.7.1", "siwe": "^2.1.3", "tailwind-merge": "^1.9.0", "tailwindcss-animate": "^1.0.5", + "uuid": "^9.0.0", "wagmi": "~0.10.0" }, "devDependencies": { @@ -53,12 +63,14 @@ "@types/node": "^17.0.31", "@types/react": "^18.0.9", "@types/react-dom": "^18.0.3", + "@types/uuid": "^9.0.1", "autoprefixer": "^10.4.13", "eslint": "^8.15.0", "eslint-config-next": "^12.1.6", "jest": "^29.4.2", "jest-environment-jsdom": "^29.4.2", "postcss": "^8.4.21", + "supabase": "^1.45.2", "tailwindcss": "^3.2.4", "typescript": "^4.6.4" } diff --git a/public/images/admin-panel-teaser.jpg b/public/images/admin-panel-teaser.jpg new file mode 100644 index 000000000..2fe4ae49d Binary files /dev/null and b/public/images/admin-panel-teaser.jpg differ diff --git a/public/images/icon/icon.svg b/public/images/icon/icon.svg index c0dc706bc..9fd5b72a9 100644 --- a/public/images/icon/icon.svg +++ b/public/images/icon/icon.svg @@ -1,3 +1,3 @@ - + diff --git a/public/images/logo/logo.svg b/public/images/logo/logo.svg new file mode 100644 index 000000000..e2940db72 --- /dev/null +++ b/public/images/logo/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/logo/loopgate-brand-dark.svg b/public/images/logo/loopgate-brand-dark.svg deleted file mode 100644 index 9ba68eef4..000000000 --- a/public/images/logo/loopgate-brand-dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/logo/loopgate-brand-light.svg b/public/images/logo/loopgate-brand-light.svg deleted file mode 100644 index 3508a3393..000000000 --- a/public/images/logo/loopgate-brand-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/logo/loopgate-brand.svg b/public/images/logo/loopgate-brand.svg deleted file mode 100644 index 9702d699a..000000000 --- a/public/images/logo/loopgate-brand.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/logo/loopgate-dark.svg b/public/images/logo/loopgate-dark.svg deleted file mode 100644 index c5cbdbea7..000000000 --- a/public/images/logo/loopgate-dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/logo/loopgate-light.svg b/public/images/logo/loopgate-light.svg deleted file mode 100644 index 908221a53..000000000 --- a/public/images/logo/loopgate-light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/loopgate-glossy.png b/public/images/loopgate-glossy.png new file mode 100644 index 000000000..2dbbca648 Binary files /dev/null and b/public/images/loopgate-glossy.png differ diff --git a/public/images/marquee/logos/gme-wallet.png b/public/images/marquee/logos/gme-wallet.png new file mode 100644 index 000000000..29861dc14 Binary files /dev/null and b/public/images/marquee/logos/gme-wallet.png differ diff --git a/public/images/marquee/logos/loopring.png b/public/images/marquee/logos/loopring.png new file mode 100644 index 000000000..8d3a42788 Binary files /dev/null and b/public/images/marquee/logos/loopring.png differ diff --git a/public/images/marquee/logos/metamask.png b/public/images/marquee/logos/metamask.png new file mode 100644 index 000000000..ea20451b5 Binary files /dev/null and b/public/images/marquee/logos/metamask.png differ diff --git a/public/images/marquee/logos/netlify.png b/public/images/marquee/logos/netlify.png new file mode 100644 index 000000000..dd56189f9 Binary files /dev/null and b/public/images/marquee/logos/netlify.png differ diff --git a/public/images/marquee/logos/next.png b/public/images/marquee/logos/next.png new file mode 100644 index 000000000..c1b047d7d Binary files /dev/null and b/public/images/marquee/logos/next.png differ diff --git a/public/images/marquee/logos/pinata.png b/public/images/marquee/logos/pinata.png new file mode 100644 index 000000000..473a077c8 Binary files /dev/null and b/public/images/marquee/logos/pinata.png differ diff --git a/public/images/marquee/logos/siwe.png b/public/images/marquee/logos/siwe.png new file mode 100644 index 000000000..b2acecee7 Binary files /dev/null and b/public/images/marquee/logos/siwe.png differ diff --git a/public/images/marquee/logos/supabase.png b/public/images/marquee/logos/supabase.png new file mode 100644 index 000000000..9c082bb76 Binary files /dev/null and b/public/images/marquee/logos/supabase.png differ diff --git a/public/images/marquee/logos/walletconnect.png b/public/images/marquee/logos/walletconnect.png new file mode 100644 index 000000000..78c008b93 Binary files /dev/null and b/public/images/marquee/logos/walletconnect.png differ diff --git a/public/images/og-image.png b/public/images/og-image.png new file mode 100644 index 000000000..d18c100ec Binary files /dev/null and b/public/images/og-image.png differ diff --git a/public/images/unlocks-og-image.png b/public/images/unlocks-og-image.png new file mode 100644 index 000000000..9c661861d Binary files /dev/null and b/public/images/unlocks-og-image.png differ diff --git a/src/components/ConnectPrompt/ConnectPrompt.tsx b/src/components/ConnectPrompt/ConnectPrompt.tsx deleted file mode 100644 index df255294f..000000000 --- a/src/components/ConnectPrompt/ConnectPrompt.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ConnectKitButton } from "connectkit"; -import { useAccount } from "wagmi"; - -const SignInHint = () => ( -
-
-

- Sign in to continue -

-
-); - -const ConnectPrompt = () => { - const { address } = useAccount(); - - return ( -
-
-

- Connect with your L2 Wallet -

-

- To see if you are worthy of unlocking content... -

-
- {address ? ( -
- - -
- ) : ( - - )} -
- ); -}; - -export default ConnectPrompt; diff --git a/src/components/ConnectPrompt/index.ts b/src/components/ConnectPrompt/index.ts deleted file mode 100644 index 172aa3920..000000000 --- a/src/components/ConnectPrompt/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import ConnectPrompt from "./ConnectPrompt"; -export default ConnectPrompt; diff --git a/src/components/ConnectedPage/ConnectedPage.tsx b/src/components/ConnectedPage/ConnectedPage.tsx deleted file mode 100644 index 1cb0ee1d0..000000000 --- a/src/components/ConnectedPage/ConnectedPage.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useState } from "react"; - -import EmptyState from "./EmptyState"; -import UnlockLink from "./UnlockLink"; -import Spinner from "../Spinner"; -import { useAccount } from "wagmi"; -import axios from "axios"; -import toast from "react-hot-toast"; - -const ConnectedPage = () => { - const { address } = useAccount(); - const [isLoading, setIsLoading] = useState(true); - const [unlocks, setUnlocks] = useState([]); - - const getUserUnlocks = (address: `0x${string}` | undefined) => { - axios - .get(`/api/getUserUnlocks?address=${address}`) - .then((data) => { - setUnlocks(data.data.unlocks); - setIsLoading(false); - }) - .catch((error) => { - toast.error(error.request.response); - setIsLoading(false); - }); - }; - - // On render: make API calls to determine NFT holdings - // 1.: GET user's Loopring ID (Loopring API) - // 2.: GET user's NFTs (Loopring API) - // 3.: Check config to compare NFTs and unlocks - // 4.: GET submarined content (Pinata API) - - useEffect(() => { - setIsLoading(true); - getUserUnlocks(address); - }, [address]); - - return ( -
-
-

- Your unlocked content -

- - {isLoading ? ( -
- -

Loading...

-
- ) : ( - <> - {unlocks.length >= 1 ? ( -
- {unlocks.map((unlock) => ( - - ))} -
- ) : ( - - )} - - )} -
-
- ); -}; - -export default ConnectedPage; diff --git a/src/components/ConnectedPage/EmptyState.tsx b/src/components/ConnectedPage/EmptyState.tsx deleted file mode 100644 index 104330e71..000000000 --- a/src/components/ConnectedPage/EmptyState.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useDisconnect } from "wagmi"; -import Button from "../Button"; -import { EyeSlashIcon, XMarkIcon } from "@heroicons/react/24/outline"; - -const EmptyState = () => { - const { disconnect } = useDisconnect(); - - return ( -
- -
-

- There's nothing here! -

-

- Bummer.. your wallet does not hold any NFTs that provide access to - token-gated content. -

-
- -
- ); -}; - -export default EmptyState; diff --git a/src/components/ConnectedPage/UnlockLink.tsx b/src/components/ConnectedPage/UnlockLink.tsx deleted file mode 100644 index 72614bf0c..000000000 --- a/src/components/ConnectedPage/UnlockLink.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { - LockClosedIcon, - LockOpenIcon, - ChevronRightIcon, -} from "@heroicons/react/24/solid"; - -interface Props { - title: string; - unlockUrl: string; - cid: `baf${string}`; -} - -const UnlockLink = ({ title, unlockUrl, cid }: Props) => { - return ( - -
- - -
-
-

- {title ? title : "Untitled"} -

-

{cid}

-
- -
- ); -}; - -export default UnlockLink; diff --git a/src/components/ConnectedPage/index.ts b/src/components/ConnectedPage/index.ts deleted file mode 100644 index 5ef37f4bc..000000000 --- a/src/components/ConnectedPage/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import ConnectedPage from "./ConnectedPage"; -export default ConnectedPage; diff --git a/src/components/Fonts/index.ts b/src/components/Fonts/index.ts new file mode 100644 index 000000000..ee2eea46b --- /dev/null +++ b/src/components/Fonts/index.ts @@ -0,0 +1,3 @@ +import { inter, unbounded } from "./Fonts"; + +export { inter, unbounded }; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 03e49cce2..546eb9200 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,11 +1,17 @@ import { ConnectKitButton } from "connectkit"; import Image from "next/image"; -import LogoBrandLight from "@/public/images/logo/loopgate-brand-light.svg"; +import LogoSrc from "@/public/images/logo/logo.svg"; +import Link from "next/link"; const Header = () => { return ( -
- loopgate Logo +
+ + LoopGate Logo +
diff --git a/src/components/Pages/Home/CTABanner/CTABanner.tsx b/src/components/Pages/Home/CTABanner/CTABanner.tsx new file mode 100644 index 000000000..04c13f113 --- /dev/null +++ b/src/components/Pages/Home/CTABanner/CTABanner.tsx @@ -0,0 +1,68 @@ +import { ArrowRightIcon } from "@heroicons/react/24/outline"; +import Image from "next/image"; + +const CTABanner = () => { + return ( +
+
+ + +
+

+ LoopGate is in development. + Stay tuned for updates. +

+

+ We have a vision of making Token-Gating with Loopring L2 NFTs for + non-techies a reality. This takes time. We'll keep you in the + loop on how this goes. +

+ +
+
+ LoopGate's Admin UI +
+
+
+ ); +}; + +export default CTABanner; diff --git a/src/components/Pages/Home/CTABanner/index.ts b/src/components/Pages/Home/CTABanner/index.ts new file mode 100644 index 000000000..949ffce29 --- /dev/null +++ b/src/components/Pages/Home/CTABanner/index.ts @@ -0,0 +1,2 @@ +import CTABanner from "./CTABanner"; +export default CTABanner; diff --git a/src/components/Pages/Home/ContentBlocks/Block.tsx b/src/components/Pages/Home/ContentBlocks/Block.tsx new file mode 100644 index 000000000..215aebba6 --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/Block.tsx @@ -0,0 +1,71 @@ +import { ReactNode } from "react"; +import { cn } from "@/src/utils/generic"; + +export enum ColourModes { + ACCENT = "accent", + BASE = "base", +} + +type Props = { + title: string; + description: string[]; + icon: ReactNode; + colourMode: ColourModes; + children: ReactNode | ReactNode[]; + className: string; +}; + +const Block = ({ + title, + description, + icon, + colourMode, + children, + className, +}: Props) => { + return ( +
+
+ {icon} +
+

{title}

+ {description.map((item) => ( +

+ {item} +

+ ))} + {children} +
+ ); +}; + +export default Block; diff --git a/src/components/Pages/Home/ContentBlocks/BrowseAllBtn.tsx b/src/components/Pages/Home/ContentBlocks/BrowseAllBtn.tsx new file mode 100644 index 000000000..e33ab446e --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/BrowseAllBtn.tsx @@ -0,0 +1,35 @@ +import { ArrowLongRightIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; + +type Props = { + label: string; + url: string; +}; + +const Inner = ({ label }: { label: string }) => ( + <> + {label} + + +); + +const CtaBtn = ({ label, url }: Props) => { + const style = + "inline-flex items-center space-x-2 bg-white/20 border border-white/10 text-slate-900 font-medium px-3 py-2 rounded-md hover:bg-white/30 duration-150 mt-6 shadow-md shadow-slate-900/20"; + + return ( + <> + {url.startsWith("#") ? ( + + + + ) : ( + + + + )} + + ); +}; + +export default CtaBtn; diff --git a/src/components/Pages/Home/ContentBlocks/ContentBlocks.tsx b/src/components/Pages/Home/ContentBlocks/ContentBlocks.tsx new file mode 100644 index 000000000..5b61347db --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/ContentBlocks.tsx @@ -0,0 +1,138 @@ +import { UnlockableV2 } from "@/src/config/types"; +import { ColourModes } from "./Block"; +import { + SparklesIcon, + ShieldCheckIcon, + RocketLaunchIcon, + CodeBracketIcon, + DocumentTextIcon, +} from "@heroicons/react/24/outline"; + +import Block from "./Block"; +import CtaBtn from "./BrowseAllBtn"; +import LogoMarquee from "./LogoMarquee"; +import Links from "./Links"; +import UnlockablesBlock from "./UnlockablesBlock"; + +type Props = { + unlockables: UnlockableV2[] | []; +}; + +const ContentBlocks = ({ unlockables }: Props) => { + const iconStyle = "w-8 h-8 flex-shrink-0"; + const linkIconStyle = "opacity-80 w-4 h-4"; + + return ( +
+ } + colourMode={ColourModes.ACCENT} + className="lg:col-span-7" + > + + + + + } + colourMode={ColourModes.BASE} + className="lg:col-span-5" + > + + + } + colourMode={ColourModes.BASE} + className="lg:col-span-7" + > + , + label: "Read the documentation", + url: "https://0xgeel.gitbook.io/loopgate-documentation/", + }, + { + icon: , + label: "Check out the Source Code", + url: "https://github.com/0xGeel/loopgate", + }, + { + icon: , + label: "Read the License", + url: "https://github.com/0xGeel/loopgate/blob/main/LICENSE", + }, + ]} + /> + +
+ ); +}; + +export default ContentBlocks; diff --git a/src/components/Pages/Home/ContentBlocks/Links.tsx b/src/components/Pages/Home/ContentBlocks/Links.tsx new file mode 100644 index 000000000..7699368ed --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/Links.tsx @@ -0,0 +1,32 @@ +type Link = { + icon: React.ReactNode; + label: string; + url: string; +}; + +type Props = { + title: string; + links: Link[]; +}; + +const Links = ({ title, links }: Props) => { + return ( +
+

{title}

+ {links.map((item) => ( + + {item.icon} + {item.label} + + ))} +
+ ); +}; + +export default Links; diff --git a/src/components/Pages/Home/ContentBlocks/LogoMarquee.tsx b/src/components/Pages/Home/ContentBlocks/LogoMarquee.tsx new file mode 100644 index 000000000..76d312a4b --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/LogoMarquee.tsx @@ -0,0 +1,44 @@ +import RFMarquee from "react-fast-marquee"; +import Image from "next/image"; + +interface Logo { + src: string; + alt: string; + href: string; +} + +interface Props { + logos: Logo[]; + className?: string; +} + +const LogoMarquee = ({ logos, className }: Props) => { + return ( + + {logos.map((item, i) => ( + + {item.alt} + + ))} + + ); +}; + +export default LogoMarquee; diff --git a/src/components/Pages/Home/ContentBlocks/UnlockablesBlock.tsx b/src/components/Pages/Home/ContentBlocks/UnlockablesBlock.tsx new file mode 100644 index 000000000..543a6b765 --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/UnlockablesBlock.tsx @@ -0,0 +1,66 @@ +import { UnlockableV2 } from "@/src/config/types"; +import { + ArrowLongRightIcon, + FolderOpenIcon, +} from "@heroicons/react/24/outline"; +import Link from "next/link"; +import UnlockablesItem from "./UnlockablesItem"; + +type Props = { + unlockables: UnlockableV2[] | []; +}; + +const BrowseAllLink = () => ( + + + Browse all{" "} + unlockable content + + + +); + +const EmptyState = () => ( +
+
+ +

This LoopGate instance has no Unlockables (yet).

+ + Learn how to get started + +
+
+); + +const UnlockablesBlock = ({ unlockables }: Props) => { + return ( +
+
+

Browse Unlockables

+ + [ {unlockables.length} ] + +
+ {unlockables.length === 0 ? ( + + ) : ( + <> + {unlockables.slice(0, 3).map((item) => ( + + ))} + + + )} +
+ ); +}; + +export default UnlockablesBlock; diff --git a/src/components/Pages/Home/ContentBlocks/UnlockablesItem.tsx b/src/components/Pages/Home/ContentBlocks/UnlockablesItem.tsx new file mode 100644 index 000000000..a30116f48 --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/UnlockablesItem.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; +import { UnlockableV2 } from "@/src/config/types"; +import Jazzicon from "react-jazzicon"; +import { uuidToNumber } from "@/src/utils/generic"; + +type Props = { + unlockable: UnlockableV2; +}; + +const UnlockablesItem = ({ unlockable }: Props) => { + const fallback = { + title: "An Unnamed Unlockable...", + description: "No description...", + }; + + return ( + + +
+

+ {unlockable.metadata?.name + ? unlockable.metadata?.name + : fallback.title} +

+

+ {unlockable.metadata?.description + ? unlockable.metadata?.description + : fallback.description} +

+
+ + ); +}; + +export default UnlockablesItem; diff --git a/src/components/Pages/Home/ContentBlocks/index.ts b/src/components/Pages/Home/ContentBlocks/index.ts new file mode 100644 index 000000000..0568a7601 --- /dev/null +++ b/src/components/Pages/Home/ContentBlocks/index.ts @@ -0,0 +1,2 @@ +import ContentBlocks from "./ContentBlocks"; +export default ContentBlocks; diff --git a/src/components/Pages/Home/Hero/Hero.tsx b/src/components/Pages/Home/Hero/Hero.tsx new file mode 100644 index 000000000..e8f1e6d77 --- /dev/null +++ b/src/components/Pages/Home/Hero/Hero.tsx @@ -0,0 +1,35 @@ +import HeroGlow from "./HeroGlow"; +import HeroImage from "./HeroImage"; +import { ArrowLongDownIcon } from "@heroicons/react/24/outline"; + +type Props = { + title: string; + subtitle: string; + ctaText: string; +}; + +const Hero = ({ title, subtitle, ctaText }: Props) => { + return ( +
+ + +
+

+ {title} +

+

+ {subtitle} +

+ +

{ctaText}

+ +
+
+
+ ); +}; + +export default Hero; diff --git a/src/components/Pages/Home/Hero/HeroGlow.tsx b/src/components/Pages/Home/Hero/HeroGlow.tsx new file mode 100644 index 000000000..468c0495e --- /dev/null +++ b/src/components/Pages/Home/Hero/HeroGlow.tsx @@ -0,0 +1,18 @@ +type Props = { + className: string; +}; + +const HeroGlow = ({ className }: Props) => ( +
+ + + +
+); + +export default HeroGlow; diff --git a/src/components/Pages/Home/Hero/HeroImage.tsx b/src/components/Pages/Home/Hero/HeroImage.tsx new file mode 100644 index 000000000..726fed052 --- /dev/null +++ b/src/components/Pages/Home/Hero/HeroImage.tsx @@ -0,0 +1,29 @@ +import Image from "next/image"; + +const Img = ({ size, className }: { size: number; className: string }) => ( + A glossy rendering of LoopGate's Logo +); + +const HeroImage = () => { + const imgSize = { + mobile: 240, + desktop: 420, + }; + + return ( + <> + + + + ); +}; + +export default HeroImage; diff --git a/src/components/Pages/Home/Hero/index.ts b/src/components/Pages/Home/Hero/index.ts new file mode 100644 index 000000000..c15a96e36 --- /dev/null +++ b/src/components/Pages/Home/Hero/index.ts @@ -0,0 +1,2 @@ +import Hero from "./Hero"; +export default Hero; diff --git a/src/components/Pages/Home/UseCases/ContentItem.tsx b/src/components/Pages/Home/UseCases/ContentItem.tsx new file mode 100644 index 000000000..d4965a080 --- /dev/null +++ b/src/components/Pages/Home/UseCases/ContentItem.tsx @@ -0,0 +1,59 @@ +import { + DocumentIcon, + FilmIcon, + FolderIcon, + GiftIcon, + MicrophoneIcon, + MusicalNoteIcon, + NewspaperIcon, + PaperClipIcon, + QrCodeIcon, + RadioIcon, + ShoppingCartIcon, + TicketIcon, + VideoCameraIcon, +} from "@heroicons/react/24/outline"; + +import { Content, IconNames } from "./types"; + +const renderIcon = (iconName?: IconNames) => { + const iconStyle = "w-5 h-5 text-sky-500"; + + switch (iconName) { + case IconNames.FILM: + return ; + case IconNames.FOLDER: + return ; + case IconNames.GIFT: + return ; + case IconNames.MICROPHONE: + return ; + case IconNames.MUSIC: + return ; + case IconNames.NEWSPAPER: + return ; + case IconNames.PAPERCLIP: + return ; + case IconNames.QR: + return ; + case IconNames.RADIO: + return ; + case IconNames.SHOPPING_CART: + return ; + case IconNames.TICKET: + return ; + case IconNames.VIDEO_CAMERA: + return ; + default: + return ; + } +}; + +const ContentItem = ({ title, iconName }: Content) => ( +
+ {renderIcon(iconName)} + {title} +
+); + +export default ContentItem; diff --git a/src/components/Pages/Home/UseCases/MarqueeRow.tsx b/src/components/Pages/Home/UseCases/MarqueeRow.tsx new file mode 100644 index 000000000..d5ff61a92 --- /dev/null +++ b/src/components/Pages/Home/UseCases/MarqueeRow.tsx @@ -0,0 +1,25 @@ +import { Content, Direction } from "./types"; +import Marquee from "react-fast-marquee"; +import ContentItem from "./ContentItem"; + +type Props = { + content: Content[]; + direction: Direction; +}; + +const MarqueeRow = ({ content, direction }: Props) => { + return ( + + {content.map((item, i) => ( + + ))} + + ); +}; + +export default MarqueeRow; diff --git a/src/components/Pages/Home/UseCases/UseCases.tsx b/src/components/Pages/Home/UseCases/UseCases.tsx new file mode 100644 index 000000000..372d49ec0 --- /dev/null +++ b/src/components/Pages/Home/UseCases/UseCases.tsx @@ -0,0 +1,150 @@ +import { useState } from "react"; +import { ArrowLongRightIcon } from "@heroicons/react/24/outline"; +import MarqueeRow from "./MarqueeRow"; +import { UseCase, IconNames, Direction } from "./types"; +import { animated } from "react-spring"; +import useBoop from "@/src/hooks/useBoop"; + +const data: UseCase[] = [ + { + audience: "Musicians", + description: + "Establish a stronger connection to your fans exclusive content, priority tickets to live events and more...", + content: [ + { + title: "HQ Downloads", + iconName: IconNames.MUSIC, + }, + { + title: "Sheet Music", + iconName: IconNames.DOCUMENT, + }, + { + title: "Access to Meet & Greets", + iconName: IconNames.TICKET, + }, + { + title: "Concert Recordings", + iconName: IconNames.MICROPHONE, + }, + { + title: "Early Access to Ticket Sales", + iconName: IconNames.TICKET, + }, + { + title: "Behind The Scenes Recordings", + iconName: IconNames.FILM, + }, + ], + }, + { + audience: "3D Artists", + description: + "A secure place to let your customers truly own their assets alongside other benefits...", + content: [ + { + title: "Downloadable, Metaverse-Ready Game Assets", + iconName: IconNames.FOLDER, + }, + { + title: "Concept Art", + iconName: IconNames.VIDEO_CAMERA, + }, + { + title: "Shopping Discount Codes", + iconName: IconNames.SHOPPING_CART, + }, + ], + }, + { + audience: "any L2 Creator", + description: + "Add utility to your NFTs by giving your customers cheeky benefits and true ownership of assets...", + content: [ + { + title: "Exclusive Digital Collectibles", + iconName: IconNames.GIFT, + }, + { + title: "Articles and Blog Posts", + iconName: IconNames.DOCUMENT, + }, + { + title: "Early Access to Content", + iconName: IconNames.NEWSPAPER, + }, + { + title: "Alpha Game Builds", + iconName: IconNames.FILM, + }, + { + title: "Loopring Red Packets", + iconName: IconNames.QR, + }, + { + title: "Shopping Discount Codes", + iconName: IconNames.SHOPPING_CART, + }, + ], + }, +]; + +const UseCases = () => { + const [selectedUseCase, setSelectedUseCase] = useState(0); + const [boopStyle, boopTrigger] = useBoop({ scale: 1.05 }); + + const onNextItem = () => { + if (selectedUseCase + 1 >= data.length) { + setSelectedUseCase(0); + } else { + setSelectedUseCase(selectedUseCase + 1); + } + boopTrigger(); + }; + + return ( +
+
+

Use Cases

+ +
+ +
+

+ For{" "} + + {data[selectedUseCase].audience} + +

+

+ {data[selectedUseCase].description} +

+
+
+ + +
+
+
+ ); +}; + +export default UseCases; diff --git a/src/components/Pages/Home/UseCases/index.ts b/src/components/Pages/Home/UseCases/index.ts new file mode 100644 index 000000000..75ebc6389 --- /dev/null +++ b/src/components/Pages/Home/UseCases/index.ts @@ -0,0 +1,2 @@ +import UseCases from "./UseCases"; +export default UseCases; diff --git a/src/components/Pages/Home/UseCases/types.ts b/src/components/Pages/Home/UseCases/types.ts new file mode 100644 index 000000000..e3f4bbc60 --- /dev/null +++ b/src/components/Pages/Home/UseCases/types.ts @@ -0,0 +1,31 @@ +export enum IconNames { + DOCUMENT = "document", + FILM = "film", + FOLDER = "folder", + GIFT = "gift", + MICROPHONE = "microphone", + MUSIC = "music", + NEWSPAPER = "newspaper", + PAPERCLIP = "paperclip", + QR = "qr", + RADIO = "radio", + SHOPPING_CART = "shopping-cart", + TICKET = "ticket", + VIDEO_CAMERA = "video-camera", +} + +export type Content = { + title: string; + iconName?: IconNames; +}; + +export type UseCase = { + audience: string; + description: string; + content: Content[]; +}; + +export enum Direction { + LEFT = "left", + RIGHT = "right", +} diff --git a/src/components/SEO/NextHeadBase.tsx b/src/components/SEO/NextHeadBase.tsx index c608d1491..8addb7a4a 100644 --- a/src/components/SEO/NextHeadBase.tsx +++ b/src/components/SEO/NextHeadBase.tsx @@ -1,27 +1,33 @@ import Head from "next/head"; -const NextHeadBase = () => { - const baseUrl = "https://loopgate.netlify.app/"; - const ogImgUrl = `${baseUrl}/images/splash.png`; - const title = "LoopGate — Token-Gated Content using Loopring NFTs"; - const description = - "Securely and easily access exclusive digital content with LoopGate, using Loopring NFTs."; +type Props = { + title?: string; + baseUrl?: string; + ogImgUrl?: string; + description?: string; +}; +const NextHeadBase = ({ + title = "LoopGate — Token-Gated Content using Loopring NFTs", + baseUrl = "https://loopgate.netlify.app/", + ogImgUrl = `/images/og-image.png`, + description = "Securely and easily access exclusive digital content with LoopGate, using Loopring NFTs.", +}: Props) => { return ( - LoopGate - Token-Gated Content using Loopring NFTs + {title} - + - + ); diff --git a/src/components/UnlockablePage/404.tsx b/src/components/UnlockablePage/404.tsx new file mode 100644 index 000000000..87973c25a --- /dev/null +++ b/src/components/UnlockablePage/404.tsx @@ -0,0 +1,31 @@ +import Layout from "./Layout"; +import Link from "next/link"; + +type Props = { + label?: string; +}; + +const FourOhFour = ({ label }: Props) => { + return ( + + <> +

+ Four + –Oh– + Four +

+

+ {label ? label : "The page you were looking for could not be found."} +

+ + Go back home + + +
+ ); +}; + +export default FourOhFour; diff --git a/src/components/UnlockablePage/Layout.tsx b/src/components/UnlockablePage/Layout.tsx new file mode 100644 index 000000000..e35607a3a --- /dev/null +++ b/src/components/UnlockablePage/Layout.tsx @@ -0,0 +1,25 @@ +import Header from "../Header"; +import Footer from "../Footer"; +import { ReactNode } from "react"; +import { techPattern } from "@/src/styles/inline-styles"; +import { cn } from "@/src/utils/generic"; + +type Props = { + containerClass: string; + children: ReactNode | ReactNode[]; +}; + +const Layout = ({ containerClass, children }: Props) => { + return ( +
+
+
{children}
+
+
+ ); +}; + +export default Layout; diff --git a/src/components/UnlockablePage/List/List.tsx b/src/components/UnlockablePage/List/List.tsx new file mode 100644 index 000000000..4d07a6189 --- /dev/null +++ b/src/components/UnlockablePage/List/List.tsx @@ -0,0 +1,26 @@ +import ListItem from "./ListItem"; +import { UnlockableV2 } from "@/src/config/types"; + +type Props = { + unlockables: UnlockableV2[]; +}; + +const List = ({ unlockables }: Props) => { + return ( +
+

+ Browse Unlockables + + [ {unlockables.length} ] + +

+
+ {unlockables.map((item) => ( + + ))} +
+
+ ); +}; + +export default List; diff --git a/src/components/UnlockablePage/List/ListItem.tsx b/src/components/UnlockablePage/List/ListItem.tsx new file mode 100644 index 000000000..f50435c42 --- /dev/null +++ b/src/components/UnlockablePage/List/ListItem.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import { UnlockableV2 } from "@/src/config/types"; +import Jazzicon from "react-jazzicon"; +import { uuidToNumber } from "@/src/utils/generic"; + +type Props = { + unlockable: UnlockableV2; +}; + +const ListItem = ({ unlockable }: Props) => { + const fallback = { + title: "Unnamed unlockable", + description: "No description", + }; + + return ( + +
+ +
+

+ {unlockable.metadata?.name + ? unlockable.metadata?.name + : fallback.title} +

+

+ {unlockable.metadata?.description + ? unlockable.metadata?.description + : fallback.description} +

+
+
+ + ); +}; + +export default ListItem; diff --git a/src/components/UnlockablePage/List/index.ts b/src/components/UnlockablePage/List/index.ts new file mode 100644 index 000000000..e5780f79f --- /dev/null +++ b/src/components/UnlockablePage/List/index.ts @@ -0,0 +1,3 @@ +import List from "./List"; + +export default List; diff --git a/src/components/UnlockablePage/UnlockCard/Description.tsx b/src/components/UnlockablePage/UnlockCard/Description.tsx new file mode 100644 index 000000000..e61e21cf5 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/Description.tsx @@ -0,0 +1,11 @@ +type Props = { text: string | undefined }; + +const Description = ({ text }: Props) => ( +

+ {text + ? text + : "The owner of this unlockable has not (yet) specified a description for this content. We are sure it kicks ass though."} +

+); + +export default Description; diff --git a/src/components/UnlockablePage/UnlockCard/Metadata.tsx b/src/components/UnlockablePage/UnlockCard/Metadata.tsx new file mode 100644 index 000000000..6d1f7acd5 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/Metadata.tsx @@ -0,0 +1,45 @@ +import { UnlockCriteria } from "@/src/config/types"; +import { formatRelativeDate, truncate0x } from "@/src/utils/generic"; +import { + ClockIcon, + LockClosedIcon, + UserGroupIcon, +} from "@heroicons/react/20/solid"; + +type Props = { + lastUpdated: string | Date; + owner: `0x${string}`; + unlockCriteria: UnlockCriteria; +}; + +const Metadata = ({ lastUpdated, owner, unlockCriteria }: Props) => ( +
+
+ +

{formatRelativeDate(lastUpdated)}

+
+ + +

+ {truncate0x(owner)} +

+
+ {/* */} +
+); + +export default Metadata; diff --git a/src/components/UnlockablePage/UnlockCard/NoAccess.tsx b/src/components/UnlockablePage/UnlockCard/NoAccess.tsx new file mode 100644 index 000000000..a0312a382 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/NoAccess.tsx @@ -0,0 +1,21 @@ +import { NoSymbolIcon } from "@heroicons/react/20/solid"; + +const NoAccess = () => { + return ( +
+
+ +
+

+ No access granted +

+

+ Your connected wallet does not meet the unlock requirements. +

+
+
+
+ ); +}; + +export default NoAccess; diff --git a/src/components/UnlockablePage/UnlockCard/ShinyLogo.tsx b/src/components/UnlockablePage/UnlockCard/ShinyLogo.tsx new file mode 100644 index 000000000..d19f8beb1 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/ShinyLogo.tsx @@ -0,0 +1,22 @@ +import LoopGateLogo from "@/public/images/icon/icon.svg"; +import Image from "next/image"; + +const Img = ({ className }: { className?: string }) => { + return ( + LoopGate Icon + ); +}; + +const ShinyLogo = () => ( +
+ + +
+); + +export default ShinyLogo; diff --git a/src/components/UnlockablePage/UnlockCard/SignInHint.tsx b/src/components/UnlockablePage/UnlockCard/SignInHint.tsx new file mode 100644 index 000000000..76a84f408 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/SignInHint.tsx @@ -0,0 +1,10 @@ +const SignInHint = () => ( +
+
+

+ Sign in to continue +

+
+); + +export default SignInHint; diff --git a/src/components/UnlockablePage/UnlockCard/Title.tsx b/src/components/UnlockablePage/UnlockCard/Title.tsx new file mode 100644 index 000000000..bf996536a --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/Title.tsx @@ -0,0 +1,11 @@ +type Props = { + text: string | undefined; +}; + +const Title = ({ text }: Props) => ( +

+ {text ? text : "A mystery awaits..."} +

+); + +export default Title; diff --git a/src/components/UnlockablePage/UnlockCard/UnlockCard.tsx b/src/components/UnlockablePage/UnlockCard/UnlockCard.tsx new file mode 100644 index 000000000..1546ae08d --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/UnlockCard.tsx @@ -0,0 +1,52 @@ +import ShinyLogo from "./ShinyLogo"; +import { ConnectKitButton, useSIWE } from "connectkit"; +import { useAccount } from "wagmi"; +import SignInHint from "./SignInHint"; +import { UnlockableV2 } from "@/src/config/types"; +import Metadata from "./Metadata"; +import Title from "./Title"; +import Description from "./Description"; +import UnlockSection from "./UnlockSection"; + +type Props = { + unlockable: UnlockableV2; +}; + +const UnlockCard = ({ unlockable }: Props) => { + const { signedIn } = useSIWE(); + const { address } = useAccount(); + + return ( +
+
+ +
+ + <Description text={unlockable.metadata.description} /> + <Metadata + lastUpdated={unlockable.metadata.lastUpdated} + owner={unlockable.owner} + unlockCriteria={unlockable.unlockCriteria} + /> + </div> + </div> + {!signedIn ? ( + <div className="flex flex-col md:flex-row items-center border-t border-white/10 p-8 bg-gradient-to-b from-sky-500/10 to-transparent rounded-b relative"> + <div className="w-full h-full border absolute top-0 left-0 rounded-b animate-pulse border-white/50 opacity-5 pointer-events-none" /> + <div className="relative md:mr-6"> + <ConnectKitButton /> + {address && <SignInHint />} + </div> + <p className="text-sm text-white/60 max-w-sm text-center md:text-left mt-4 md:mt-0"> + Unlock this content by connecting with your wallet to verify you + have the required NFT(s). + </p> + </div> + ) : ( + <UnlockSection unlockable={unlockable} /> + )} + </div> + ); +}; + +export default UnlockCard; diff --git a/src/components/UnlockablePage/UnlockCard/UnlockLink.tsx b/src/components/UnlockablePage/UnlockCard/UnlockLink.tsx new file mode 100644 index 000000000..683dda443 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/UnlockLink.tsx @@ -0,0 +1,52 @@ +import { ArrowLongRightIcon } from "@heroicons/react/20/solid"; + +type Props = { + accessLink: string; +}; + +const PulseAnimation = () => { + const circleBaseClass = "border-4 border-sky-500 rounded-full absolute"; + const animationBaseClass = + "will-change-transform will-change-opacity fill-mode-forwards"; + + return ( + <div className="w-full h-full absolute top-0 left-0 -z-10 pointer-events-none overflow-hidden flex items-center justify-center"> + <div + className={`flex items-center justify-center animate-unlock-success ${animationBaseClass}`} + > + <div + className={`w-20 h-20 animate-unlock-success-1 ${animationBaseClass} ${circleBaseClass}`} + /> + <div + className={`w-40 h-40 animate-unlock-success-2 ${animationBaseClass} ${circleBaseClass}`} + /> + <div + className={`w-60 h-60 animate-unlock-success-3 ${animationBaseClass} ${circleBaseClass}`} + /> + </div> + </div> + ); +}; + +const UnlockLink = ({ accessLink }: Props) => { + return ( + <> + <PulseAnimation /> + <a + href={accessLink} + target="_blank" + rel="noreferrer" + className="flex justify-center items-center bg-sky-400 hover:bg-sky-300 duration-150 text-slate-900 px-8 py-4 border-slate-900 border-t-4 rounded-b-md group" + > + <div className="flex justify-center items-center space-x-2 transform group-hover:translate-x-4 group-active:scale-90 duration-150"> + <h2 className="font-display font-medium text-sm"> + Click here to gain access + </h2> + <ArrowLongRightIcon className="w-6 h-6" /> + </div> + </a> + </> + ); +}; + +export default UnlockLink; diff --git a/src/components/UnlockablePage/UnlockCard/UnlockSection.tsx b/src/components/UnlockablePage/UnlockCard/UnlockSection.tsx new file mode 100644 index 000000000..148394be1 --- /dev/null +++ b/src/components/UnlockablePage/UnlockCard/UnlockSection.tsx @@ -0,0 +1,65 @@ +import { UnlockableV2 } from "@/src/config/types"; +import { useAccount } from "wagmi"; +import { useState, useEffect } from "react"; +import axios from "axios"; +import Spinner from "../../Spinner"; +import UnlockLink from "./UnlockLink"; +import NoAccess from "./NoAccess"; +import toast from "react-hot-toast"; +import logger from "@/src/utils/logger"; + +type Props = { + unlockable: UnlockableV2; +}; + +const UnlockSection = ({ unlockable }: Props) => { + const { address } = useAccount(); + + const [isLoading, setIsLoading] = useState(false); + const [unlockedContent, setUnlockedContent] = useState<any>(null); + + const checkUnlock = ( + address: `0x${string}` | undefined, + unlockable: UnlockableV2 + ) => { + axios + .get( + `/api/checkUnlockable?address=${address}&unlockableId=${unlockable.id}` + ) + .then((data) => { + setUnlockedContent(data.data.unlock); + setIsLoading(false); + }) + .catch((error) => { + setIsLoading(false); + logger.error(error.request.response); + toast.error(error.request.response); + }); + }; + + useEffect(() => { + setIsLoading(true); + checkUnlock(address, unlockable); + }, [address, unlockable]); + + return ( + <> + {isLoading ? ( + <div className="flex justify-center items-center font-display text-sm space-x-2 border-t border-white/10 px-8 py-5 bg-gradient-to-b from-sky-500/10 to-transparent rounded-b"> + <Spinner /> + <p>Loading...</p> + </div> + ) : ( + <> + {unlockedContent ? ( + <UnlockLink accessLink={unlockedContent.accessLink} /> + ) : ( + <NoAccess /> + )} + </> + )} + </> + ); +}; + +export default UnlockSection; diff --git a/src/config/config.ts b/src/config/config.ts index 5130a94b2..feb1981d4 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1,4 +1,4 @@ -import { Unlockable } from "./types"; +import { Unlockable, UnlockableV2 } from "./types"; /********************************************************* Replace the `unlockables` below with your own content. @@ -41,4 +41,69 @@ const unlockables: Unlockable[] = [ }, ]; -export { unlockables }; +/********************************************************* + * Version 2: Unlockables that can be stored in a Supabase + * database, to be edited with a GUI instead of the config + *********************************************************/ +const unlockablesV2: UnlockableV2[] = [ + { + id: "ee3fd6ff-4718-4949-b621-f35ccad89ee4", + owner: "0x1337CC354AeAf15B0E98A609cd348DF171174e14", + metadata: { + name: "Token Gating with NFTs: Unlocking New Ways to Bring Value", + description: + "This exclusive article contains a primer on what Token Gating is, and provides four actionable prompts on how to implement it to bring value to members of your community.", + lastUpdated: "2023-03-13 16:05:23.481327", + }, + content: { + type: "IPFS", + url: "bafybeiehgpaip4f7jafzf7imgannx3nnv3ubaiwp6ph56mlyzijpqxi45m", + }, + unlockCriteria: { + unlockAmount: 1, + nftId: [ + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee", + ], + }, + }, + { + id: "c9e21ebf-59cc-42dd-9dc4-fd427be153b9", + owner: "0x1337CC354AeAf15B0E98A609cd348DF171174e14", + metadata: { + lastUpdated: "2023-03-13 16:05:23.481327", + }, + content: { + type: "IPFS", + url: "bafybeihx5eacyxeydcpvudwxa242rnjhn67femy46gzas5d2djb24ti5mi", + }, + unlockCriteria: { + unlockAmount: 1, + nftId: [ + "0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c", + ], + }, + }, + { + id: "3eade688-8839-4fd7-b97a-f7c5f5bfc6ad", + owner: "0x1337CC354AeAf15B0E98A609cd348DF171174e14", + metadata: { + name: "Flappy Bird: Origins", + description: + "An incredibly exclusive web game built in Godot 3, optimized for browsers. Dodge the obstacles, and fly for your life...", + lastUpdated: "2023-03-13 16:05:23.481327", + }, + content: { + type: "IPFS", + url: "bafybeihhx5v3saq3b7n55ub5q3atuw2udbqc5ictkv2ih7vd3hxptu22nu", + }, + unlockCriteria: { + unlockAmount: 2, + nftId: [ + "0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c", + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee", + ], + }, + }, +]; + +export { unlockables, unlockablesV2 }; diff --git a/src/config/types.ts b/src/config/types.ts index 9301317e2..e5eb212ca 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -4,6 +4,28 @@ export interface Unlockable { nftId: `0x${string}`[]; } +export interface Metadata { + name?: string; + description?: string; + lastUpdated: Date | string; +} + +export interface UnlockCriteria { + unlockAmount: number; + nftId: string[]; +} + +export interface UnlockableV2 { + id: string; + owner: `0x${string}`; + metadata: Metadata; + content: { + type: "IPFS"; + url: string; + }; + unlockCriteria: UnlockCriteria; +} + export class ConfigError extends Error { constructor(message: string) { super(message); diff --git a/src/hooks/useBoop.ts b/src/hooks/useBoop.ts new file mode 100644 index 000000000..564a2822a --- /dev/null +++ b/src/hooks/useBoop.ts @@ -0,0 +1,59 @@ +import { useState, useEffect, useCallback } from "react"; +import { useSpring, SpringConfig } from "react-spring"; +import usePrefersReducedMotion from "./usePrefersReducedMotion"; + +interface BoopProps { + x?: number; + y?: number; + rotation?: number; + scale?: number; + timing?: number; + springConfig?: SpringConfig; +} + +const useBoop = ({ + x = 0, + y = 0, + rotation = 0, + scale = 1, + timing = 150, + springConfig = { + tension: 300, + friction: 10, + }, +}: BoopProps): [{ transform?: string }, () => void] => { + const prefersReducedMotion = usePrefersReducedMotion(); + const [isBooped, setIsBooped] = useState(false); + + const style = useSpring({ + transform: isBooped + ? `translate(${x}px, ${y}px) + rotate(${rotation}deg) + scale(${scale})` + : `translate(0px, 0px) + rotate(0deg) + scale(1)`, + config: springConfig, + }); + + useEffect(() => { + if (!isBooped) { + return; + } + const timeoutId = window.setTimeout(() => { + setIsBooped(false); + }, timing); + return () => { + window.clearTimeout(timeoutId); + }; + }, [isBooped, timing]); + + const trigger = useCallback(() => { + setIsBooped(true); + }, []); + + const appliedStyle = prefersReducedMotion ? {} : style; + return [appliedStyle, trigger]; +}; + +export default useBoop; diff --git a/src/hooks/usePrefersReducedMotion.ts b/src/hooks/usePrefersReducedMotion.ts new file mode 100644 index 000000000..3308969eb --- /dev/null +++ b/src/hooks/usePrefersReducedMotion.ts @@ -0,0 +1,36 @@ +import { useState, useEffect } from "react"; + +const QUERY = "(prefers-reduced-motion: no-preference)"; +const isRenderingOnServer = typeof window === "undefined"; +const getInitialState = () => { + // For our initial server render, we won't know if the user + // prefers reduced motion, but it doesn't matter. This value + // will be overwritten on the client, before any animations + // occur. + return isRenderingOnServer ? true : !window.matchMedia(QUERY).matches; +}; +const usePrefersReducedMotion = () => { + const [prefersReducedMotion, setPrefersReducedMotion] = + useState(getInitialState); + useEffect(() => { + const mediaQueryList = window.matchMedia(QUERY); + const listener = (event: any) => { + setPrefersReducedMotion(!event.matches); + }; + if (mediaQueryList.addEventListener) { + mediaQueryList.addEventListener("change", listener); + } else { + mediaQueryList.addListener(listener); + } + return () => { + if (mediaQueryList.removeEventListener) { + mediaQueryList.removeEventListener("change", listener); + } else { + mediaQueryList.removeListener(listener); + } + }; + }, []); + return prefersReducedMotion; +}; + +export default usePrefersReducedMotion; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index f894a1807..f46dd2188 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -8,7 +8,7 @@ import { WagmiClient } from "../utils/wagmi"; import { siwe } from "../utils/siwe"; import { overrides } from "../styles/ConnectKit/overrides"; import NextHeadBase from "../components/SEO/NextHeadBase"; -import { inter, unbounded } from "../components/Fonts/Fonts"; +import { inter, unbounded } from "../components/Fonts"; import { Toaster } from "react-hot-toast"; const App = ({ Component, pageProps }: AppProps) => { @@ -50,19 +50,3 @@ const App = ({ Component, pageProps }: AppProps) => { }; export default App; - -// const disclaimerOptions = { -// disclaimer: ( -// <> -// By connecting your wallet you agree to the{" "} -// <a target="_blank" rel="noopener noreferrer" href="#!"> -// Terms of Service -// </a>{" "} -// and{" "} -// <a target="_blank" rel="noopener noreferrer" href="#!"> -// Privacy Policy -// </a> -// . -// </> -// ), -// }; diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index a633fdc3e..b6e8d410b 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -2,7 +2,10 @@ import { Html, Head, Main, NextScript } from "next/document"; const Document = () => { return ( - <Html lang="en" className="bg-slate-900 text-white"> + <Html + lang="en" + className="bg-slate-900 text-white scroll-smooth selection:bg-sky-500/20" + > <Head /> <body> <Main /> diff --git a/src/pages/api/checkUnlockable.ts b/src/pages/api/checkUnlockable.ts new file mode 100644 index 000000000..0cd918050 --- /dev/null +++ b/src/pages/api/checkUnlockable.ts @@ -0,0 +1,88 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { siwe } from "@/src/utils/siwe"; +import { getUserAddress, getAllUserNftIds } from "@/src/utils/loopring"; +import { findUnlockableByUuid } from "@/src/utils/generic"; +import { getPinataIndexLink } from "@/src/utils/pinata"; +import { withSessionRoute } from "@/src/utils/iron-session/withSession"; +import logger from "@/src/utils/logger"; + +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const query = req.query; + const { address, unlockableId } = query; + const siweSesh = await siwe.getSession(req, res); + + // Check validity of request + if ( + !address || + Array.isArray(address) || + !unlockableId || + Array.isArray(unlockableId) + ) { + const errorMsg = + "Invalid Request: 0x address not provided. Please provide a valid 0x address and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + // Check if there is a session. Only connected users may call this endpoint. + if (!siweSesh.address || siweSesh.address !== address) { + const errorMsg = + "You are not authorized to access this resource. Sign In With Ethereum, and try again."; + logger.error(errorMsg); + return res.status(401).send(errorMsg); + } + + // 1️⃣ Call the Loopring API to find the User's Loopring Account ID + const accountId = await getUserAddress(address); + + if (!accountId) { + const errorMsg = + "No Loopring Account could be found for the connected 0x address. Is your L2 account activated?"; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + const unlockable = findUnlockableByUuid(unlockableId); + if (!unlockable) { + const errorMsg = "Unable to find the Unlockable for the specified UUID."; + logger.error(errorMsg); + return res.status(404).send(errorMsg); + } + + // 2️⃣ Call the Loopring API to find the NFTs held by the user + const userNftIds = await getAllUserNftIds(accountId); + + if (!userNftIds) { + const errorMsg = "Unable to find any NFTs for the specified 0x address."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + // 3️⃣ Check if the user meets the unlock criteria + const intersection = unlockable.unlockCriteria.nftId.filter((value) => + userNftIds.includes(value) + ); + + if (intersection.length < unlockable.unlockCriteria.unlockAmount) { + const errorMsg = + "Your connected wallet does not meet the unlock requirements."; + logger.error(errorMsg); + return res.status(405).send(errorMsg); + } + + // 4️⃣ Get access link for the unlockable + const unlock = await getPinataIndexLink(unlockable.content.url); + + if (!unlock) { + const errorMsg = + "Unable to find the submarined content on Pinata. It may be deleted."; + logger.error(errorMsg); + return res.status(404).send(errorMsg); + } + + return res.status(200).json({ + unlock: unlock, + }); +}; + +export default withSessionRoute(handler); diff --git a/src/pages/api/getHoldersForNftId.ts b/src/pages/api/getHoldersForNftId.ts new file mode 100644 index 000000000..17a5b6f4d --- /dev/null +++ b/src/pages/api/getHoldersForNftId.ts @@ -0,0 +1,55 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { + getMinterAndToken, + getNftData, + getNftHolders, +} from "@/src/utils/loopring"; +import logger from "@/src/utils/logger"; + +// Request holders for a NFT held on Loopring +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const query = req.query; + const { nftId } = query; + + // Check if multiple or no Account IDs are specified. If so: early return. + if (!nftId || Array.isArray(nftId)) { + const errorMsg = "Unable to find data for the NFT ID you supplied."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + // Call TheGraph API to find NFT Datas for a NFT ID + const theGraphRes = await getMinterAndToken(nftId); + + if (!theGraphRes) { + const errorMsg = + "Unable to retrieve data from TheGraph with this NFT ID. Please provide a valid Loopring NFT ID, and try again."; + logger.error(errorMsg); + return res.status(404).send(errorMsg); + } + + const nftDataRes = await getNftData( + theGraphRes.minter, + theGraphRes.tokenAddress, + nftId + ); + + if (!nftDataRes) { + const errorMsg = + "Unable to retrieve data from the Loopring API with this NFT ID. Please provide a valid Loopring NFT ID, and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + const holders = await getNftHolders(nftDataRes.nftData); + + if (!holders) { + const errorMsg = "Unable to find holders for this NFT ID."; + logger.error(errorMsg); + return res.status(404).send(errorMsg); + } + + return res.status(200).json(holders); +}; + +export default handler; diff --git a/src/pages/api/getNftData.ts b/src/pages/api/getNftData.ts new file mode 100644 index 000000000..4319b7073 --- /dev/null +++ b/src/pages/api/getNftData.ts @@ -0,0 +1,44 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { getMinterAndToken, getNftData } from "@/src/utils/loopring"; +import logger from "@/src/utils/logger"; + +// Request NFTs Data for a Loopring NFT ID +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const query = req.query; + const { nftId } = query; + + // Check if multiple or no Account IDs are specified. If so: early return. + if (!nftId || Array.isArray(nftId)) { + const errorMsg = + "Invalid request. Please provide a valid Loopring NFT ID, and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + // Call TheGraph API to find NFT Datas for a NFT ID + const theGraphRes = await getMinterAndToken(nftId); + + if (!theGraphRes) { + const errorMsg = + "Unable to retrieve data from TheGraph with this NFT ID. Please provide a valid Loopring NFT ID, and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + const nftDataRes = await getNftData( + theGraphRes.minter, + theGraphRes.tokenAddress, + nftId + ); + + if (!nftDataRes) { + const errorMsg = + "Unable to retrieve data from the Loopring API with this NFT ID. Please provide a valid Loopring NFT ID, and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + return res.status(200).json({ nftData: nftDataRes.nftData }); +}; + +export default handler; diff --git a/src/pages/api/getNftHolders.ts b/src/pages/api/getNftHolders.ts new file mode 100644 index 000000000..650b0b809 --- /dev/null +++ b/src/pages/api/getNftHolders.ts @@ -0,0 +1,30 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import getNftHolders from "@/src/utils/loopring/getNftHolders"; +import logger from "@/src/utils/logger"; + +// Request NFTs on Loopring held by a user +const handler = async (req: NextApiRequest, res: NextApiResponse) => { + const query = req.query; + const { nftData } = query; + + // Check if multiple or no Account IDs are specified. If so: early return. + if (!nftData || Array.isArray(nftData)) { + const errorMsg = + "Invalid request. Please provide a valid Loopring 'NFT Data', and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); + } + + // Call TheGraph API to find NFT Data for a NFT ID + const holders = await getNftHolders(nftData); + + if (!holders) { + const errorMsg = `Unable to find holders for NFT Data '${nftData}'`; + logger.error(errorMsg); + return res.status(404).send(errorMsg); + } + + return res.status(200).json(holders); +}; + +export default handler; diff --git a/src/pages/api/getUserNfts.ts b/src/pages/api/getUserNfts.ts index 79c9a0342..acf169343 100644 --- a/src/pages/api/getUserNfts.ts +++ b/src/pages/api/getUserNfts.ts @@ -1,30 +1,30 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { getAllUserNftIds } from "../../utils/loopring"; +import logger from "@/src/utils/logger"; // Request NFTs on Loopring held by a user const handler = async (req: NextApiRequest, res: NextApiResponse) => { const query = req.query; const { accountId } = query; - if (!accountId || Array.isArray(accountId[0])) { - // Check if multiple or no Account IDs are specified. If so: early return. - return res - .status(400) - .send( - "Invalid Request: 0x address not provided. Please provide a valid 0x address and try again." - ); + // Check if multiple or no Account IDs are specified. If so: early return. + if (!accountId || Array.isArray(accountId)) { + const errorMsg = + "Invalid Request: 0x address not provided. Please provide a valid 0x address and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); } // Call Loopring API to find- and extract all user NFT IDs const allNftIds = await getAllUserNftIds(accountId); - return allNftIds - ? res.status(200).json(allNftIds) - : res - .status(400) - .send( - "Invalid Request: Unable to find any NFTs for the specified 0x address." - ); + if (!allNftIds) { + const errorMsg = `Unable to find any NFTs for Loopring accountId '${accountId}'`; + logger.error(errorMsg); + return res.status(404).send(errorMsg); + } + + return res.status(200).json(allNftIds); }; export default handler; diff --git a/src/pages/api/getUserUnlocks.ts b/src/pages/api/getUserUnlocks.ts index 6022f7ab0..23e6e7aa1 100644 --- a/src/pages/api/getUserUnlocks.ts +++ b/src/pages/api/getUserUnlocks.ts @@ -4,6 +4,7 @@ import { getAllUserNftIds, getUserAddress } from "../../utils/loopring"; import { getPinataIndexLink } from "../../utils/pinata"; import { withSessionRoute } from "@/src/utils/iron-session/withSession"; import { siwe } from "@/src/utils/siwe"; +import logger from "@/src/utils/logger"; // Summary of what happens: // 1️⃣ Call the Loopring API to find the User's Loopring Account ID @@ -15,41 +16,38 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const { address } = query; const siweSesh = await siwe.getSession(req, res); + // Check if multiple or no Account IDs are specified. If so: early return. if (!address || Array.isArray(address)) { - return res - .status(400) - .send( - "0x address not provided. Please provide a valid 0x address and try again." - ); + const errorMsg = + "0x address not provided. Please provide a valid 0x address and try again."; + logger.error(errorMsg); + return res.status(400).send(errorMsg); } // Check if there is a session. Only connected users may call this endpoint. if (!siweSesh.address || siweSesh.address !== address) { - return res - .status(401) - .send( - "You are not authorized to access this resource. Sign In With Ethereum, and try again." - ); + const errorMsg = + "You are not authorized to access this resource. Sign In With Ethereum, and try again."; + logger.error(errorMsg); + return res.status(401).send(errorMsg); } // 1️⃣ Call the Loopring API to find the User's Loopring Account ID const accountId = await getUserAddress(address); if (!accountId) { - return res - .status(400) - .send( - "No Loopring Account could be found for the connected 0x address. Is your L2 account activated?" - ); + const errorMsg = `No Loopring Account could be found for ${address}. Is your L2 account activated?`; + logger.error(errorMsg); + return res.status(400).send(errorMsg); } // 2️⃣ Call the Loopring API to find the NFTs held by the user const allNftIds = await getAllUserNftIds(accountId); if (!allNftIds) { - return res - .status(404) - .send("Unable to find any NFTs for the specified 0x address."); + const errorMsg = `Unable to find any NFTs for ${address}.`; + logger.error(errorMsg); + return res.status(404).send(errorMsg); } // 3️⃣ Check the user's NFT IDs against the config.ts to determine unlocks diff --git a/src/pages/api/helpers/generateUuid.ts b/src/pages/api/helpers/generateUuid.ts new file mode 100644 index 000000000..1deb4e001 --- /dev/null +++ b/src/pages/api/helpers/generateUuid.ts @@ -0,0 +1,10 @@ +import { v4 as uuidv4 } from "uuid"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const uuid = uuidv4(); + res.status(200).json({ uuid }); +} diff --git a/src/pages/api/helpers/sampleUnlockable.ts b/src/pages/api/helpers/sampleUnlockable.ts new file mode 100644 index 000000000..6a4d9f189 --- /dev/null +++ b/src/pages/api/helpers/sampleUnlockable.ts @@ -0,0 +1,37 @@ +import { v4 as uuidv4 } from "uuid"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { UnlockableV2 } from "@/src/config/types"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // This API Endpoint generates a sample UnlockableV2 object you can use for your `config.ts` file. + + const now = new Date(); + const dateObj = now.toISOString().replace("T", " ").split("Z")[0]; + + const sampleUnlockable = { + id: uuidv4(), + owner: "0x000000000000000000000000000000000000dEaD", + metadata: { + name: "The title of your Unlockable", + description: + "Write a short but intriguing description about your Unlockable. Ideally under 120 characters. Less is more.", + lastUpdated: dateObj, // Format: "2023-03-13 16:05:23.481327" + }, + content: { + type: "IPFS", + url: "YOUR_CID_HERE", + }, + unlockCriteria: { + unlockAmount: 2, + nftId: [ + "0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c", + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee", + ], + }, + }; + + res.status(200).json(sampleUnlockable); +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ae959846d..fd4a8b908 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,23 +1,52 @@ -import ConnectPrompt from "../components/ConnectPrompt"; -import ConnectedPage from "../components/ConnectedPage"; +import { GetServerSideProps } from "next"; +import { fetchAllUnlockables, findAllUnlockables } from "../utils/generic"; +import { UnlockableV2 } from "../config/types"; + import Header from "../components/Header"; import Footer from "../components/Footer"; -import { useSIWE } from "connectkit"; +import Hero from "../components/Pages/Home/Hero"; +import ContentBlocks from "../components/Pages/Home/ContentBlocks"; +import UseCases from "../components/Pages/Home/UseCases"; +import CTABanner from "../components/Pages/Home/CTABanner"; +import { techPattern } from "../styles/inline-styles"; + +export const getServerSideProps: GetServerSideProps = async (context) => { + context.res.setHeader( + "Cache-Control", + "public, s-maxage=3000, stale-while-revalidate=5000" + ); + + const unlockables = + process.env.LOOPGATE_MODE === "supabase" + ? await fetchAllUnlockables() // Use Supabase as the source for the Unlockables + : findAllUnlockables(); // Use the 'config.ts' file as the source for the Unlockables -const Page = () => { - const { signedIn } = useSIWE(); + if (!unlockables) { + return { + props: { unlockables: [] }, + }; + } + return { + props: { unlockables: unlockables }, + }; +}; + +const Page = ({ unlockables }: { unlockables: UnlockableV2[] | [] }) => { return ( <div - className="min-h-screen h-full flex flex-col bg-cover bg-center" - style={{ - backgroundImage: "url('images/hero-blur.jpg')", - }} + className="min-h-screen h-full flex flex-col bg-center" + style={techPattern} > <Header /> - - {!signedIn ? <ConnectPrompt /> : <ConnectedPage />} - + <Hero + title="Token-Gate Content on Loopring L2" + subtitle="Reward your community with exclusive content available only to few. LoopGate adds utility to NFTs by empowering creators to unlock special content for token holders." + ctaText="Learn more" + /> + <ContentBlocks unlockables={unlockables} /> + <UseCases /> + <CTABanner /> <Footer /> </div> ); diff --git a/src/pages/unlocks/[uuid].tsx b/src/pages/unlocks/[uuid].tsx new file mode 100644 index 000000000..077b4bcd5 --- /dev/null +++ b/src/pages/unlocks/[uuid].tsx @@ -0,0 +1,71 @@ +import { + fetchUnlockableByUuid, + findUnlockableByUuid, +} from "@/src/utils/generic"; +import Layout from "@/src/components/UnlockablePage/Layout"; +import FourOhFour from "@/src/components/UnlockablePage/404"; +import UnlockCard from "@/src/components/UnlockablePage/UnlockCard/UnlockCard"; +import { GetServerSideProps } from "next"; +import { UnlockableV2 } from "@/src/config/types"; +import { isUuid } from "@/src/utils/supabase/helpers"; +import NextHeadBase from "@/src/components/SEO/NextHeadBase"; + +export const getServerSideProps: GetServerSideProps = async (context) => { + context.res.setHeader( + "Cache-Control", + "public, s-maxage=3000, stale-while-revalidate=5000" + ); + + const { query } = context; + + if ( + !query.uuid || + Array.isArray(query.uuid) || + !isUuid(query.uuid as string) + ) { + return { + props: { unlockable: null }, + }; + } + + const unlockable = + process.env.LOOPGATE_MODE === "supabase" + ? await fetchUnlockableByUuid(query.uuid) // Use Supabase as the source for the Unlockables + : findUnlockableByUuid(query.uuid); // Use the 'config.ts' file as the source for the Unlockables + + if (!unlockable) { + return { + props: { unlockable: null }, + }; + } + + return { + props: { unlockable: unlockable }, + }; +}; + +const Page = ({ unlockable }: { unlockable: UnlockableV2 | undefined }) => { + if (!unlockable) { + return ( + <FourOhFour + label={"The unlockable you were looking for could not be found... 😭"} + /> + ); + } + + return ( + <Layout containerClass="flex-grow flex items-center justify-center"> + <NextHeadBase + title={`Unlock ${ + unlockable.metadata.name + ? "'" + unlockable.metadata.name + "'" + : "This token-gated content" + } on LoopGate`} + ogImgUrl="/images/unlocks-og-image.png" + /> + <UnlockCard unlockable={unlockable} /> + </Layout> + ); +}; + +export default Page; diff --git a/src/pages/unlocks/browse.tsx b/src/pages/unlocks/browse.tsx new file mode 100644 index 000000000..3d08b41c7 --- /dev/null +++ b/src/pages/unlocks/browse.tsx @@ -0,0 +1,46 @@ +import { findAllUnlockables, fetchAllUnlockables } from "@/src/utils/generic"; +import Layout from "@/src/components/UnlockablePage/Layout"; +import FourOhFour from "@/src/components/UnlockablePage/404"; +import { GetServerSideProps } from "next"; +import { UnlockableV2 } from "@/src/config/types"; +import List from "@/src/components/UnlockablePage/List"; + +export const getServerSideProps: GetServerSideProps = async (context) => { + context.res.setHeader( + "Cache-Control", + "public, s-maxage=3000, stale-while-revalidate=5000" + ); + + const unlockables = + process.env.LOOPGATE_MODE === "supabase" + ? await fetchAllUnlockables() // Use Supabase as the source for the Unlockables + : findAllUnlockables(); // Use the 'config.ts' file as the source for the Unlockables + + if (!unlockables) { + return { + props: { unlockables: [] }, + }; + } + + return { + props: { unlockables: unlockables }, + }; +}; + +const Page = ({ unlockables }: { unlockables: UnlockableV2[] | [] }) => { + if (unlockables.length === 0) { + return ( + <FourOhFour + label={"This LoopGate Instance does not have any Unlockables yet."} + /> + ); + } + + return ( + <Layout containerClass="flex-grow flex justify-center mt-4 md:mt-10 p-4 md:p-6"> + <List unlockables={unlockables} /> + </Layout> + ); +}; + +export default Page; diff --git a/src/styles/inline-styles.ts b/src/styles/inline-styles.ts new file mode 100644 index 000000000..79cbccbf0 --- /dev/null +++ b/src/styles/inline-styles.ts @@ -0,0 +1,4 @@ +export const techPattern = { + backgroundImage: `linear-gradient(0deg, #0f172a, transparent), + url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 304 304' width='304' height='304'%3E%3Cpath fill='rgba(56, 189, 248, 100)' fill-opacity='0.03' d='M44.1 224a5 5 0 1 1 0 2H0v-2h44.1zm160 48a5 5 0 1 1 0 2H82v-2h122.1zm57.8-46a5 5 0 1 1 0-2H304v2h-42.1zm0 16a5 5 0 1 1 0-2H304v2h-42.1zm6.2-114a5 5 0 1 1 0 2h-86.2a5 5 0 1 1 0-2h86.2zm-256-48a5 5 0 1 1 0 2H0v-2h12.1zm185.8 34a5 5 0 1 1 0-2h86.2a5 5 0 1 1 0 2h-86.2zM258 12.1a5 5 0 1 1-2 0V0h2v12.1zm-64 208a5 5 0 1 1-2 0v-54.2a5 5 0 1 1 2 0v54.2zm48-198.2V80h62v2h-64V21.9a5 5 0 1 1 2 0zm16 16V64h46v2h-48V37.9a5 5 0 1 1 2 0zm-128 96V208h16v12.1a5 5 0 1 1-2 0V210h-16v-76.1a5 5 0 1 1 2 0zm-5.9-21.9a5 5 0 1 1 0 2H114v48H85.9a5 5 0 1 1 0-2H112v-48h12.1zm-6.2 130a5 5 0 1 1 0-2H176v-74.1a5 5 0 1 1 2 0V242h-60.1zm-16-64a5 5 0 1 1 0-2H114v48h10.1a5 5 0 1 1 0 2H112v-48h-10.1zM66 284.1a5 5 0 1 1-2 0V274H50v30h-2v-32h18v12.1zM236.1 176a5 5 0 1 1 0 2H226v94h48v32h-2v-30h-48v-98h12.1zm25.8-30a5 5 0 1 1 0-2H274v44.1a5 5 0 1 1-2 0V146h-10.1zm-64 96a5 5 0 1 1 0-2H208v-80h16v-14h-42.1a5 5 0 1 1 0-2H226v18h-16v80h-12.1zm86.2-210a5 5 0 1 1 0 2H272V0h2v32h10.1zM98 101.9V146H53.9a5 5 0 1 1 0-2H96v-42.1a5 5 0 1 1 2 0zM53.9 34a5 5 0 1 1 0-2H80V0h2v34H53.9zm60.1 3.9V66H82v64H69.9a5 5 0 1 1 0-2H80V64h32V37.9a5 5 0 1 1 2 0zM101.9 82a5 5 0 1 1 0-2H128V37.9a5 5 0 1 1 2 0V82h-28.1zm16-64a5 5 0 1 1 0-2H146v44.1a5 5 0 1 1-2 0V18h-26.1zm102.2 270a5 5 0 1 1 0 2H98v14h-2v-16h124.1zM242 149.9V160h16v34h-16v62h48v48h-2v-46h-48v-66h16v-30h-16v-12.1a5 5 0 1 1 2 0zM53.9 18a5 5 0 1 1 0-2H64V2H48V0h18v18H53.9zm112 32a5 5 0 1 1 0-2H192V0h50v2h-48v48h-28.1zm-48-48a5 5 0 0 1-9.8-2h2.07a3 3 0 1 0 5.66 0H178v34h-18V21.9a5 5 0 1 1 2 0V32h14V2h-58.1zm0 96a5 5 0 1 1 0-2H137l32-32h39V21.9a5 5 0 1 1 2 0V66h-40.17l-32 32H117.9zm28.1 90.1a5 5 0 1 1-2 0v-76.51L175.59 80H224V21.9a5 5 0 1 1 2 0V82h-49.59L146 112.41v75.69zm16 32a5 5 0 1 1-2 0v-99.51L184.59 96H300.1a5 5 0 0 1 3.9-3.9v2.07a3 3 0 0 0 0 5.66v2.07a5 5 0 0 1-3.9-3.9H185.41L162 121.41v98.69zm-144-64a5 5 0 1 1-2 0v-3.51l48-48V48h32V0h2v50H66v55.41l-48 48v2.69zM50 53.9v43.51l-48 48V208h26.1a5 5 0 1 1 0 2H0v-65.41l48-48V53.9a5 5 0 1 1 2 0zm-16 16V89.41l-34 34v-2.82l32-32V69.9a5 5 0 1 1 2 0zM12.1 32a5 5 0 1 1 0 2H9.41L0 43.41V40.6L8.59 32h3.51zm265.8 18a5 5 0 1 1 0-2h18.69l7.41-7.41v2.82L297.41 50H277.9zm-16 160a5 5 0 1 1 0-2H288v-71.41l16-16v2.82l-14 14V210h-28.1zm-208 32a5 5 0 1 1 0-2H64v-22.59L40.59 194H21.9a5 5 0 1 1 0-2H41.41L66 216.59V242H53.9zm150.2 14a5 5 0 1 1 0 2H96v-56.6L56.6 162H37.9a5 5 0 1 1 0-2h19.5L98 200.6V256h106.1zm-150.2 2a5 5 0 1 1 0-2H80v-46.59L48.59 178H21.9a5 5 0 1 1 0-2H49.41L82 208.59V258H53.9zM34 39.8v1.61L9.41 66H0v-2h8.59L32 40.59V0h2v39.8zM2 300.1a5 5 0 0 1 3.9 3.9H3.83A3 3 0 0 0 0 302.17V256h18v48h-2v-46H2v42.1zM34 241v63h-2v-62H0v-2h34v1zM17 18H0v-2h16V0h2v18h-1zm273-2h14v2h-16V0h2v16zm-32 273v15h-2v-14h-14v14h-2v-16h18v1zM0 92.1A5.02 5.02 0 0 1 6 97a5 5 0 0 1-6 4.9v-2.07a3 3 0 1 0 0-5.66V92.1zM80 272h2v32h-2v-32zm37.9 32h-2.07a3 3 0 0 0-5.66 0h-2.07a5 5 0 0 1 9.8 0zM5.9 0A5.02 5.02 0 0 1 0 5.9V3.83A3 3 0 0 0 3.83 0H5.9zm294.2 0h2.07A3 3 0 0 0 304 3.83V5.9a5 5 0 0 1-3.9-5.9zm3.9 300.1v2.07a3 3 0 0 0-1.83 1.83h-2.07a5 5 0 0 1 3.9-3.9zM97 100a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-48 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 96a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-144a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-96 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm96 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-32 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM49 36a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-32 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM33 68a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 240a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm80-176a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm112 176a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM17 180a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM17 84a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'%3E%3C/path%3E%3C/svg%3E")`, +}; diff --git a/src/utils/generic/fetchAllUnlockables.ts b/src/utils/generic/fetchAllUnlockables.ts new file mode 100644 index 000000000..45dfdf4da --- /dev/null +++ b/src/utils/generic/fetchAllUnlockables.ts @@ -0,0 +1,22 @@ +import supabase from "../supabase"; +import { parseSqlUnlockable } from "../supabase/helpers"; + +const fetchALlUnlockables = async () => { + let { data: unlockables, error } = await supabase + .from("unlockables_with_criteria") + .select(`*`); + + if (error) { + throw error; + } + + if (unlockables) { + const parsedUnlockables = unlockables.map((item) => { + return parseSqlUnlockable(item); + }); + + return parsedUnlockables; + } +}; + +export default fetchALlUnlockables; diff --git a/src/utils/generic/fetchUnlockableByUuid.ts b/src/utils/generic/fetchUnlockableByUuid.ts new file mode 100644 index 000000000..bc66d0d8a --- /dev/null +++ b/src/utils/generic/fetchUnlockableByUuid.ts @@ -0,0 +1,31 @@ +import supabase from "../supabase"; +import { parseSqlUnlockable } from "../supabase/helpers"; + +// WIP: See https://supabase.com/docs/reference/javascript/typescript-support#type-hints +// type UnlockablesResponse = Awaited<ReturnType<typeof fetchUnlockableByUuid>>; +// export type UnlockablesResponseSuccess = UnlockablesResponse["data"]; +// export type UnlockablesResponseError = UnlockablesResponse["error"]; + +// TODO: Inserting data https://supabase.com/docs/reference/javascript/insert +// TODO: Updating data https://supabase.com/docs/reference/javascript/update +// OR: Upserting data https://supabase.com/docs/reference/javascript/upsert +// TODO: Deleting data https://supabase.com/docs/reference/javascript/delete +// RLS Policies: Only auth users can mutate + +const fetchUnlockableByUuid = async (uuid: string) => { + let { data: unlockables, error } = await supabase + .from("unlockables_with_criteria") + .select(`*`) + .eq("id", uuid) + .maybeSingle(); + + if (error) { + throw error; + } + + if (unlockables) { + return parseSqlUnlockable(unlockables); + } +}; + +export default fetchUnlockableByUuid; diff --git a/src/utils/generic/findAllUnlockables.ts b/src/utils/generic/findAllUnlockables.ts new file mode 100644 index 000000000..50a0e1a38 --- /dev/null +++ b/src/utils/generic/findAllUnlockables.ts @@ -0,0 +1,14 @@ +import { unlockablesV2 } from "../../config/config"; +import { UnlockableV2, ConfigError } from "../../config/types"; + +const findAllUnlockables = ( + unlockablesArray: UnlockableV2[] = unlockablesV2 +) => { + if (unlockablesArray.length === 0) { + throw new ConfigError("Empty config file"); + } + + return unlockablesArray; +}; + +export default findAllUnlockables; diff --git a/src/utils/generic/findUnlockableByUuid.test.ts b/src/utils/generic/findUnlockableByUuid.test.ts new file mode 100644 index 000000000..c2024433e --- /dev/null +++ b/src/utils/generic/findUnlockableByUuid.test.ts @@ -0,0 +1,22 @@ +import { findUnlockableByUuid } from "./"; +import { ConfigError } from "../../config/types"; + +describe("find unlockables based on UUID", () => { + it("should return one result if the user meets the criteria for it", () => { + const result = findUnlockableByUuid("ee3fd6ff-4718-4949-b621-f35ccad89ee4"); + expect(result.metadata.name).toEqual( + "Token Gating with NFTs: Unlocking New Ways to Bring Value" + ); + }); + + it("should return an empty array if the UUID is not found", () => { + const result = findUnlockableByUuid("bruh-this-is-no-uuid"); + expect(result).toBeUndefined(); + }); + + it("should throw an error if the config file is empty", () => { + expect(() => + findUnlockableByUuid("ee3fd6ff-4718-4949-b621-f35ccad89ee4", []) + ).toThrowError(ConfigError); + }); +}); diff --git a/src/utils/generic/findUnlockableByUuid.ts b/src/utils/generic/findUnlockableByUuid.ts new file mode 100644 index 000000000..00236b1b0 --- /dev/null +++ b/src/utils/generic/findUnlockableByUuid.ts @@ -0,0 +1,15 @@ +import { unlockablesV2 } from "../../config/config"; +import { UnlockableV2, ConfigError } from "../../config/types"; + +const findUnlockableByUuid = ( + uuid: string, + unlockablesArray: UnlockableV2[] = unlockablesV2 +) => { + if (unlockablesArray.length === 0) { + throw new ConfigError("Empty config file"); + } + + return unlockablesArray.filter((item) => item.id === uuid)[0]; +}; + +export default findUnlockableByUuid; diff --git a/src/utils/generic/formatRelativeDate.ts b/src/utils/generic/formatRelativeDate.ts new file mode 100644 index 000000000..cd442c26a --- /dev/null +++ b/src/utils/generic/formatRelativeDate.ts @@ -0,0 +1,8 @@ +import { formatDistance } from "date-fns"; + +const formatRelativeDate = (date: Date | string) => { + const ts = new Date(date); + return formatDistance(ts, new Date(), { addSuffix: true }); +}; + +export default formatRelativeDate; diff --git a/src/utils/generic/index.ts b/src/utils/generic/index.ts index f0bd2cb11..e9f28b926 100644 --- a/src/utils/generic/index.ts +++ b/src/utils/generic/index.ts @@ -1,6 +1,25 @@ import checkIfContainsAll from "./checkIfContainsAll"; import findUnlockedCids from "./findUnlockedCids"; import getCurrentYear from "./getCurrentYear"; +import findUnlockableByUuid from "./findUnlockableByUuid"; +import findAllUnlockables from "./findAllUnlockables"; +import fetchUnlockableByUuid from "./fetchUnlockableByUuid"; +import fetchAllUnlockables from "./fetchAllUnlockables"; +import uuidToNumber from "./uuidToNumber"; +import truncate0x from "./truncate0x"; +import formatRelativeDate from "./formatRelativeDate"; import cn from "./cn"; -export { checkIfContainsAll, findUnlockedCids, getCurrentYear, cn }; +export { + checkIfContainsAll, + findUnlockedCids, + findUnlockableByUuid, + findAllUnlockables, + fetchUnlockableByUuid, + fetchAllUnlockables, + uuidToNumber, + getCurrentYear, + truncate0x, + formatRelativeDate, + cn, +}; diff --git a/src/utils/generic/truncate0x.test.ts b/src/utils/generic/truncate0x.test.ts new file mode 100644 index 000000000..789541584 --- /dev/null +++ b/src/utils/generic/truncate0x.test.ts @@ -0,0 +1,17 @@ +import { truncate0x } from "./"; + +describe("make a eth 0x address more readable by truncating it", () => { + it("should truncate a valid eth address", () => { + expect(truncate0x("0x1337cc354aeaf15b0e98a609cd348df171174e14")).toEqual( + "0x1337…4e14" + ); + }); + + it("should return the input string if it does not start with 0x", () => { + expect(truncate0x("some random content")).toEqual("some random content"); + }); + + it("should return the input string if it is not a valid 0x address", () => { + expect(truncate0x("0xbruh")).toEqual("0xbruh"); + }); +}); diff --git a/src/utils/generic/truncate0x.ts b/src/utils/generic/truncate0x.ts new file mode 100644 index 000000000..3107ed282 --- /dev/null +++ b/src/utils/generic/truncate0x.ts @@ -0,0 +1,11 @@ +const truncate0x = (address: string) => { + const len = address.length; + + if (!address.startsWith("0x") || len != 42) { + return address; + } + + return `${address.slice(0, 6)}…${address.slice(len - 4, len)}`; +}; + +export default truncate0x; diff --git a/src/utils/generic/uuidToNumber.test.ts b/src/utils/generic/uuidToNumber.test.ts new file mode 100644 index 000000000..4d1fc76c6 --- /dev/null +++ b/src/utils/generic/uuidToNumber.test.ts @@ -0,0 +1,18 @@ +import uuidToNumber from "./uuidToNumber"; + +describe("uuidToNumber", () => { + it("should generate a number between 0 and 133742069", () => { + const uuid = "123e4567-e89b-12d3-a456-426655440000"; + const randomNumber = uuidToNumber(uuid); + expect(randomNumber).toBeGreaterThanOrEqual(0); + expect(randomNumber).toBeLessThanOrEqual(133742069); + }); + + it("should generate different numbers for different UUIDs", () => { + const uuid1 = "123e4567-e89b-12d3-a456-426655440000"; + const uuid2 = "123e4567-e89b-12d3-a456-426655440001"; + const randomNumber1 = uuidToNumber(uuid1); + const randomNumber2 = uuidToNumber(uuid2); + expect(randomNumber1).not.toBe(randomNumber2); + }); +}); diff --git a/src/utils/generic/uuidToNumber.ts b/src/utils/generic/uuidToNumber.ts new file mode 100644 index 000000000..c359743e4 --- /dev/null +++ b/src/utils/generic/uuidToNumber.ts @@ -0,0 +1,12 @@ +import { createHash } from "crypto"; + +const uuidToNumber = (uuid: string) => { + const hash = createHash("sha256").update(uuid).digest("hex"); + const hexDigits = hash.slice(0, 4); + const decimalValue = parseInt(hexDigits, 16); + const percentage = decimalValue / 0xffff; + const randomNumber = Math.floor(percentage * 133742069); + return randomNumber; +}; + +export default uuidToNumber; diff --git a/src/utils/logger/index.ts b/src/utils/logger/index.ts new file mode 100644 index 000000000..43759dd01 --- /dev/null +++ b/src/utils/logger/index.ts @@ -0,0 +1,3 @@ +import logger from "./logger"; + +export default logger; diff --git a/src/utils/logger/logger.ts b/src/utils/logger/logger.ts new file mode 100644 index 000000000..b09fa6930 --- /dev/null +++ b/src/utils/logger/logger.ts @@ -0,0 +1,35 @@ +import pino from "pino"; +import { createPinoBrowserSend, createWriteStream } from "pino-logflare"; + +const pinoCredentials = { + apiKey: process.env.NEXT_PUBLIC_LOGFLARE_API_KEY, + sourceToken: process.env.NEXT_PUBLIC_LOGFLARE_SOURCE_TOKEN, +}; + +const checkCredentials = (credentials: string[]): boolean => + credentials.every((x) => typeof x !== "undefined"); + +const credentialsDefined = checkCredentials([ + pinoCredentials.apiKey, + pinoCredentials.sourceToken, +]); + +// Include a mock logger in case no LogFlare credentials are present +const mockLogger = { + error: () => {}, +}; + +const logger = credentialsDefined + ? pino( + { + browser: { + transmit: { + send: createPinoBrowserSend(pinoCredentials), + }, + }, + }, + createWriteStream(pinoCredentials) + ) + : mockLogger; + +export default logger; diff --git a/src/utils/loopring/_constants.ts b/src/utils/loopring/_constants.ts index a028cd47d..be0d039f2 100644 --- a/src/utils/loopring/_constants.ts +++ b/src/utils/loopring/_constants.ts @@ -1,8 +1,14 @@ -const BASE_URL = "https://api3.loopring.io/api/v3"; +const LOOP_BASE_URL = "https://api3.loopring.io/api/v3"; -export const API = { - USER_ACCOUNT: `${BASE_URL}/account`, - USER_NFT_BALANCE: `${BASE_URL}/user/nft/balances`, +export const LOOP_API = { + USER_ACCOUNT: `${LOOP_BASE_URL}/account`, + USER_NFT_BALANCE: `${LOOP_BASE_URL}/user/nft/balances`, + NFT_DATA: `${LOOP_BASE_URL}/nft/info/nftData`, + NFT_HOLDERS: `${LOOP_BASE_URL}/nft/info/nftHolders`, +}; + +export const THE_GRAPH = { + GATEWAY_URL: "https://api.thegraph.com/subgraphs/name/loopring/loopring", }; export const CONTRACTS = { diff --git a/src/utils/loopring/getAllUserNftIds.ts b/src/utils/loopring/getAllUserNftIds.ts index e02aad5df..8e28c42fc 100644 --- a/src/utils/loopring/getAllUserNftIds.ts +++ b/src/utils/loopring/getAllUserNftIds.ts @@ -1,6 +1,6 @@ import axios from "axios"; import rateLimit from "axios-rate-limit"; -import { API } from "./_constants"; +import { LOOP_API } from "./_constants"; import { headerOpts, extractNfts } from "./index"; // Loopring API accepts 5 requests per second, max. @@ -14,7 +14,7 @@ const getAllUserNftIds = async (accountId: string | string[]) => { // Gets all Loopring L2 NFTs for a specified Account ID const LIMIT = 50; // API can handle up to 50 per call const firstReq = await axios.get( - `${API.USER_NFT_BALANCE}?accountId=${accountId}&limit=${LIMIT}`, + `${LOOP_API.USER_NFT_BALANCE}?accountId=${accountId}&limit=${LIMIT}`, headerOpts ); @@ -41,9 +41,9 @@ const getAllUserNftIds = async (accountId: string | string[]) => { const followUpReqs = await Promise.all( amountOfCalls.map(async (index) => { return await rateLimitedAxios.get( - `${API.USER_NFT_BALANCE}?accountId=${accountId}&limit=${LIMIT}&offset=${ - LIMIT * index - }`, + `${ + LOOP_API.USER_NFT_BALANCE + }?accountId=${accountId}&limit=${LIMIT}&offset=${LIMIT * index}`, headerOpts ); }) diff --git a/src/utils/loopring/getMinterAndToken.test.ts b/src/utils/loopring/getMinterAndToken.test.ts new file mode 100644 index 000000000..1acc098e4 --- /dev/null +++ b/src/utils/loopring/getMinterAndToken.test.ts @@ -0,0 +1,28 @@ +import getMinterAndToken from "./getMinterAndToken"; + +import axios from "axios"; +jest.mock("axios"); +const mockedAxios = axios as jest.Mocked<typeof axios>; + +describe("querying Loopring TheGraph to get minter and token address information", () => { + it.todo("returns a minter and token address for a valid NFT ID"); + // it("returns a minter and token address for a valid NFT ID", async () => { + // mockedAxios.get.mockResolvedValue({ + // minter: "0x94743548ba8d82a4ee8ea3dfad589ea501ad2738", + // tokenAddress: "0xc76eca2937b006606ebe717621409e4c2df906f1", + // }); + // const data = await getMinterAndToken( + // "0x271d3a38c3572ab21225fbb7f97468051ca9c631f002bf2dde82aee9b8511ac0" + // ); + // expect(data).toEqual({ + // minter: "0x94743548ba8d82a4ee8ea3dfad589ea501ad2738", + // tokenAddress: "0xc76eca2937b006606ebe717621409e4c2df906f1", + // }); + // }); + + it("returns false for an invalid NFT ID", async () => { + mockedAxios.get.mockRejectedValue(false); + const data = await getMinterAndToken("0xbruh"); + expect(data).toEqual(false); + }); +}); diff --git a/src/utils/loopring/getMinterAndToken.ts b/src/utils/loopring/getMinterAndToken.ts new file mode 100644 index 000000000..30291b8e1 --- /dev/null +++ b/src/utils/loopring/getMinterAndToken.ts @@ -0,0 +1,37 @@ +import axios from "axios"; +import { THE_GRAPH } from "./_constants"; +import logger from "@/src/utils/logger"; + +// Queries Loopring TheGraph to find Minter and Token Address from NFT ID +// Example input: `0x271d3a38c3572ab21225fbb7f97468051ca9c631f002bf2dde82aee9b8511ac0` +// Example output: `minter: "0x94743548ba8d82a4ee8ea3dfad589ea501ad2738", tokenAddress: "0xc76eca2937b006606ebe717621409e4c2df906f1"` +const getMinterAndToken = async (nftId: string) => { + const query = `{ + nonFungibleTokens( + where: {nftID: "${nftId}"} + ) { + minter { + address + }, + token + } + }`; + + try { + const response = await axios.post(THE_GRAPH.GATEWAY_URL, { query }); + + if (response.data.data.nonFungibleTokens.length == 0) { + return false; + } + + return { + minter: response.data.data.nonFungibleTokens[0].minter.address, + tokenAddress: response.data.data.nonFungibleTokens[0].token, + }; + } catch (error) { + logger.error(error); + return false; + } +}; + +export default getMinterAndToken; diff --git a/src/utils/loopring/getNftData.ts b/src/utils/loopring/getNftData.ts new file mode 100644 index 000000000..2939da88a --- /dev/null +++ b/src/utils/loopring/getNftData.ts @@ -0,0 +1,27 @@ +import { LOOP_API } from "./_constants"; +import axios from "axios"; +import headerOpts from "./headerOpts"; +import logger from "@/src/utils/logger"; + +const getNftData = async ( + minter: string, + tokenAddress: string, + nftId: string +) => { + // Queries the Loopring API to find the NFT Datas + // Example input: `minter: "0x94743548ba8d82a4ee8ea3dfad589ea501ad2738", tokenAddress: "0xc76eca2937b006606ebe717621409e4c2df906f1", nftId: 0x271d3a38c3572ab21225fbb7f97468051ca9c631f002bf2dde82aee9b8511ac0` + // Example output: `...` + + try { + const response = await axios.get( + `${LOOP_API.NFT_DATA}?minter=${minter}&tokenAddress=${tokenAddress}&nftId=${nftId}`, + headerOpts + ); + return response.data; + } catch (error) { + logger.error(error); + return false; + } +}; + +export default getNftData; diff --git a/src/utils/loopring/getNftHolders.test.ts b/src/utils/loopring/getNftHolders.test.ts new file mode 100644 index 000000000..4dd178a05 --- /dev/null +++ b/src/utils/loopring/getNftHolders.test.ts @@ -0,0 +1,13 @@ +import getNftHolders from "./getNftHolders"; +import axios from "axios"; + +jest.mock("axios"); +const mockedAxios = axios as jest.Mocked<typeof axios>; + +describe("querying the Loopring API to get NFT Holders for a NFT with nftDatas", () => { + it.todo("returns a single NFT holder for a NFT with correct nftDatas"); + + it.todo("returns multiple NFT holders for a NFT with correct nftDatas"); + + it.todo("returns false for invalid nftData"); +}); diff --git a/src/utils/loopring/getNftHolders.ts b/src/utils/loopring/getNftHolders.ts new file mode 100644 index 000000000..720adca6d --- /dev/null +++ b/src/utils/loopring/getNftHolders.ts @@ -0,0 +1,56 @@ +import { LOOP_API } from "./_constants"; +import axios from "axios"; +import { headerOpts, rateLimitedAxios } from "./index"; +import logger from "@/src/utils/logger"; + +const getNftHolders = async (nftData: string) => { + const LIMIT = 500; // API can handle up to 500 per call + + try { + const firstReq = await axios.get( + `${LOOP_API.NFT_HOLDERS}?nftData=${nftData}&limit=${LIMIT}`, + headerOpts + ); + + // Keep track of the total amount of holders + const amountOfHolders = firstReq.data.totalNum; + + // Determine if we need to do follow-up API calls + if (amountOfHolders <= LIMIT) { + // No need to. Parse the nft IDs from the response and early return + return firstReq.data.nftHolders; + } + + // Determine the amount of calls to be done, generate an array for follow-up requests + // Create and return an array of the amount of calls. I.e. => [1, 2, ..., n] + const amountOfCalls = Array.from( + Array(Math.ceil(amountOfHolders / LIMIT) - 1), + (_, index) => index + 1 + ); + + // Call the API for all of these ^ + const followUpReqs = await Promise.all( + amountOfCalls.map(async (index) => { + return await rateLimitedAxios.get( + `${LOOP_API.NFT_HOLDERS}?nftData=${nftData}&limit=${LIMIT}&offset=${ + LIMIT * index + }`, + headerOpts + ); + }) + ); + + // Parse the API response, merge with the first API Call and flatten the array + const nftApiResFlattened = followUpReqs + .map((item) => item.data) + .concat(firstReq.data.nftHolders) + .flat(); + + return nftApiResFlattened; + } catch (error) { + logger.error(error); + return false; + } +}; + +export default getNftHolders; diff --git a/src/utils/loopring/getUserAddress.ts b/src/utils/loopring/getUserAddress.ts index af4546015..946419f17 100644 --- a/src/utils/loopring/getUserAddress.ts +++ b/src/utils/loopring/getUserAddress.ts @@ -1,11 +1,15 @@ -import { API } from "./_constants"; +import { LOOP_API } from "./_constants"; import axios from "axios"; +import logger from "@/src/utils/logger"; const getUserAddress = async (address: string | string[]) => { try { - const response = await axios.get(`${API.USER_ACCOUNT}?owner=${address}`); + const response = await axios.get( + `${LOOP_API.USER_ACCOUNT}?owner=${address}` + ); return response.data.accountId; } catch (error) { + logger.error(error); return false; } }; diff --git a/src/utils/loopring/getUserNfts.ts b/src/utils/loopring/getUserNfts.ts index 2e3dd8729..cc0e1dfc2 100644 --- a/src/utils/loopring/getUserNfts.ts +++ b/src/utils/loopring/getUserNfts.ts @@ -1,16 +1,17 @@ import headerOpts from "./headerOpts"; -import { API } from "./_constants"; +import { LOOP_API } from "./_constants"; import axios from "axios"; +import logger from "@/src/utils/logger"; const getUserNfts = async (accountId: string) => { try { const response = await axios.get( - `${API.USER_NFT_BALANCE}?accountId=${accountId}&limit=50`, + `${LOOP_API.USER_NFT_BALANCE}?accountId=${accountId}&limit=50`, headerOpts ); return response.data; } catch (error) { - console.log(error); + logger.error(error); return false; } }; diff --git a/src/utils/loopring/headerOpts.ts b/src/utils/loopring/headerOpts.ts index 00739a194..8fcf43c24 100644 --- a/src/utils/loopring/headerOpts.ts +++ b/src/utils/loopring/headerOpts.ts @@ -1,7 +1,7 @@ // Fetch options used whenever the Loopring API Key is required const headerOpts = { headers: { - "X-API-KEY": `${process.env.LOOPRING_API_KEY}`, + "X-API-KEY": process.env.LOOPRING_API_KEY || "Undefined Loopring API Key", }, }; diff --git a/src/utils/loopring/index.ts b/src/utils/loopring/index.ts index d0c1725c0..57840abbc 100644 --- a/src/utils/loopring/index.ts +++ b/src/utils/loopring/index.ts @@ -1,16 +1,24 @@ -// API Call Implementations +// API Implementations import getUserAddress from "./getUserAddress"; import getUserNfts from "./getUserNfts"; +import getNftData from "./getNftData"; +import getNftHolders from "./getNftHolders"; import getAllUserNftIds from "./getAllUserNftIds"; +import getMinterAndToken from "./getMinterAndToken"; // Utils / general import extractNfts from "./extractNfts"; import headerOpts from "./headerOpts"; +import rateLimitedAxios from "./rateLimitedAxios"; export { extractNfts, getUserAddress, getUserNfts, + getNftData, + getMinterAndToken, getAllUserNftIds, + getNftHolders, headerOpts, + rateLimitedAxios, }; diff --git a/src/utils/loopring/rateLimitedAxios.ts b/src/utils/loopring/rateLimitedAxios.ts new file mode 100644 index 000000000..62deaa74d --- /dev/null +++ b/src/utils/loopring/rateLimitedAxios.ts @@ -0,0 +1,9 @@ +import axios from "axios"; +import rateLimit from "axios-rate-limit"; + +const rateLimitedAxios = rateLimit(axios.create(), { + maxRequests: 10, + perMilliseconds: 1000, +}); + +export default rateLimitedAxios; diff --git a/src/utils/pinata/_routes.ts b/src/utils/pinata/_routes.ts deleted file mode 100644 index c02c1d94c..000000000 --- a/src/utils/pinata/_routes.ts +++ /dev/null @@ -1 +0,0 @@ -export const SINGLE_FILE = "GATEWAY_URL/ipfs/FILE_CID?accessToken=TOKEN"; diff --git a/src/utils/pinata/generateAccessLink.ts b/src/utils/pinata/generateAccessLink.ts index c3394db0b..4db6d7c1e 100644 --- a/src/utils/pinata/generateAccessLink.ts +++ b/src/utils/pinata/generateAccessLink.ts @@ -3,7 +3,7 @@ import sub from "./sub"; // Checks a submarined CID. If valid, generates an access link that is accessible for a set amount of time const generateAccessLink = async ( cid: string, - unlockTimeInSec: number = 3600 + unlockTimeInSec: number = 60 * 3 ) => { const foundContent = await sub.getSubmarinedContentByCid(cid); const folder = foundContent.items[0]; diff --git a/src/utils/pinata/sub.ts b/src/utils/pinata/sub.ts index 0df6ff84b..3417a202e 100644 --- a/src/utils/pinata/sub.ts +++ b/src/utils/pinata/sub.ts @@ -1,9 +1,8 @@ import { Submarine } from "pinata-submarine"; -const PINATA_GATEWAY_URL = process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL; const sub = new Submarine( - process.env.PINATA_SUBMARINE_KEY as String, - PINATA_GATEWAY_URL + process.env.PINATA_SUBMARINE_KEY || "Undefined Pinata Submarine Key", + process.env.NEXT_PUBLIC_PINATA_GATEWAY_URL || "Undefined Pinata Gateway Url" ); export default sub; diff --git a/src/utils/siwe/siwe.ts b/src/utils/siwe/siwe.ts index 471fc173d..a0e8a266b 100644 --- a/src/utils/siwe/siwe.ts +++ b/src/utils/siwe/siwe.ts @@ -9,6 +9,6 @@ export const siwe = configureSIWE({ apiRoutePrefix: "/api/siwe", session: { cookieName: "LG_SIWE", - password: `${process.env.SESSION_SECRET}`, + password: process.env.SESSION_SECRET || "Undefined Session Secret", }, }); diff --git a/src/utils/supabase/helpers/index.ts b/src/utils/supabase/helpers/index.ts new file mode 100644 index 000000000..e34a7b18d --- /dev/null +++ b/src/utils/supabase/helpers/index.ts @@ -0,0 +1,5 @@ +import parseNftIdString from "./parseNftIdString"; +import parseSqlUnlockable from "./parseSqlUnlockable"; +import isUuid from "./isUuid"; + +export { parseNftIdString, parseSqlUnlockable, isUuid }; diff --git a/src/utils/supabase/helpers/isUuid.test.ts b/src/utils/supabase/helpers/isUuid.test.ts new file mode 100644 index 000000000..c2eaf709f --- /dev/null +++ b/src/utils/supabase/helpers/isUuid.test.ts @@ -0,0 +1,15 @@ +import isUuid from "./isUuid"; + +describe("isUuid", () => { + it("returns true for valid UUIDs", () => { + expect(isUuid("f5b6e5d6-8a32-4ebf-991c-0e19d018f7b1")).toBe(true); + expect(isUuid("DF11D6C5-CB8B-44A5-87AE-6407A90902C8")).toBe(true); + expect(isUuid("f5b6e5d68a324ebf991c0e19d018f7b1")).toBe(false); // Without dashes + }); + + it("returns false for invalid UUIDs", () => { + expect(isUuid("not-a-uuid")).toBe(false); + expect(isUuid("f5b6e5d6-8a32-4ebf-991c")).toBe(false); // Too short + expect(isUuid("f5b6e5d6-8a32-4ebf-991c-0e19d018f7b1-extra")).toBe(false); // Too long + }); +}); diff --git a/src/utils/supabase/helpers/isUuid.ts b/src/utils/supabase/helpers/isUuid.ts new file mode 100644 index 000000000..4d69b712e --- /dev/null +++ b/src/utils/supabase/helpers/isUuid.ts @@ -0,0 +1,8 @@ +const uuidRegex = + /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/; + +const isUuid = (potentialUuid: string): boolean => { + return uuidRegex.test(potentialUuid as string); +}; + +export default isUuid; diff --git a/src/utils/supabase/helpers/parseNftIdString.test.ts b/src/utils/supabase/helpers/parseNftIdString.test.ts new file mode 100644 index 000000000..20b63545e --- /dev/null +++ b/src/utils/supabase/helpers/parseNftIdString.test.ts @@ -0,0 +1,33 @@ +import parseNftIdString from "./parseNftIdString"; + +describe("parseNftIdString", () => { + it("should parse a single NFT ID", () => { + const input = + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee"; + const expectedOutput = [input]; + const actualOutput = parseNftIdString(input); + expect(actualOutput).toEqual(expectedOutput); + }); + + it("should parse multiple NFT IDs separated by commas", () => { + const input = + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee, 0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c"; + const expectedOutput = [ + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee", + "0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c", + ]; + const actualOutput = parseNftIdString(input); + expect(actualOutput).toEqual(expectedOutput); + }); + + it("should remove spaces between NFT IDs before parsing", () => { + const input = + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee , 0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c"; + const expectedOutput = [ + "0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee", + "0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c", + ]; + const actualOutput = parseNftIdString(input); + expect(actualOutput).toEqual(expectedOutput); + }); +}); diff --git a/src/utils/supabase/helpers/parseNftIdString.ts b/src/utils/supabase/helpers/parseNftIdString.ts new file mode 100644 index 000000000..079462638 --- /dev/null +++ b/src/utils/supabase/helpers/parseNftIdString.ts @@ -0,0 +1,15 @@ +/** + * Parses NFT IDs text array from SQL DB + * Input: "0x00...00, 1x11...11" + * Output: ["0x00...00", 1x11.11] + */ + +const parseNftIdString = (nftIdString?: string): string[] => { + if (!nftIdString) { + return []; + } + + return nftIdString.replaceAll(" ", "").split(","); +}; + +export default parseNftIdString; diff --git a/src/utils/supabase/helpers/parseSqlUnlockable.ts b/src/utils/supabase/helpers/parseSqlUnlockable.ts new file mode 100644 index 000000000..96441c0f7 --- /dev/null +++ b/src/utils/supabase/helpers/parseSqlUnlockable.ts @@ -0,0 +1,28 @@ +import { UnlockableV2 } from "@/src/config/types"; +import parseNftIdString from "./parseNftIdString"; + +const parseSqlUnlockable = (supabaseUnlockable: any): UnlockableV2 => { + const nftIds = parseNftIdString(supabaseUnlockable.nft_ids); + + const unlockable = { + id: supabaseUnlockable.id, + owner: supabaseUnlockable.owner, + metadata: { + name: supabaseUnlockable?.name, + description: supabaseUnlockable?.description, + lastUpdated: supabaseUnlockable.updated_at, + }, + content: { + type: "IPFS" as "IPFS", + url: supabaseUnlockable.content_url, + }, + unlockCriteria: { + unlockAmount: supabaseUnlockable.criteria_unlock_amount, + nftId: nftIds, + }, + }; + + return unlockable; +}; + +export default parseSqlUnlockable; diff --git a/src/utils/supabase/index.ts b/src/utils/supabase/index.ts new file mode 100644 index 000000000..4e83902a0 --- /dev/null +++ b/src/utils/supabase/index.ts @@ -0,0 +1,5 @@ +import supabase from "./supabase"; +import * as helpers from "./helpers"; + +export { helpers }; +export default supabase; diff --git a/src/utils/supabase/sql/content_types.sql b/src/utils/supabase/sql/content_types.sql new file mode 100644 index 000000000..c70285841 --- /dev/null +++ b/src/utils/supabase/sql/content_types.sql @@ -0,0 +1,10 @@ +-- Create and fill a table with the available content types + +CREATE TABLE content_types ( + content_id SERIAL, + content_name VARCHAR(20) NOT NULL, + -- + PRIMARY KEY (content_id) +); + +INSERT INTO content_types (content_name) VALUES ('IPFS'); \ No newline at end of file diff --git a/src/utils/supabase/sql/roles.sql b/src/utils/supabase/sql/roles.sql new file mode 100644 index 000000000..252093f8c --- /dev/null +++ b/src/utils/supabase/sql/roles.sql @@ -0,0 +1,11 @@ +-- Create and fill a table with the available user roles + +CREATE TABLE roles ( + role_id SERIAL, + role_name VARCHAR(20) NOT NULL, + -- + PRIMARY KEY (role_id) +); + +INSERT INTO roles (role_name) VALUES ('admin'); +INSERT INTO roles (role_name) VALUES ('user'); \ No newline at end of file diff --git a/src/utils/supabase/sql/unlock_criteria.sql b/src/utils/supabase/sql/unlock_criteria.sql new file mode 100644 index 000000000..cb61ac261 --- /dev/null +++ b/src/utils/supabase/sql/unlock_criteria.sql @@ -0,0 +1,25 @@ +-- Create and fill a table with unlock criteria + +CREATE TABLE unlock_criteria ( + id SERIAL, + unlockable_id uuid NOT NULL, + nft_id VARCHAR(66) NOT NULL, + updated_at timestamp default current_timestamp, + -- + PRIMARY KEY (id), + FOREIGN KEY (unlockable_id) REFERENCES unlockables(id), +); + +-- Criteria #1: Own '0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee' (GM #1) to unlock. +INSERT INTO unlock_criteria (unlockable_id, nft_id) +VALUES ('ee3fd6ff-4718-4949-b621-f35ccad89ee4', '0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee'); + +-- Criteria #2: Own '0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c' (GM #2) to unlock. +INSERT INTO unlock_criteria (unlockable_id, nft_id) +VALUES ('c9e21ebf-59cc-42dd-9dc4-fd427be153b9', '0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c'); + +-- Criteria #3: Own '0x3d483...27d9ee' & '0x8aa9d...37e78c' (GM #2) to unlock. +INSERT INTO unlock_criteria (unlockable_id, nft_id) +VALUES ('3eade688-8839-4fd7-b97a-f7c5f5bfc6ad', '0x3d483f631a391a3706446613929d253cfddcb47900a07593c5004c5e3827d9ee'); +INSERT INTO unlock_criteria (unlockable_id, nft_id) +VALUES ('3eade688-8839-4fd7-b97a-f7c5f5bfc6ad', '0x8aa9d39f44b4b8488d0bbf04ea12bec99ddbe676a1b9a38d853701327437e78c'); \ No newline at end of file diff --git a/src/utils/supabase/sql/unlockables.sql b/src/utils/supabase/sql/unlockables.sql new file mode 100644 index 000000000..75f0c9632 --- /dev/null +++ b/src/utils/supabase/sql/unlockables.sql @@ -0,0 +1,33 @@ +-- Create and fill a table with unlockables + +-- @DEV Only create this extension if it hasn't already been created. +-- create extension 'uuid-ossp' with schema extensions; + +CREATE TABLE unlockables ( + id uuid default uuid_generate_v4(), + name text, + description text, + owner VARCHAR(42) NOT NULL, + content_type_id int NOT NULL, + content_url VARCHAR(200) NOT NULL, + criteria_unlock_amount int NOT NULL, + updated_at timestamp default current_timestamp, + -- + PRIMARY KEY (id), + FOREIGN KEY (owner) REFERENCES users(eth_address), + FOREIGN KEY (content_type_id) REFERENCES content_types(content_type_id), + -- + constraint unlock_amount_nonnegative check (criteria_unlock_amount > 0) +); + +-- Unlockable #1: HTML Blog Example +INSERT INTO unlockables (name, description, owner, content_type_id, content_url, criteria_unlock_amount) +VALUES ('Token Gating with NFTs: Unlocking New Ways to Bring Value', 'This exclusive article contains a primer on what Token Gating is, and provides four actionable prompts on how to implement it to bring value to members of your community.', '0x1337cc354aeaf15b0e98a609cd348df171174e14', 1, 'bafybeiehgpaip4f7jafzf7imgannx3nnv3ubaiwp6ph56mlyzijpqxi45m', 1); + +-- Unlockable #2: MP4 Video Example +INSERT INTO unlockables (name, description, owner, content_type_id, content_url, criteria_unlock_amount) +VALUES ('0x1337cc354aeaf15b0e98a609cd348df171174e14', 1, 'bafybeihx5eacyxeydcpvudwxa242rnjhn67femy46gzas5d2djb24ti5mi', 1); + +-- Unlockable #3: Web Game Example +INSERT INTO unlockables (name, description, owner, content_type_id, content_url, criteria_unlock_amount) +VALUES ('Flappy Bird: Origins', 'An incredibly exclusive web game built in Godot 3, optimized for browsers. Dodge the obstacles, and fly for your life...', '0x1337cc354aeaf15b0e98a609cd348df171174e14', 1, 'bafybeihhx5v3saq3b7n55ub5q3atuw2udbqc5ictkv2ih7vd3hxptu22nu', 2); \ No newline at end of file diff --git a/src/utils/supabase/sql/users.sql b/src/utils/supabase/sql/users.sql new file mode 100644 index 000000000..fa261927e --- /dev/null +++ b/src/utils/supabase/sql/users.sql @@ -0,0 +1,11 @@ +-- Create and fill a table with users + +CREATE TABLE users ( + eth_address VARCHAR(42), + role_id int NOT NULL, + -- + PRIMARY KEY (eth_address), + FOREIGN KEY (role_id) REFERENCES roles(role_id) +); + +INSERT INTO users (eth_address, role_id) VALUES ('0x1337cc354aeaf15b0e98a609cd348df171174e14', 1); \ No newline at end of file diff --git a/src/utils/supabase/sql/view/unlockables_with_criteria.sql b/src/utils/supabase/sql/view/unlockables_with_criteria.sql new file mode 100644 index 000000000..fcbd50b36 --- /dev/null +++ b/src/utils/supabase/sql/view/unlockables_with_criteria.sql @@ -0,0 +1,4 @@ +CREATE VIEW unlockables_with_criteria AS + SELECT u.id, u.name, u.description, u.owner, u.content_url, u.criteria_unlock_amount, u.updated_at, c.nft_id + FROM unlockables u + LEFT JOIN unlock_criteria c ON u.id = c.unlockable_id diff --git a/src/utils/supabase/supabase.ts b/src/utils/supabase/supabase.ts new file mode 100644 index 000000000..b81f59017 --- /dev/null +++ b/src/utils/supabase/supabase.ts @@ -0,0 +1,9 @@ +import { createClient } from "@supabase/supabase-js"; +import { Database } from "./types"; + +const supabase = createClient<Database>( + process.env.NEXT_PUBLIC_SUPABASE_URL || "Undefined supabase URL", + process.env.NEXT_PUBLIC_SUPABASE_ANON || "Undefined supabase Anon" +); + +export default supabase; diff --git a/src/utils/supabase/typeExtensions.ts b/src/utils/supabase/typeExtensions.ts new file mode 100644 index 000000000..ba53e5f4b --- /dev/null +++ b/src/utils/supabase/typeExtensions.ts @@ -0,0 +1,4 @@ +import { Database } from "./types"; + +export type SupabaseUnlockable = + Database["public"]["Views"]["unlockables_with_criteria"]["Row"]; diff --git a/src/utils/supabase/types.ts b/src/utils/supabase/types.ts new file mode 100644 index 000000000..c423afe70 --- /dev/null +++ b/src/utils/supabase/types.ts @@ -0,0 +1,134 @@ +export type Json = + | string + | number + | boolean + | null + | { [key: string]: Json } + | Json[] + +export interface Database { + public: { + Tables: { + content_types: { + Row: { + content_id: number + content_name: string + } + Insert: { + content_id?: number + content_name: string + } + Update: { + content_id?: number + content_name?: string + } + } + roles: { + Row: { + role_id: number + role_name: string + } + Insert: { + role_id?: number + role_name: string + } + Update: { + role_id?: number + role_name?: string + } + } + unlock_criteria: { + Row: { + id: number + nft_id: string + owner: string + unlockable_id: string + updated_at: string | null + } + Insert: { + id?: number + nft_id: string + owner: string + unlockable_id: string + updated_at?: string | null + } + Update: { + id?: number + nft_id?: string + owner?: string + unlockable_id?: string + updated_at?: string | null + } + } + unlockables: { + Row: { + content_type_id: number + content_url: string + criteria_unlock_amount: number + description: string | null + id: string + name: string | null + owner: string + updated_at: string | null + } + Insert: { + content_type_id: number + content_url: string + criteria_unlock_amount: number + description?: string | null + id?: string + name?: string | null + owner: string + updated_at?: string | null + } + Update: { + content_type_id?: number + content_url?: string + criteria_unlock_amount?: number + description?: string | null + id?: string + name?: string | null + owner?: string + updated_at?: string | null + } + } + users: { + Row: { + eth_address: string + role_id: number + } + Insert: { + eth_address: string + role_id: number + } + Update: { + eth_address?: string + role_id?: number + } + } + } + Views: { + unlockables_with_criteria: { + Row: { + content_url: string | null + criteria_unlock_amount: number | null + description: string | null + id: string | null + name: string | null + nft_ids: string | null + owner: string | null + updated_at: string | null + } + } + } + Functions: { + [_ in never]: never + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } +} diff --git a/tailwind.config.js b/tailwind.config.js index f5278688e..01e70b62a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,7 @@ const { fontFamily } = require("tailwindcss/defaultTheme"); +const unlockSuccessDuration = "2s 1"; + /** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ["class", '[data-theme="dark"]'], @@ -11,6 +13,27 @@ module.exports = { display: ["var(--font-unbounded)", ...fontFamily.sans], }, keyframes: { + float: { + "0%, 100%": { transform: "translateY(0)" }, + "50%": { transform: "translateY(4px)" }, + }, + "unlock-success": { + "0%": { opacity: 0, transform: "scale(1)" }, + "50%": { opacity: 0.8 }, + "100%": { opacity: 0, transform: "scale(5)" }, + }, + "unlock-success-1": { + "0%": { opacity: 0.5, transform: "scale(1)" }, + "100%": { opacity: 0, transform: "scale(1)" }, + }, + "unlock-success-2": { + "0%": { opacity: 0.2, transform: "scale(1)" }, + "100%": { opacity: 0, transform: "scale(1.5)" }, + }, + "unlock-success-3": { + "0%": { opacity: 0.1, transform: "scale(1)" }, + "100%": { opacity: 0, transform: "scale(2)" }, + }, "accordion-down": { from: { height: 0 }, to: { height: "var(--radix-accordion-content-height)" }, @@ -21,8 +44,13 @@ module.exports = { }, }, animation: { + float: "float 4s ease-in-out infinite", "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "unlock-success": `unlock-success ${unlockSuccessDuration}`, + "unlock-success-1": `unlock-success-1 ${unlockSuccessDuration}`, + "unlock-success-2": `unlock-success-2 ${unlockSuccessDuration}`, + "unlock-success-3": `unlock-success-3 ${unlockSuccessDuration}`, }, }, },