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{" "}
-
-
+
+
+
+
+ {`${data.casts.length} casts, ${data.reactions.length} reactions, ${data.links.length} links, ${data.signers.length} signers, ${data.verifications.length} 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]}
+
+
+ );
+}