From 066d5e859008dedadf1ce6d85498b1f291bbc608 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Sat, 8 Jun 2024 23:38:52 +0200 Subject: [PATCH] feat: backfill --- .gitignore | 2 + package-lock.json | 419 +++++++++++++++++++++++++++++++++++- package.json | 11 +- src/app/globals.css | 26 +-- src/app/layout.tsx | 34 ++- src/app/page.tsx | 303 +++++++++++++++++--------- src/app/paginate.ts | 157 ++++++++++++++ src/app/utils.ts | 96 +++++++++ src/components/UserData.tsx | 20 ++ 9 files changed, 925 insertions(+), 143 deletions(-) create mode 100644 src/app/paginate.ts create mode 100644 src/app/utils.ts create mode 100644 src/components/UserData.tsx diff --git a/.gitignore b/.gitignore index fd3dbb5..b9f8d23 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.env \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fffa2f9..8e483e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,14 @@ "name": "farcaster-signer-migration", "version": "0.1.0", "dependencies": { + "@farcaster/hub-web": "^0.8.9", + "@tanstack/query-sync-storage-persister": "^5.40.0", + "@tanstack/react-query": "^5.40.1", + "@tanstack/react-query-persist-client": "^5.40.1", "next": "14.2.3", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "viem": "^2.13.7" }, "devDependencies": { "@types/node": "^20", @@ -21,6 +26,11 @@ "typescript": "^5" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz", + "integrity": "sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -33,6 +43,127 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@farcaster/core": { + "version": "0.14.13", + "resolved": "https://registry.npmjs.org/@farcaster/core/-/core-0.14.13.tgz", + "integrity": "sha512-rsXfHvXznyj3eNMlJj8ObOtSh05/UD3bLrso6wh7somH5iONVqqk+a8QIaswaxAD0Unc0NhBXK03BI7qDg2Qug==", + "dependencies": { + "@noble/curves": "^1.0.0", + "@noble/hashes": "^1.3.0", + "bs58": "^5.0.0", + "neverthrow": "^6.0.0", + "viem": "^1.12.2" + } + }, + "node_modules/@farcaster/core/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@farcaster/core/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@farcaster/core/node_modules/abitype": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.9.8.tgz", + "integrity": "sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.19.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@farcaster/core/node_modules/isows": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.3.tgz", + "integrity": "sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/@farcaster/core/node_modules/viem": { + "version": "1.21.4", + "resolved": "https://registry.npmjs.org/viem/-/viem-1.21.4.tgz", + "integrity": "sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "0.9.8", + "isows": "1.0.3", + "ws": "8.13.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@farcaster/hub-web": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@farcaster/hub-web/-/hub-web-0.8.9.tgz", + "integrity": "sha512-9vTbyl7f6ennAZ8F/hzmSYOz42twi3DHreYPdYNz9HalTJyeaLuqU4nPFYF8MgwLR+aEXyAUHfyyw7uZIr3OVg==", + "dependencies": { + "@farcaster/core": "^0.14.12", + "@improbable-eng/grpc-web": "^0.15.0", + "rxjs": "^7.8.0" + } + }, + "node_modules/@improbable-eng/grpc-web": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.15.0.tgz", + "integrity": "sha512-ERft9/0/8CmYalqOVnJnpdDry28q+j+nAlFFARdjyxXDJ+Mhgv9+F600QC8BR9ygOfrXRlAk6CvST2j+JCpQPg==", + "dependencies": { + "browser-headers": "^0.4.1" + }, + "peerDependencies": { + "google-protobuf": "^3.14.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -283,6 +414,83 @@ "node": ">=14" } }, + "node_modules/@scure/base": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.2.tgz", + "integrity": "sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==", + "dependencies": { + "@noble/curves": "~1.2.0", + "@noble/hashes": "~1.3.2", + "@scure/base": "~1.1.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", + "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -297,6 +505,71 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.40.0.tgz", + "integrity": "sha512-eD8K8jsOIq0Z5u/QbvOmfvKKE/XC39jA7yv4hgpl/1SRiU+J8QCIwgM/mEHuunQsL87dcvnHqSVLmf9pD4CiaA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-persist-client-core": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.40.0.tgz", + "integrity": "sha512-dGyxR5uEYBDDU4ARCbm7PehBbMLTqgCG/O6Q4P8mwnu7JIqn2CgCu3iSSWzCrudEq2fhiRLgZ5/3Kx7ymto6PA==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-sync-storage-persister": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.40.0.tgz", + "integrity": "sha512-My7nvaCj+WNX6NLKX6eWCHCSWHZrg5LHJrvFa3f1RT0IYAZHkW/ZRC2vhrmCfGnCDhoaPYnDZMaOLPEse8u+5A==", + "dependencies": { + "@tanstack/query-core": "5.40.0", + "@tanstack/query-persist-client-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.40.1.tgz", + "integrity": "sha512-gOcmu+gpFd2taHrrgMM9RemLYYEDYfsCqszxCC0xtx+csDa4R8t7Hr7SfWXQP13S2sF+mOxySo/+FNXJFYBqcA==", + "dependencies": { + "@tanstack/query-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@tanstack/react-query-persist-client": { + "version": "5.40.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.40.1.tgz", + "integrity": "sha512-V++NTK4PhkEgm4PD0XxK0Iak+rJ+GlSeQZQ9moACMIyG/SyX/pWyzLizJKvZw8AA0MlgdWB2J6J2Qw252nZurA==", + "dependencies": { + "@tanstack/query-persist-client-core": "5.40.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.40.1", + "react": "^18 || ^19" + } + }, "node_modules/@types/node": { "version": "20.14.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", @@ -331,6 +604,26 @@ "@types/react": "*" } }, + "node_modules/abitype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.0.tgz", + "integrity": "sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -386,6 +679,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -419,6 +717,19 @@ "node": ">=8" } }, + "node_modules/browser-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", + "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" + }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dependencies": { + "base-x": "^4.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -704,6 +1015,12 @@ "node": ">=10.13.0" } }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==", + "peer": true + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -790,6 +1107,20 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isows": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.4.tgz", + "integrity": "sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wagmi-dev" + } + ], + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", @@ -931,6 +1262,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/neverthrow": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-6.2.2.tgz", + "integrity": "sha512-POR1FACqdK9jH0S2kRPzaZEvzT11wsOxLW520PQV/+vKi9dQe+hXq19EiOvYx7lSRaF5VB9lYGsPInynrnN05w==" + }, "node_modules/next": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", @@ -1363,6 +1699,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1657,7 +2001,7 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1678,6 +2022,57 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/viem": { + "version": "2.13.7", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.13.7.tgz", + "integrity": "sha512-SZWn9LPrz40PHl4PM2iwkPTTtjWPDFsnLr32UwpqC/Z5f0AwxitjLyZdDKcImvbWZ3vLQ0oPggR1aLlqvTcUug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.0", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@scure/bip32": "1.3.2", + "@scure/bip39": "1.2.1", + "abitype": "1.0.0", + "isows": "1.0.4", + "ws": "8.13.0" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1784,6 +2179,26 @@ "node": ">=8" } }, + "node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", diff --git a/package.json b/package.json index 7e1653a..6069cc1 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,21 @@ "lint": "next lint" }, "dependencies": { + "@farcaster/hub-web": "^0.8.9", + "@tanstack/query-sync-storage-persister": "^5.40.0", + "@tanstack/react-query": "^5.40.1", + "@tanstack/react-query-persist-client": "^5.40.1", + "next": "14.2.3", "react": "^18", "react-dom": "^18", - "next": "14.2.3" + "viem": "^2.13.7" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "postcss": "^8", - "tailwindcss": "^3.4.1" + "tailwindcss": "^3.4.1", + "typescript": "^5" } } diff --git a/src/app/globals.css b/src/app/globals.css index 875c01e..9bd134e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,31 +1,7 @@ -@tailwind base; +/* @tailwind base; */ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - @layer utilities { .text-balance { text-wrap: balance; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3314e47..58dd926 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,13 +1,26 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +"use client"; + +import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; +import { QueryClient } from "@tanstack/react-query"; +import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"; + import "./globals.css"; +// export const metadata: Metadata = { +// title: "Farcaster Signer Migration", +// description: "Easily migrate/backup messages from your farcaster account.", +// }; -const inter = Inter({ subsets: ["latin"] }); +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + gcTime: 1000 * 60 * 60 * 24, // 24 hours + }, + }, +}); -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; +const persister = createSyncStoragePersister({ + storage: window.localStorage, +}); export default function RootLayout({ children, @@ -16,7 +29,12 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 2acfd44..5f821f4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,113 +1,206 @@ -import Image from "next/image"; +"use client"; + +import { Message, OnChainEvent, SignerOnChainEvent } from "@farcaster/hub-web"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { bytesToHex } from "viem"; +import { UserAccount } from "../components/UserData"; +import { getFullProfileFromHub } from "./utils"; export default function Home() { + // Queries + const { + data: dataRaw, + isLoading, + error, + isError, + } = useQuery({ + queryKey: ["profile", 1689], + queryFn: async () => getFullProfileFromHub(1689), + }); + + const data = useMemo(() => { + if (!dataRaw) return; + return { + ...dataRaw, + casts: dataRaw.casts.map((cast) => Message.fromJSON(cast)), + reactions: dataRaw.reactions.map((reaction) => + Message.fromJSON(reaction) + ), + links: dataRaw.links.map((link) => Message.fromJSON(link)), + verifications: dataRaw.verifications.map((verification) => + Message.fromJSON(verification) + ), + userData: dataRaw.userData.map((userData) => Message.fromJSON(userData)), + signers: dataRaw.signers.map((signer) => { + const { metadata, ...event } = signer as any; + const eventDecoded = OnChainEvent.fromJSON( + event + ) as unknown as SignerOnChainEvent; + return { + ...eventDecoded, + metadata: metadata as { + requestFid: number; + requestSigner: string; + signature: string; + deadline: number; + }, + }; + }), + }; + }, [dataRaw]); + + const signersByFid = useMemo(() => { + // Group signers by requestFid + return data?.signers.reduce( + (acc, signer) => { + if (!acc.fidToSigner[signer.metadata.requestFid]) { + acc.fidToSigner[signer.metadata.requestFid.toString()] = []; + } + acc.fidToSigner[signer.metadata.requestFid.toString()].push(signer); + + if (!acc.signerToFid[bytesToHex(signer.signerEventBody.key)]) { + acc.signerToFid[signer.metadata.requestFid.toString()] = + signer.metadata.requestFid.toString(); + } + + return acc; + }, + { fidToSigner: {}, signerToFid: {} } as { + fidToSigner: Record; + signerToFid: Record; + } + ); + }, [data]); + + const messagesBySigner = useMemo(() => { + if (!data) return; + + const messages = {} as Record< + string, + Omit + >; + data.casts.map((cast) => { + const signer = bytesToHex(cast.signer); + if (!messages[signer]) { + messages[signer] = { + casts: [], + reactions: [], + links: [], + verifications: [], + userData: [], + }; + } + messages[signer].casts.push(cast); + }); + + data.reactions.map((reaction) => { + const signer = bytesToHex(reaction.signer); + if (!messages[signer]) { + messages[signer] = { + casts: [], + reactions: [], + links: [], + verifications: [], + userData: [], + }; + } + messages[signer].reactions.push(reaction); + }); + + data.links.map((link) => { + const signer = bytesToHex(link.signer); + if (!messages[signer]) { + messages[signer] = { + casts: [], + reactions: [], + links: [], + verifications: [], + userData: [], + }; + } + messages[signer].links.push(link); + }); + + data.verifications.map((verification) => { + const signer = bytesToHex(verification.signer); + if (!messages[signer]) { + messages[signer] = { + casts: [], + reactions: [], + links: [], + verifications: [], + userData: [], + }; + } + messages[signer].verifications.push(verification); + }); + + return messages; + }, [data]); + + const messageCountsByFid = useMemo(() => { + if (!messagesBySigner) return; + + return Object.entries(messagesBySigner).reduce( + (acc, [signer, messages]) => { + if (!signersByFid) return acc; + + const fid = signersByFid.signerToFid[signer]; + if (!acc[fid]) { + acc[fid] = { + casts: 0, + reactions: 0, + links: 0, + verifications: 0, + }; + } + acc[fid].casts += messages.casts.length; + acc[fid].reactions += messages.reactions.length; + acc[fid].links += messages.links.length; + acc[fid].verifications += messages.verifications.length; + + return acc; + }, + {} as Record< + string, + { + casts: number; + reactions: number; + links: number; + verifications: number; + } + > + ); + }, [messagesBySigner]); + + if (isLoading) + return
{process.env.NEXT_PUBLIC_HUB_REST_URL} Loading...
; + + if (isError || !data || !signersByFid) + return ( +
Error {error instanceof Error && error.message + error.stack}
+ ); + return ( -
-
-

- Get started by editing  - src/app/page.tsx -

-
- - By{" "} - Vercel Logo - +
+
+ +
+ {`${data.casts.length} casts, ${data.reactions.length} reactions, ${data.links.length} links, ${data.signers.length} signers, ${data.verifications.length} verifications`}
- -
- Next.js Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
+
+ {Object.entries(signersByFid.fidToSigner).map(([fid, signers], i) => ( +
+ +
+
{`${signers.length} signers`}
+ {/*
{`${messageCountsByFid?.[fid].casts} casts, ${messageCountsByFid?.[fid].reactions} reactions, ${messageCountsByFid?.[fid].links} links, ${messageCountsByFid?.[fid].verifications} verifications`}
*/} +
+
+ ))}
-
+ ); } diff --git a/src/app/paginate.ts b/src/app/paginate.ts new file mode 100644 index 0000000..bd74c64 --- /dev/null +++ b/src/app/paginate.ts @@ -0,0 +1,157 @@ +import { FidRequest, OnChainEvent, SignerEventType } from "@farcaster/hub-web"; + +import { bytesToHex, decodeAbiParameters } from "viem"; +import { MAX_PAGE_SIZE } from "./utils"; + +export const signedKeyRequestAbi = [ + { + components: [ + { + name: "requestFid", + type: "uint256", + }, + { + name: "requestSigner", + type: "address", + }, + { + name: "signature", + type: "bytes", + }, + { + name: "deadline", + type: "uint256", + }, + ], + name: "SignedKeyRequest", + type: "tuple", + }, +] as const; + +export async function getAllMessagesFromHubEndpoint({ + endpoint, + fid, +}: { + endpoint: string; + fid: number; +}) { + const messages: unknown[] = new Array(); + let nextPageToken: string | undefined; + + while (true) { + const params = new URLSearchParams({ + fid: fid.toString(), + pageSize: MAX_PAGE_SIZE.toString(), + }); + + if (nextPageToken) { + params.append("pageToken", nextPageToken); + } + + const url = `${process.env.NEXT_PUBLIC_HUB_REST_URL}${endpoint}?${params}`; + + const res = await fetch(url); + const { messages: resMessages, nextPageToken: _nextPageToken } = + await res.json(); + + nextPageToken = _nextPageToken; + + messages.push(...resMessages); + + if (resMessages.length < MAX_PAGE_SIZE) { + break; + } + } + + return messages; +} + +export async function getAllCastsByFid(fid: FidRequest) { + const casts: unknown[] = await getAllMessagesFromHubEndpoint({ + endpoint: "/v1/castsByFid", + fid: fid.fid, + }); + + return casts; +} + +export async function getAllReactionsByFid(fid: FidRequest) { + const reactions: unknown[] = await getAllMessagesFromHubEndpoint({ + endpoint: "/v1/reactionsByFid", + fid: fid.fid, + }); + + return reactions; +} + +export async function getAllLinksByFid(fid: FidRequest) { + const links: unknown[] = await getAllMessagesFromHubEndpoint({ + endpoint: "/v1/linksByFid", + fid: fid.fid, + }); + + return links; +} + +export function decodeSignedKeyRequestMetadata(metadata: Uint8Array) { + return decodeAbiParameters(signedKeyRequestAbi, bytesToHex(metadata))[0]; +} + +export async function getAllSignersByFid(fid: FidRequest) { + const events: unknown[] = new Array(); + let nextPageToken: string | undefined; + + while (true) { + const params = new URLSearchParams({ + fid: fid.fid.toString(), + pageSize: MAX_PAGE_SIZE.toString(), + }); + + if (nextPageToken) { + params.append("pageToken", nextPageToken); + } + + const res = await fetch( + `${process.env.NEXT_PUBLIC_HUB_REST_URL}/v1/onChainSignersByFid?${params}` + ); + const { events: resEvents, ..._nextPageToken } = await res.json(); + + nextPageToken = _nextPageToken; + + for (const signerJson of resEvents) { + const signer = OnChainEvent.fromJSON(signerJson); + const body = signer.signerEventBody; + const timestamp = new Date(signer.blockTimestamp * 1000); + + switch (body?.eventType) { + case SignerEventType.ADD: { + const signedKeyRequestMetadata = decodeSignedKeyRequestMetadata( + body.metadata + ); + const metadataJson = { + requestFid: Number(signedKeyRequestMetadata.requestFid), + requestSigner: signedKeyRequestMetadata.requestSigner, + signature: signedKeyRequestMetadata.signature, + deadline: Number(signedKeyRequestMetadata.deadline), + }; + + events.push({ + ...signerJson, + metadata: metadataJson, + }); + + break; + } + case SignerEventType.REMOVE: { + break; + } + } + } + + if (resEvents.length < MAX_PAGE_SIZE) { + break; + } + } + + return events; +} diff --git a/src/app/utils.ts b/src/app/utils.ts new file mode 100644 index 0000000..94a347b --- /dev/null +++ b/src/app/utils.ts @@ -0,0 +1,96 @@ +import { + FidRequest, + Message, + UserDataType, + isUserDataAddMessage, +} from "@farcaster/hub-web"; +import { + getAllCastsByFid, + getAllLinksByFid, + getAllMessagesFromHubEndpoint, + getAllReactionsByFid, + getAllSignersByFid, +} from "./paginate"; + +export const MAX_PAGE_SIZE = 1_000; + +/** + * Index all messages from a profile + * @param fid Farcaster ID + */ +export async function getFullProfileFromHub(_fid: number) { + const fid = FidRequest.create({ fid: _fid }); + + const verifications = await getAllMessagesFromHubEndpoint({ + endpoint: "/v1/verificationsByFid", + fid: fid.fid, + }); + + const signers = await getAllSignersByFid(fid); + + const userData = await getAllMessagesFromHubEndpoint({ + endpoint: "/v1/userDataByFid", + fid: fid.fid, + }); + + const signerFidsUnique = Array.from( + new Set( + signers.map( + (s) => (s as { metadata: { requestFid: number } }).metadata.requestFid + ) + ) + ); + + const signerProfiles: Record< + string, + Awaited> + > = {}; + for (const signerFid of signerFidsUnique) { + signerProfiles[signerFid.toString()] = await getUserData(signerFid); + } + + return { + casts: await getAllCastsByFid(fid), + reactions: await getAllReactionsByFid(fid), + links: await getAllLinksByFid(fid), + userData, + userDataAggregated: aggregateUserData(userData), + verifications: verifications, + signers, + signerProfiles, + + // Onchain events + // registrations: getAllRegistrationsByFid(_fid), + // storage: getAllStorageByFid(_fid), + }; +} + +function aggregateUserData(messagesJson: unknown[]) { + return messagesJson.reduce( + (acc: Partial>, messageJson) => { + const decodedMessage = Message.fromJSON(messageJson); + + if (!isUserDataAddMessage(decodedMessage)) { + return acc; + } + + return { + ...acc, + [decodedMessage.data.userDataBody.type]: + decodedMessage.data.userDataBody.value, + }; + }, + {} as Partial> + ); +} + +async function getUserData(fid: number) { + const userData = await getAllMessagesFromHubEndpoint({ + endpoint: "/v1/userDataByFid", + fid, + }); + + return aggregateUserData(userData); +} + +export type UserDataAggType = ReturnType; diff --git a/src/components/UserData.tsx b/src/components/UserData.tsx new file mode 100644 index 0000000..3e67d59 --- /dev/null +++ b/src/components/UserData.tsx @@ -0,0 +1,20 @@ +import { UserDataType } from "@farcaster/hub-web"; +import { UserDataAggType } from "../app/utils"; + +export function UserAccount({ data }: { data: UserDataAggType }) { + return ( +
+
+ +
+
+
{data[UserDataType.DISPLAY]}
+
@{data[UserDataType.USERNAME]}
+
{data[UserDataType.BIO]}
+
+
+ ); +}