├── .env.example
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── pnpm-lock.yaml
├── postcss.config.cjs
├── src
├── app.css
├── app.html
├── global.d.ts
├── hooks.ts
├── lib
│ ├── components
│ │ ├── AddFacet.svelte
│ │ ├── Bookmark.svelte
│ │ ├── Comments.svelte
│ │ ├── ConnectToOrbis.svelte
│ │ ├── FacetCard.svelte
│ │ ├── Featured.svelte
│ │ ├── Header.svelte
│ │ ├── History.svelte
│ │ ├── LatestDiamonds.svelte
│ │ ├── Loading.svelte
│ │ ├── ProfileDropdown.svelte
│ │ ├── ReadContract.svelte
│ │ ├── RemoveFacet.svelte
│ │ ├── Search.svelte
│ │ ├── Sponsor.svelte
│ │ ├── Stats.svelte
│ │ ├── TopDiamonds.svelte
│ │ ├── TransactionNotification.svelte
│ │ ├── Transactions.svelte
│ │ └── WriteContract.svelte
│ ├── config.ts
│ ├── services
│ │ ├── contractReader.ts
│ │ └── diamond.ts
│ ├── stores
│ │ ├── navigationState.ts
│ │ ├── orbis.ts
│ │ └── user.ts
│ └── utils.ts
├── routes
│ ├── +error.svelte
│ ├── +layout.svelte
│ ├── +page.svelte
│ ├── api
│ │ ├── contract
│ │ │ └── +server.ts
│ │ ├── events
│ │ │ └── +server.ts
│ │ ├── facets
│ │ │ └── +server.ts
│ │ ├── leaderboard
│ │ │ └── +server.ts
│ │ ├── readContract
│ │ │ └── +server.ts
│ │ ├── stats
│ │ │ └── +server.ts
│ │ └── transactions
│ │ │ └── +server.ts
│ ├── bookmarks
│ │ ├── +page.js
│ │ └── +page.svelte
│ ├── diamond
│ │ └── [address]
│ │ │ ├── +page.svelte
│ │ │ └── +page.ts
│ └── sponsor
│ │ └── +page.svelte
└── types
│ └── entities.ts
├── static
├── favicon.ico
├── img
│ ├── aavegotchi-mainnet-logo.png
│ ├── aavegotchi-polygon-logo.jpg
│ ├── barnbridge-logo.jpg
│ ├── beanstalk-logo.png
│ ├── connext-logo.png
│ ├── escabro-logo.png
│ ├── gelato-logo.png
│ ├── lifi.png
│ ├── mark3labslogo.png
│ ├── piedao-logo.png
│ └── quicknode-logo.svg
├── louper-logo-transparent.png
├── louper-logo.png
├── louper-logo.svg
├── manifest.json
└── thumbnail.png
├── supabase
├── config.toml
└── migrations
│ ├── 20220601172756_add_contracts_table.sql
│ └── 20220805092229_add_leaderboard.sql
├── svelte.config.js
├── tailwind.config.cjs
├── tsconfig.json
└── vite.config.ts
/.env.example:
--------------------------------------------------------------------------------
1 | BINANCE_ETHERSCAN_API_KEY=
2 | POLYGON_ETHERSCAN_API_KEY=
3 | ETHERSCAN_API_KEY=
4 | FUJI_ETHERSCAN_API_KEY=
5 | SUPABASE_URL=http://localhost:54321
6 | SUPABASE_KEY=
7 | INFURA_API_KEY=
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5 | plugins: ['svelte3', '@typescript-eslint'],
6 | ignorePatterns: ['*.cjs'],
7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8 | settings: {
9 | 'svelte3/typescript': () => require('typescript'),
10 | },
11 | parserOptions: {
12 | sourceType: 'module',
13 | ecmaVersion: 2019,
14 | },
15 | env: {
16 | browser: true,
17 | es2017: true,
18 | node: true,
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .vercel_build_output
8 | # Supabase
9 | **/supabase/.branches
10 | **/supabase/.temp
11 | .vercel
12 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": false,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "printWidth": 100,
6 | "semi": false
7 | }
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Louper - The Ethereum Diamond Inspector
2 |
3 | A simple UI for viewing details about EVM smart contracts using EIP-2535 (Diamond Standard)
4 |
5 | **Features:**
6 |
7 | - View diamond details include all facets and their methods
8 | - Read from facet methods
9 | - Write to facet methods
10 | - **MIT License** completely open source to do with as you please
11 |
12 | ## Development
13 |
14 | ### Prerequisites
15 |
16 | You will need a Subabase local db instance. Follow the [instructions](https://supabase.com/docs/guides/local-development) to get set up.
17 |
18 | Run:
19 |
20 | ```sh
21 | supabase init
22 | supabase start
23 | supabase db reset
24 | ```
25 |
26 | You should now have a working DB.
27 |
28 | Run:
29 |
30 | ```sh
31 | cp .env.example .env
32 | supabase status
33 | ```
34 |
35 | Copy the `service_role key` and paste that as the value of `SUPABASE_KEY` in `.env`
36 |
37 | Ensure you have [pnpm](https://pnpm.io/installation) installed.
38 |
39 | Run:
40 |
41 | ```sh
42 | pnpm install
43 | pnpm run dev
44 | ```
45 |
46 | You should now have a working dev environment.
47 |
48 | ## License
49 |
50 | MIT License
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "louper-v2",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "vite build",
7 | "preview": "vite preview",
8 | "check": "svelte-check --tsconfig ./tsconfig.json",
9 | "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
10 | "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
11 | "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. .",
12 | "postinstall": "svelte-kit sync"
13 | },
14 | "devDependencies": {
15 | "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
16 | "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
17 | "@sveltejs/kit": "1.24.1",
18 | "@types/node": "^20.6.0",
19 | "@typescript-eslint/eslint-plugin": "^6.6.0",
20 | "@typescript-eslint/parser": "^6.6.0",
21 | "autoprefixer": "^10.4.15",
22 | "buffer": "^6.0.3",
23 | "cssnano": "^6.0.1",
24 | "esbuild": "^0.19.2",
25 | "eslint": "^8.49.0",
26 | "eslint-config-prettier": "^9.0.0",
27 | "postcss": "^8.4.29",
28 | "postcss-load-config": "^4.0.1",
29 | "prettier": "^3.0.3",
30 | "prettier-plugin-svelte": "^3.0.3",
31 | "rollup": "^3.29.1",
32 | "rollup-plugin-node-polyfills": "^0.2.1",
33 | "rollup-plugin-polyfill-node": "^0.12.0",
34 | "svelte": "^4.2.0",
35 | "svelte-check": "^3.5.1",
36 | "svelte-preprocess": "^5.0.4",
37 | "tailwindcss": "^3.3.3",
38 | "tslib": "^2.6.2",
39 | "typescript": "^5.2.2",
40 | "vite": "^4.4.9",
41 | "vite-plugin-node-polyfills": "^0.12.0"
42 | },
43 | "type": "module",
44 | "dependencies": {
45 | "@ceramicnetwork/stream-tile": "^2.29.0",
46 | "@ethersproject/abi": "^5.7.0",
47 | "@ethersproject/bignumber": "^5.7.0",
48 | "@ethersproject/contracts": "^5.7.0",
49 | "@ethersproject/providers": "^5.7.2",
50 | "@opentelemetry/api": "^1.4.1",
51 | "@orbisclub/orbis-sdk": "^0.4.87",
52 | "@supabase/supabase-js": "^2.33.1",
53 | "@sveltejs/adapter-vercel": "3.0.3",
54 | "@tailwindcss/typography": "^0.5.10",
55 | "axios": "^1.5.0",
56 | "daisyui": "^3.7.3",
57 | "dotenv": "^16.3.1",
58 | "eslint-plugin-svelte": "^2.33.1",
59 | "ethers": "^6.7.1",
60 | "lodash": "^4.17.21",
61 | "redaxios": "^0.5.1",
62 | "svelte-notifications": "^0.9.98",
63 | "svelte-select": "^5.7.0",
64 | "svelte-tags-input": "^5.0.0",
65 | "web3w": "0.3.2-beta.22",
66 | "web3w-walletconnect-loader": "^0.4.3"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const tailwindcss = require('tailwindcss')
2 | const autoprefixer = require('autoprefixer')
3 | const cssnano = require('cssnano')
4 |
5 | const mode = process.env.NODE_ENV
6 | const dev = mode === 'development'
7 |
8 | const config = {
9 | plugins: [
10 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind,
11 | tailwindcss(),
12 | //But others, like autoprefixer, need to run after,
13 | autoprefixer(),
14 | !dev &&
15 | cssnano({
16 | preset: 'default',
17 | }),
18 | ],
19 | }
20 |
21 | module.exports = config
22 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | /* Write your global styles here, in PostCSS syntax */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | .svelte-tags-input-layout {
7 | @apply border-2 border-primary rounded m-2 bg-base-100 !important;
8 | }
9 |
10 | .svelte-tags-input {
11 | @apply bg-base-100;
12 | }
13 |
14 | .svelte-tags-input-tag {
15 | @apply badge-primary !important;
16 | }
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 | Louper - The Ethereum Diamond Inspector
12 |
13 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
32 | %sveltekit.head%
33 |
34 |
35 | %sveltekit.body%
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import type { Handle } from "@sveltejs/kit";
2 |
3 | export const handle: Handle = async ({ resolve, event }) => {
4 | const response = await resolve(event);
5 |
6 | // Apply CORS header for API routes
7 | if (event.url.pathname.startsWith('/api')) {
8 | // Required for CORS to work
9 | if (event.request.method === 'OPTIONS') {
10 | return new Response(null, {
11 | headers: {
12 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, DELETE',
13 | 'Access-Control-Allow-Origin': '*',
14 | }
15 | });
16 | }
17 |
18 | response.headers.append('Access-Control-Allow-Origin', `*`);
19 | }
20 |
21 | return response;
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/components/AddFacet.svelte:
--------------------------------------------------------------------------------
1 |
148 |
149 | {#if showModal}
150 |
151 |
152 |
153 |
Add New Facet
154 |
155 | {#if fetchFacetError}
156 |
157 |
158 | 🛑
159 |
160 | {fetchFacetError}
161 |
162 |
163 |
164 | {/if}
165 |
166 | {#if $wallet.state !== 'Ready'}
167 |
168 | {#if $wallet.state !== 'Ready'}
169 | {#if $builtin.available}
170 | connect()}>
171 | Connect With Metamask
172 |
173 | {/if}
174 | connect('walletconnect')}>
175 | Connect With WalletConnect
176 |
177 | {/if}
178 |
179 | {:else}
180 | {#if !facet}
181 |
182 |
183 |
184 | Facet Address
185 |
186 |
191 |
192 |
193 | Fetch Facet Info
194 |
195 |
196 | {/if}
197 |
198 | {#if facet}
199 |
217 |
218 |
219 |
220 | Init Contract Address
221 |
222 |
227 |
228 |
229 |
230 | Init Calldata
231 |
232 |
237 |
238 |
239 |
240 |
241 | {#if $wallet.pendingUserConfirmation}
242 | Please check and approve the transaction in your wallet.
243 | {/if}
244 |
245 | {#if $flow.inProgress}
246 |
247 |
248 |
249 | {/if}
250 |
251 | {#if error}
252 |
253 |
254 | {error.message}
255 |
256 |
257 | {/if}
258 |
259 |
260 |
261 |
262 |
265 | flow.execute(async (contracts) => {
266 | const tx = await contracts.facet['diamondCut'](...args)
267 | await tx.wait()
268 | window.location.reload()
269 | })}
270 | >
271 |
278 |
284 |
285 | ADD FACET
286 |
287 |
288 | {/if}
289 | {/if}
290 |
291 |
292 |
293 |
294 |
295 | Close
296 |
297 |
298 | {/if}
299 |
--------------------------------------------------------------------------------
/src/lib/components/Bookmark.svelte:
--------------------------------------------------------------------------------
1 |
53 |
54 |
55 |
62 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/src/lib/components/Comments.svelte:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
59 |
60 |
67 |
73 |
74 | Comments
75 | {#if comments.length}
76 |
{comments.length}
77 | {/if}
78 |
79 |
80 | {#each comments as comment}
81 |
82 | {comment.creator_details.profile
83 | ? comment.creator_details.profile.username
84 | : shortProfile(comment.creator_details.metadata.address)}
85 |
86 |
87 | {comment.content.body}
88 |
89 | {/each}
90 | {#if $user}
91 |
92 |
99 |
100 |
101 | Post
102 |
103 |
104 |
105 | {:else}
106 |
107 | You must sign in to post a comment.
108 |
109 |
110 | {/if}
111 |
112 |
113 |
--------------------------------------------------------------------------------
/src/lib/components/ConnectToOrbis.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
15 | Sign In
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/FacetCard.svelte:
--------------------------------------------------------------------------------
1 |
36 |
37 |
38 |
39 |
64 |
65 |
window.open(getExplorerAddressUrl(facet.address, diamond.network))}
68 | >
69 | {facet.address}
70 |
76 |
79 |
82 |
83 |
84 |
{
87 | await navigator.clipboard.writeText(`${facet.address}`)
88 | alert('Address copied!')
89 | }}
90 | >
91 |
100 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | Method
113 | Selector
114 |
115 |
116 |
117 |
118 |
119 | {#if readOnlyMethods.length > 0}
120 |
121 | Read Only
122 |
123 | {/if}
124 | {#each readOnlyMethods as method}
125 |
126 | {method.signature}
127 |
128 | {method.selector}
129 |
130 |
131 | {/each}
132 | {#if writeableMethods.length > 0}
133 |
134 | Writeable
135 |
136 | {/if}
137 | {#each writeableMethods as method}
138 |
139 | {method.signature}
140 |
141 | {method.selector}
142 |
143 |
144 | {/each}
145 | {#if unknownMethods.length > 0}
146 |
147 | Unknown
148 |
149 | {/if}
150 | {#each unknownMethods as method}
151 |
152 | {method.signature}
153 |
154 | {method.selector}
155 |
156 |
157 | {/each}
158 |
159 |
160 |
161 | {#if facet.name}
162 | {#if readOnlyMethods.length}
163 |
{
166 | showReadContract = true
167 | showWriteContract = false
168 | showRemoveFacet = false
169 | activeFacet = facet
170 | }}
171 | >
172 |
181 |
182 |
183 |
184 | Read
185 |
186 | {/if}
187 | {#if writeableMethods.length}
188 |
{
191 | showReadContract = false
192 | showWriteContract = true
193 | showRemoveFacet = false
194 | activeFacet = facet
195 | }}
196 | >
197 |
203 |
208 |
209 | Write
210 |
211 | {/if}
212 | {/if}
213 |
{
216 | showReadContract = false
217 | showWriteContract = false
218 | showRemoveFacet = true
219 | activeFacet = facet
220 | }}
221 | >
222 |
229 |
235 |
236 | Remove
237 |
238 |
239 |
240 |
241 |
--------------------------------------------------------------------------------
/src/lib/components/Featured.svelte:
--------------------------------------------------------------------------------
1 |
67 |
68 | Featured Diamonds
69 |
70 |
71 |
74 |
75 | {#each diamonds as diamond}
76 |
goto(diamond.url)}
79 | >
80 |
81 |
82 |
83 |
84 |
85 |
{diamond.name}
86 |
87 | {diamond.description}
88 |
89 |
107 |
108 | {/each}
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/src/lib/components/Header.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/components/History.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
21 |
27 |
28 | Upgrade History
29 | {#if events.length}
30 |
{events.length}
31 | {/if}
32 |
33 |
34 | {#if events & events.length}
35 |
36 | {#each events.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) as event}
37 |
38 |
41 | 💎
42 |
43 |
44 |
Diamond Cut
45 |
46 | {new Date(event.timestamp * 1000).toUTCString()}
47 |
48 |
53 | {event.txHash}
54 |
60 |
63 |
66 |
67 |
68 | {#each event[0] as cut}
69 |
100 |
101 | {#each cut[2] as selector}
102 | {selector}
103 | {/each}
104 |
105 | {/each}
106 |
107 |
108 | {/each}
109 |
110 | {/if}
111 |
112 |
113 |
--------------------------------------------------------------------------------
/src/lib/components/LatestDiamonds.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
Recent Diamonds Inspected
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Name
26 | Address
27 | Network
28 |
29 |
30 |
31 | {#each diamonds.filter((d) => {
32 | NETWORKS[d.network] !== undefined
33 | }) as diamond}
34 | goto(`/diamond/${diamond.address}?network=${diamond.network}`)}
37 | >
38 | {diamond.name.substring(0, 25)}
39 | {diamond.address.substring(0, 20)}...{diamond.address.substring(36)}
40 |
41 | {NETWORKS[diamond.network].emoji}
42 | {NETWORKS[diamond.network].title}
43 |
44 |
45 | {/each}
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/lib/components/Loading.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/components/ProfileDropdown.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 | {#if $user}
13 |
14 |
15 | {#if $user.profile}
16 |
17 | {:else}
18 |
19 | {/if}
20 | {$user.profile ? $user.profile.username : shortProfile($user.metadata.address)}
21 |
22 |
62 |
63 | {/if}
64 |
--------------------------------------------------------------------------------
/src/lib/components/ReadContract.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 | {#if showModal && facet}
20 |
21 |
22 | Read {facet.name}
23 |
24 |
25 |
26 |
27 | Choose a Method to Interact With
28 | {
33 | args = []
34 | readResult = ''
35 | }}
36 | >
37 | Select a method
38 | {#each facet.methods.filter((m) => m.fragment.constant) as method}
39 |
40 | {method.fragment.name}
41 |
42 | {/each}
43 |
44 |
45 | {#if selectedMethod}
46 |
79 |
80 |
Result
81 |
82 |
85 | {#await readResult}
86 |
87 |
88 |
89 | {:then res}
90 | {#if res}
91 |
92 | {JSON.stringify(res.data, (k, v) => (typeof v === 'bigint' ? v.toString() : v), 2)}
93 |
94 | {/if}
95 | {:catch}
96 | Whoops! Something went wrong...
97 | {/await}
98 |
99 |
100 | {/if}
101 |
102 |
103 |
104 |
105 |
106 | {
109 | showModal = false
110 | selectedMethod = null
111 | readResult = ''
112 | args = []
113 | }}
114 | >
115 | Close
116 |
117 |
118 |
119 | {/if}
120 |
--------------------------------------------------------------------------------
/src/lib/components/RemoveFacet.svelte:
--------------------------------------------------------------------------------
1 |
100 |
101 | {#if showModal && facet}
102 |
103 |
104 | Remove {facet.name}
105 |
106 |
107 |
108 |
109 | 💀
110 |
111 | This is a BETA feature and may break your diamond contract. This will remove this facet
112 | and all selectors from this contract!
113 |
114 |
115 |
116 |
117 |
118 | {#if $wallet.state !== 'Ready'}
119 | {#if $builtin.available}
120 | connect()}>
121 | Connect With Metamask
122 |
123 | {/if}
124 | connect('walletconnect')}>
125 | Connect With WalletConnect
126 |
127 | {/if}
128 |
129 |
130 | {#if $wallet.state === 'Ready'}
131 | {#if facet}
132 |
152 | {/if}
153 |
154 |
155 |
158 | flow.execute(async (contracts) => {
159 | console.log(args)
160 | const tx = await contracts.facet['diamondCut'](...args)
161 | await tx.wait()
162 | window.location.reload()
163 | })}
164 | >
165 |
172 |
178 |
179 | REMOVE
180 |
181 |
182 |
183 |
184 | {#if $wallet.pendingUserConfirmation}
185 | Please check and approve the transaction in your wallet.
186 | {/if}
187 |
188 | {#if $flow.inProgress}
189 |
190 |
191 |
192 | {/if}
193 |
194 | {#if error}
195 |
196 |
197 | {error.message}
198 |
199 |
200 | {/if}
201 |
202 |
203 | {/if}
204 |
205 |
206 |
207 |
208 | Close
209 |
210 |
211 | {/if}
212 |
--------------------------------------------------------------------------------
/src/lib/components/Search.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
Search by Address
17 |
55 |
56 |
57 |
58 |
72 |
--------------------------------------------------------------------------------
/src/lib/components/Sponsor.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
Sponsor Louper
8 |
Reach thousands of Blockchain Enthusiasts & Developers Worldwide
9 |
10 | goto('/sponsor')}>Contact Us
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/lib/components/Stats.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
💎
17 |
Diamonds Inspected
18 | {#if diamondCount}
19 |
{diamondCount}
20 | {:else}
21 |
22 | {/if}
23 |
24 |
25 |
26 |
📄
27 |
Contracts Inspected
28 | {#if contractCount}
29 |
{contractCount}
30 | {:else}
31 |
32 | {/if}
33 |
34 |
35 |
36 |
👀
37 |
Pages Views
38 | {#if pageViews}
39 |
{pageViews}
40 | {:else}
41 |
42 | {/if}
43 |
Last 30 Days
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/lib/components/TopDiamonds.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
Top Diamonds Inspected
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Name
26 | Address
27 | Views
28 | Network
29 |
30 |
31 |
32 | {#each diamonds.filter((d) => {
33 | NETWORKS[d.network] !== undefined
34 | }) as diamond}
35 | goto(`/diamond/${diamond.address}?network=${diamond.network}`)}
38 | >
39 | {diamond.name.substring(0, 25)}
40 | {diamond.address.substring(0, 20)}...{diamond.address.substring(36)}
41 | {diamond.hits}
42 |
43 | {NETWORKS[diamond.network].emoji}
44 | {NETWORKS[diamond.network].title}
45 |
46 |
47 | {/each}
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/lib/components/TransactionNotification.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
120 |
--------------------------------------------------------------------------------
/src/lib/components/Transactions.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 |
30 |
31 |
32 |
41 |
46 |
47 | Recent Transactions
48 |
49 |
50 |
51 |
52 |
53 |
54 | Block #
55 | Hash
56 | Function
57 | Age
58 | From
59 | Value
60 |
61 |
62 |
63 |
64 | {#each transactions as t}
65 |
66 | {t.blockNumber}
67 |
68 | window.open(getExplorerTxUrl(t.hash, diamond.network))}
71 | >
72 | {t.hash ? shortHash(t.hash) : ''}
73 |
79 |
82 |
85 |
86 |
87 |
88 | {t.functionName ? t.functionName.split('(')[0] : t.methodId}
89 | {humanTime(t.timeStamp)}
90 |
91 | window.open(getExplorerAddressUrl(t.address, diamond.network))}
94 | >
95 | {t.from ? shortAddress(t.from) : ''}
96 |
102 |
105 |
108 |
109 |
110 |
111 | {ethers.formatEther(typeof t.value === 'string' ? t.value : 0)}
112 |
113 | {/each}
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/src/lib/components/WriteContract.svelte:
--------------------------------------------------------------------------------
1 |
84 |
85 | {#if showModal && facet}
86 |
87 |
88 | Write {facet.name}
89 |
90 | {#if $wallet.state !== 'Ready'}
91 | {#if $builtin.available}
92 |
connect()}>
93 | Connect With Metamask
94 |
95 | {/if}
96 |
connect('walletconnect')}>
97 | Connect With WalletConnect
98 |
99 | {/if}
100 |
101 | {#if $wallet.state === 'Ready'}
102 |
103 | Choose a Method to Interact With
104 |
105 | {
110 | args = []
111 | error = null
112 | }}
113 | >
114 | Select a method
115 | {#each facet.methods.filter((m) => !m.fragment.constant) as method}
116 |
117 | {method.fragment.name}
118 |
119 | {/each}
120 |
121 |
122 | {/if}
123 | {#if selectedMethod}
124 |
186 |
187 |
188 | {#if $wallet.pendingUserConfirmation}
189 | Please check and approve the transaction in your wallet.
190 | {/if}
191 |
192 | {#if $flow.inProgress}
193 |
194 |
195 |
196 | {/if}
197 |
198 | {#if error}
199 |
200 |
201 | {error.message}
202 |
203 |
204 | {/if}
205 |
206 |
207 | {/if}
208 |
209 |
210 |
211 |
212 |
213 | Close
214 |
215 |
216 | {/if}
217 |
--------------------------------------------------------------------------------
/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | type Network = {
2 | title: string
3 | rpcUrl: string
4 | explorerUrl: string
5 | explorerApiUrl: string
6 | emoji: string
7 | chainId: string
8 | txHistorySupported?: boolean
9 | }
10 |
11 | export const NETWORKS: Record = {
12 | mainnet: {
13 | title: 'Ethereum',
14 | rpcUrl: 'https://1rpc.io/eth',
15 | explorerUrl: 'https://etherscan.io',
16 | explorerApiUrl: 'https://api.etherscan.io/api',
17 | emoji: '🟢',
18 | chainId: '1',
19 | txHistorySupported: true,
20 | },
21 | goerli: {
22 | title: 'Goerli Testnet',
23 | rpcUrl: 'https://rpc.ankr.com/eth_goerli',
24 | explorerUrl: 'https://goerli.etherscan.io',
25 | explorerApiUrl: 'https://api-goerli.etherscan.io/api',
26 | emoji: '🧪',
27 | chainId: '5',
28 | txHistorySupported: true,
29 | },
30 | sepolia: {
31 | title: 'Sepolia Testnet',
32 | rpcUrl: 'https://endpoints.omniatech.io/v1/eth/sepolia/public',
33 | explorerUrl: 'https://sepolia.etherscan.io/',
34 | explorerApiUrl: 'https://api-sepolia.etherscan.io/api',
35 | emoji: '🏀',
36 | chainId: '11155111',
37 | txHistorySupported: true,
38 | },
39 | xdai: {
40 | title: 'Gnosis (XDai)',
41 | rpcUrl: 'https://rpc.gnosischain.com/',
42 | explorerUrl: 'https://gnosisscan.io',
43 | explorerApiUrl: 'https://api.gnosisscan.io/api',
44 | emoji: '🟡',
45 | chainId: '100',
46 | txHistorySupported: true,
47 | },
48 | polygon: {
49 | title: 'Polygon',
50 | rpcUrl: 'https://polygon-rpc.com/',
51 | explorerUrl: 'https://polygonscan.com',
52 | explorerApiUrl: 'https://api.polygonscan.com/api',
53 | emoji: '🟣',
54 | chainId: '137',
55 | txHistorySupported: true,
56 | },
57 | polygon_zkevm: {
58 | title: 'Polygon ZK-EVM',
59 | rpcUrl: 'https://zkevm-rpc.com/',
60 | explorerUrl: 'https://zkevm.polygonscan.com',
61 | explorerApiUrl: 'https://api-zkevm.polygonscan.com/api',
62 | emoji: '🟪',
63 | chainId: '1101',
64 | },
65 | linea: {
66 | title: 'Linea',
67 | rpcUrl: 'https://rpc.linea.build',
68 | explorerUrl: 'https://lineascan.build',
69 | explorerApiUrl: 'https://api.lineascan.build/api',
70 | emoji: '🟦',
71 | chainId: '59144',
72 | },
73 | mumbai: {
74 | title: 'Polygon Mumbai Testnet',
75 | rpcUrl: 'https://matic-mumbai.chainstacklabs.com/',
76 | explorerUrl: 'https://mumbai.polygonscan.com/',
77 | explorerApiUrl: 'https://api-testnet.polygonscan.com/api',
78 | emoji: '🧪',
79 | chainId: '80001',
80 | txHistorySupported: true,
81 | },
82 | binance: {
83 | title: 'Binance Smart Chain',
84 | rpcUrl: 'https://bsc-dataseed.binance.org/',
85 | explorerUrl: 'https://bscscan.com',
86 | explorerApiUrl: 'https://api.bscscan.com/api',
87 | emoji: '🟠',
88 | chainId: '56',
89 | txHistorySupported: true,
90 | },
91 | binance_testnet: {
92 | title: 'Binance Smart Chain Testnet',
93 | rpcUrl: 'https://data-seed-prebsc-1-s1.binance.org:8545/',
94 | explorerUrl: 'https://testnet.bscscan.com/',
95 | explorerApiUrl: 'https://api-testnet.bscscan.com/api',
96 | emoji: '🟠',
97 | chainId: '97',
98 | },
99 | avalanche: {
100 | title: 'Avalanche',
101 | rpcUrl: 'https://api.avax.network/ext/bc/C/rpc',
102 | explorerUrl: 'https://snowtrace.io',
103 | explorerApiUrl: 'https://api.snowtrace.io/api',
104 | emoji: '⛰️',
105 | chainId: '43114',
106 | txHistorySupported: true,
107 | },
108 | fuji: {
109 | title: 'Avalanche Fuji Testnet',
110 | rpcUrl: 'https://api.avax-test.network/ext/bc/C/rpc',
111 | explorerUrl: 'https://testnet.snowtrace.io',
112 | explorerApiUrl: 'https://api-testnet.snowtrace.io/api',
113 | emoji: '🧪',
114 | chainId: '43113',
115 | txHistorySupported: true,
116 | },
117 | optimism: {
118 | title: 'Optimism',
119 | rpcUrl: 'https://mainnet.optimism.io',
120 | explorerUrl: 'https://optimistic.etherscan.io/',
121 | explorerApiUrl: 'https://api-optimistic.etherscan.io/api',
122 | emoji: '🔴',
123 | chainId: '10',
124 | txHistorySupported: true,
125 | },
126 | optimism_goerli: {
127 | title: 'Optimism Goerli Testnet',
128 | rpcUrl: 'https://goerli.optimism.io',
129 | explorerUrl: 'https://goerli-optimism.etherscan.io/',
130 | explorerApiUrl: 'https://api-goerli-optimistic.etherscan.io/',
131 | emoji: '🧪',
132 | chainId: '420',
133 | },
134 | arbitrum: {
135 | title: 'Arbitrum',
136 | rpcUrl: 'https://arb1.arbitrum.io/rpc',
137 | explorerUrl: 'https://arbiscan.io/',
138 | explorerApiUrl: 'https://api.arbiscan.io/api',
139 | emoji: '🔵',
140 | chainId: '42161',
141 | txHistorySupported: true,
142 | },
143 | arbitrum_goerli: {
144 | title: 'Arbitrum Goerli',
145 | rpcUrl: 'https://goerli-rollup.arbitrum.io/rpc',
146 | explorerUrl: 'https://goerli.arbiscan.io/',
147 | explorerApiUrl: 'https://api-goerli.arbiscan.io/api',
148 | emoji: '🧪',
149 | chainId: '421613',
150 | },
151 | arbitrum_sepolia: {
152 | title: 'Arbitrum Sepolia',
153 | rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc',
154 | explorerUrl: 'https://sepolia-explorer.arbitrum.io/',
155 | explorerApiUrl: 'https://sepolia-explorer.arbitrum.io/api',
156 | emoji: '🧪',
157 | chainId: '421614',
158 | },
159 | arbitrum_nova: {
160 | title: 'Arbitrum Nova',
161 | rpcUrl: 'https://nova.arbitrum.io/rpc',
162 | explorerUrl: 'https://nova.arbiscan.io',
163 | explorerApiUrl: 'https://api-nova.arbiscan.io/api',
164 | emoji: '🔵',
165 | chainId: '42170',
166 | },
167 | moonbase: {
168 | title: 'Moonbase Alpha',
169 | rpcUrl: 'https://rpc.api.moonbase.moonbeam.network',
170 | explorerUrl: 'https://moonbase.moonscan.io/',
171 | explorerApiUrl: 'https://api-moonbase.moonscan.io/api',
172 | emoji: '🌕',
173 | chainId: '1287',
174 | },
175 | moonriver: {
176 | title: 'Moonriver',
177 | rpcUrl: 'https://rpc.moonriver.moonbeam.network',
178 | explorerUrl: 'https://moonriver.moonscan.io/',
179 | explorerApiUrl: 'https://api-moonriver.moonscan.io/api',
180 | emoji: '🌕',
181 | chainId: '1285',
182 | txHistorySupported: true,
183 | },
184 | moonbeam: {
185 | title: 'Moonbeam',
186 | rpcUrl: 'https://rpc.api.moonbeam.network',
187 | explorerUrl: 'https://moonbeam.moonscan.io/',
188 | explorerApiUrl: 'https://api-moonbeam.moonscan.io/api',
189 | emoji: '🌕',
190 | chainId: '1284',
191 | txHistorySupported: true,
192 | },
193 | celo: {
194 | title: 'Celo',
195 | rpcUrl: 'https://forno.celo.org',
196 | explorerUrl: 'https://explorer.celo.org/',
197 | explorerApiUrl: 'https://explorer.celo.org/api',
198 | emoji: '🟩',
199 | chainId: '42220',
200 | txHistorySupported: true,
201 | },
202 | fantom: {
203 | title: 'Fantom',
204 | rpcUrl: 'https://rpc.ftm.tools/',
205 | explorerUrl: 'https://ftmscan.com/',
206 | explorerApiUrl: 'https://api.ftmscan.com/api',
207 | emoji: '👻',
208 | chainId: '250',
209 | txHistorySupported: true,
210 | },
211 | fantom_testnet: {
212 | title: 'Fantom Testnet',
213 | rpcUrl: 'https://rpc.testnet.fantom.network/',
214 | explorerUrl: 'https://testnet.ftmscan.com/',
215 | explorerApiUrl: 'https://api-testnet.ftmscan.com/api',
216 | emoji: '🧪',
217 | chainId: '4002',
218 | },
219 | boba: {
220 | title: 'Boba',
221 | rpcUrl: 'https://mainnet.boba.network',
222 | explorerUrl: 'https://blockexplorer.boba.network',
223 | explorerApiUrl: 'https://blockexplorer.boba.network/api',
224 | emoji: '🧋',
225 | chainId: '288',
226 | },
227 | okex: {
228 | title: 'OKEx',
229 | rpcUrl: 'https://exchainrpc.okex.org',
230 | explorerUrl: 'https://www.oklink.com/en/okc',
231 | explorerApiUrl: '',
232 | emoji: '🔵',
233 | chainId: '66',
234 | },
235 | fuse: {
236 | title: 'Fuse',
237 | rpcUrl: 'https://rpc.fuse.io',
238 | explorerUrl: 'https://explorer.fuse.io/',
239 | explorerApiUrl: 'https://explorer.fuse.io/api',
240 | emoji: '🧨',
241 | chainId: '122',
242 | },
243 | harmony: {
244 | title: 'Harmony',
245 | rpcUrl: 'https://rpc.hermesdefi.io',
246 | explorerUrl: 'https://explorer.harmony.one',
247 | explorerApiUrl: '',
248 | emoji: '1️⃣',
249 | chainId: '1666600000',
250 | },
251 | aurora: {
252 | title: 'Aurora',
253 | rpcUrl: 'https://mainnet.aurora.dev',
254 | explorerUrl: 'https://explorer.aurora.dev',
255 | explorerApiUrl: 'https://explorer.mainnet.aurora.dev/api',
256 | emoji: '🅰️',
257 | chainId: '1313161554',
258 | },
259 | aurora_testnet: {
260 | title: 'Aurora Testnet',
261 | rpcUrl: 'https://betanet.aurora.dev',
262 | explorerUrl: 'https://explorer.testnet.aurora.dev',
263 | explorerApiUrl: 'https://explorer.testnet.aurora.dev/api',
264 | emoji: '🧪',
265 | chainId: '1313161556',
266 | },
267 | metis: {
268 | title: 'METIS',
269 | rpcUrl: 'https://andromeda.metis.io/?owner=1088',
270 | explorerUrl: 'https://andromeda-explorer.metis.io',
271 | explorerApiUrl: 'https://andromeda-explorer.metis.io/api',
272 | emoji: '🟩',
273 | chainId: '1088',
274 | },
275 | ewc: {
276 | title: 'Energy Web Chain',
277 | rpcUrl: 'https://rpc.energyweb.org',
278 | explorerUrl: 'http://explorer.energyweb.org',
279 | explorerApiUrl: 'https://explorer.energyweb.org/api',
280 | emoji: '⚡',
281 | chainId: '246',
282 | },
283 | volta: {
284 | title: 'Volta - Energy Web Testnet',
285 | rpcUrl: 'https://volta-rpc.energyweb.org',
286 | explorerUrl: 'http://volta-explorer.energyweb.org',
287 | explorerApiUrl: 'https://volta-explorer.energyweb.org/api',
288 | emoji: '⚡',
289 | chainId: '73799',
290 | },
291 | telos: {
292 | title: 'Telos',
293 | rpcUrl: 'https://mainnet.telos.net/evm',
294 | explorerUrl: 'https://www.teloscan.io/',
295 | explorerApiUrl: '',
296 | emoji: '🔵',
297 | chainId: '40',
298 | },
299 | telos_testnet: {
300 | title: 'Telos Testnet',
301 | rpcUrl: 'https://testnet.telos.net/evm',
302 | explorerUrl: 'https://testnet.teloscan.io/',
303 | explorerApiUrl: '',
304 | emoji: '🧪',
305 | chainId: '41',
306 | },
307 | cronos: {
308 | title: 'Cronos',
309 | rpcUrl: 'https://evm.cronos.org',
310 | explorerUrl: 'https://cronoscan.com/',
311 | explorerApiUrl: 'api.cronoscan.com/api',
312 | emoji: '⏱️',
313 | chainId: '25',
314 | },
315 | cronos_testnet: {
316 | title: 'Cronos Testnet',
317 | rpcUrl: 'https://evm-t3.cronos.org',
318 | explorerUrl: 'https://testnet.cronoscan.com/',
319 | explorerApiUrl: 'api-testnet.cronoscan.com/api',
320 | emoji: '⏱️',
321 | chainId: '338',
322 | },
323 | skale_testnet: {
324 | title: 'Skale Testnet',
325 | rpcUrl: 'https://staging-v3.skalenodes.com/v1/staging-aware-chief-gianfar',
326 | explorerUrl: 'https://staging-aware-chief-gianfar.explorer.staging-v3.skalenodes.com/',
327 | explorerApiUrl: 'https://staging-aware-chief-gianfar.explorer.staging-v3.skalenodes.com/api',
328 | emoji: '⚙️',
329 | chainId: '0x5a79c44e',
330 | },
331 | base: {
332 | title: 'Base',
333 | rpcUrl: 'https://mainnet.base.org',
334 | explorerUrl: 'https://basescan.org',
335 | explorerApiUrl: 'https://api.basescan.org/api',
336 | emoji: '🔵',
337 | chainId: '8453',
338 | },
339 | base_testnet: {
340 | title: 'Base Testnet',
341 | rpcUrl: 'https://goerli.base.org',
342 | explorerUrl: 'https://goerli.basescan.org',
343 | explorerApiUrl: 'https://api-goerli.basescan.org/api',
344 | emoji: '🟦',
345 | chainId: '84531',
346 | },
347 | scroll_alpha_testnet: {
348 | title: 'Scroll Alpha',
349 | rpcUrl: 'https://alpha-rpc.scroll.io/l2',
350 | explorerUrl: 'https://alpha-blockscout.scroll.io',
351 | explorerApiUrl: 'https://alpha-blockscout.scroll.io/api',
352 | emoji: '📜',
353 | chainId: '534353',
354 | },
355 | scroll_sepolia_testnet: {
356 | title: 'Scroll Sepolia',
357 | rpcUrl: 'https://sepolia-rpc.scroll.io',
358 | explorerUrl: 'https://sepolia-blockscout.scroll.io',
359 | explorerApiUrl: 'https://sepolia-blockscout.scroll.io/api',
360 | emoji: '📜',
361 | chainId: '534351',
362 | },
363 | linea_testnet: {
364 | title: 'Linea Testnet',
365 | rpcUrl: 'https://rpc.goerli.linea.build',
366 | explorerUrl: 'https://explorer.goerli.linea.build',
367 | explorerApiUrl: 'https://explorer.goerli.linea.build/api',
368 | emoji: '⚫',
369 | chainId: '59140',
370 | },
371 | xdc_mainnet: {
372 | title: 'XDC Mainnet',
373 | rpcUrl: 'https://rpc.xinfin.network',
374 | explorerUrl: 'https://xdc.blocksscan.io',
375 | explorerApiUrl: 'https://xdc.blocksscan.io/api',
376 | emoji: '🐱🏍',
377 | chainId: '50',
378 | },
379 | xdc_apothem_testnet: {
380 | title: 'Apothem XDC Testnet',
381 | rpcUrl: 'https://apothem.xdcrpc.com',
382 | explorerUrl: 'https://apothem.blocksscan.io',
383 | explorerApiUrl: 'https://apothem.blocksscan.io/api',
384 | emoji: '🕸',
385 | chainId: '51',
386 | },
387 | kava_mainnet: {
388 | title: 'Kava EVM Co-Chain',
389 | rpcUrl: 'https://evm.kava.io',
390 | explorerUrl: 'https://explorer.kava.io/',
391 | explorerApiUrl: 'https://explorer.kava.io/api',
392 | emoji: '💥',
393 | chainId: '2222',
394 | },
395 | kava_testnet: {
396 | title: 'Kava EVM Testnet',
397 | rpcUrl: 'https://evm.testnet.kava.io',
398 | explorerUrl: 'https://explorer.testnet.kava.io',
399 | explorerApiUrl: 'https://explorer.testnet.kava.io/api',
400 | emoji: '💥',
401 | chainId: '2221',
402 | },
403 | velas: {
404 | title: 'Velas',
405 | rpcUrl: 'https://explorer.velas.com/rpc',
406 | explorerUrl: 'https://explorer.velas.com',
407 | explorerApiUrl: 'https://explorer.velas.com/api',
408 | emoji: '🔻',
409 | chainId: '106',
410 | },
411 | zksync: {
412 | title: 'zkSync',
413 | rpcUrl: 'https://mainnet.era.zksync.io',
414 | explorerUrl: 'https://explorer.zksync.io/',
415 | explorerApiUrl: '',
416 | emoji: '🔄',
417 | chainId: '324',
418 | },
419 | }
420 |
--------------------------------------------------------------------------------
/src/lib/services/contractReader.ts:
--------------------------------------------------------------------------------
1 | import axios from 'redaxios'
2 | import type { FunctionFragment } from 'ethers/lib/utils'
3 |
4 | export default class ContractReader {
5 | address: string
6 | network: string
7 |
8 | constructor(address: string, network: string) {
9 | this.address = address
10 | this.network = network
11 | }
12 |
13 | read = async (fragment: FunctionFragment, args: unknown[]) => {
14 | return await axios.post('/api/readContract', {
15 | address: this.address,
16 | network: this.network,
17 | fragment: fragment.format('json'),
18 | args: args,
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/services/diamond.ts:
--------------------------------------------------------------------------------
1 | import type { Diamond, Method, Facet, LouperEvent } from '../../types/entities'
2 | import { FunctionFragment, ethers } from 'ethers'
3 | import { getFacetMethods, getABIMethods } from '$lib/utils'
4 |
5 | type FetchFunction = (info: RequestInfo, init?: RequestInit) => Promise
6 |
7 | export default class DiamondContract implements Diamond {
8 | address = ''
9 | network = ''
10 | name = ''
11 | facets: Facet[] = []
12 | selectors: string[] = []
13 | events: LouperEvent[] = []
14 | isFinal = true
15 | isVerified = true
16 | facetsToName: Record = {}
17 | facetsToSelectors: Map = new Map()
18 | fetch: FetchFunction
19 | abi = []
20 | functionSelectorsTemp = []
21 | eventSignaturesTemp = []
22 |
23 | constructor(address: string, network: string, fetch: FetchFunction) {
24 | this.address = address
25 | this.network = network
26 | this.fetch = fetch
27 | }
28 |
29 | fetchContractDetails = async (): Promise => {
30 | // Fetch contract info
31 | let res = await this.fetch('/api/contract', {
32 | method: 'POST',
33 | headers: { 'Content-Type': 'application/json' },
34 | body: JSON.stringify({ address: this.address, network: this.network }),
35 | })
36 | const diamond = await res.json()
37 | this.name = diamond.name
38 |
39 | // Fetch facets and facet details
40 | res = await this.fetch('/api/facets', {
41 | method: 'POST',
42 | headers: { 'Content-Type': 'application/json' },
43 | body: JSON.stringify({ address: this.address, network: this.network }),
44 | })
45 | const facets = await res.json()
46 |
47 | for (let i = 0; i < facets.length; i++) {
48 | this.facetsToSelectors[facets[i][0]] = facets[i][1]
49 | this.selectors = this.selectors.concat(facets[i][1])
50 | res = await this.fetch('/api/contract', {
51 | method: 'POST',
52 | headers: { 'Content-Type': 'application/json' },
53 | body: JSON.stringify({ address: facets[i][0], network: this.network }),
54 | })
55 | const facetData = await res.json()
56 |
57 | let methods: Method[] = []
58 | if (!facetData.abi.length) {
59 | this.isVerified = false
60 | for (let j = 0; j < facets[i][1].length; j++) {
61 | const selector = String(facets[i][1][j])
62 |
63 | if (!this.selectors.includes(selector)) continue
64 |
65 | let signature = 'UNKNOWN'
66 | try {
67 | // get info from 4bytes
68 | console.log('Fetching selector info from sig.eth.samczsun.com...')
69 | res = await this.fetch(
70 | `https://sig.eth.samczsun.com/api/v1/signatures?function=${selector}`,
71 | {
72 | method: 'GET',
73 | headers: { 'Content-Type': 'application/json' },
74 | },
75 | )
76 | console.log('Fetched info from sig.eth.samczsun.com.')
77 | let data
78 |
79 | if (res.ok) {
80 | try {
81 | data = await res.json()
82 | } catch (e) {
83 | console.log('Could not parse data')
84 | }
85 | }
86 |
87 | if (
88 | data &&
89 | data.result &&
90 | data.result.function[selector] &&
91 | data.result.function[selector].length
92 | ) {
93 | signature = data.result.function[selector][0].name
94 | }
95 |
96 | if (selector === '0x1f931c1c') {
97 | this.isFinal = false
98 | }
99 | } catch (e) {
100 | console.log(e)
101 | }
102 |
103 | methods.push({
104 | selector,
105 | signature,
106 | fragment: undefined,
107 | })
108 | }
109 | } else {
110 | facetData.abi.forEach((abi) => {
111 | const functionMethods = getFacetMethods(this.address, [abi])
112 | // Method should be an active selector
113 | if (functionMethods.length > 0 && this.selectors.includes(functionMethods[0].selector)) {
114 | // New facets will contain already existing selectors, prevent those from being duplicated
115 | if (!this.functionSelectorsTemp.includes(functionMethods[0].selector)) {
116 | this.abi.push(abi)
117 | // A Diamond allows duplicate names (but not selectors), so we can't use the ABI name
118 | this.functionSelectorsTemp.push(functionMethods[0].selector)
119 | }
120 | }
121 |
122 | if (abi.type === 'event') {
123 | const abiMethods = getABIMethods(this.address, [abi])
124 | if (
125 | abiMethods.length > 0 &&
126 | !this.eventSignaturesTemp.includes(abiMethods[0].signature)
127 | ) {
128 | this.abi.push(abi)
129 | this.eventSignaturesTemp.push(abiMethods[0].signature)
130 | }
131 | }
132 | })
133 | methods = await this.getMethods(facets[i][0], facetData.abi)
134 | }
135 | const name = facetData.name
136 | const facet: Facet = {
137 | address: facets[i][0].toLowerCase(),
138 | name,
139 | methods,
140 | }
141 | this.facets.push(facet)
142 | this.facetsToName[facet.address] = facet.name
143 | }
144 |
145 | // Fetch diamond events
146 | try {
147 | res = await this.fetch('/api/events', {
148 | method: 'POST',
149 | headers: { 'Content-Type': 'application/json' },
150 | body: JSON.stringify({ address: this.address, network: this.network }),
151 | })
152 | this.events = await res.json()
153 |
154 | await this.fetch('/api/leaderboard', {
155 | method: 'POST',
156 | headers: { 'Content-Type': 'application/json' },
157 | body: JSON.stringify({
158 | address: this.address,
159 | network: this.network,
160 | name: this.name || 'Unknown',
161 | }),
162 | })
163 | } catch (e) {
164 | console.error(e)
165 | this.events = []
166 | }
167 |
168 | return this
169 | }
170 |
171 | private getMethods = async (address: string, abi: any): Promise => {
172 | const contract = new ethers.Contract(address, abi)
173 |
174 | const methods: Method[] = []
175 | for (const f of contract.interface.fragments.filter(
176 | (f) => f.type === 'function',
177 | ) as FunctionFragment[]) {
178 | const func = f.format('minimal').replace('function ', '')
179 | if (f.selector === '0x1f931c1c') {
180 | // diamondCut
181 | this.isFinal = false
182 | }
183 |
184 | if (!this.facetsToSelectors[address].includes(f.selector)) continue
185 |
186 | const method: Method = {
187 | signature: func,
188 | selector: f.selector,
189 | fragment: f,
190 | }
191 |
192 | methods.push(method)
193 | }
194 |
195 | console.log(methods)
196 | return methods
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/lib/stores/navigationState.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store'
2 |
3 | type NavigationState = 'loading' | 'loaded' | null
4 |
5 | export default writable(null)
6 |
--------------------------------------------------------------------------------
/src/lib/stores/orbis.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store'
2 | import { Orbis } from '@orbisclub/orbis-sdk'
3 |
4 | const orbis = new Orbis()
5 |
6 | export default writable(orbis)
7 |
--------------------------------------------------------------------------------
/src/lib/stores/user.ts:
--------------------------------------------------------------------------------
1 | import { writable } from 'svelte/store'
2 |
3 | export default writable(null)
4 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { NETWORKS } from './config'
2 | import { FunctionFragment, ethers } from 'ethers'
3 | import type { Method } from '../types/entities'
4 |
5 | export const getExplorerAddressUrl = (address: string, network = 'mainnet'): string => {
6 | return `${NETWORKS[network].explorerUrl}/address/${address}`
7 | }
8 |
9 | export const getExplorerTxUrl = (hash: string, network: string): string => {
10 | return `${NETWORKS[network].explorerUrl}/tx/${hash}`
11 | }
12 |
13 | export const getVerifyContractUrl = (address: string, network = 'mainnet'): string => {
14 | return `${NETWORKS[network].explorerUrl}/verifyContract?a=${address}`
15 | }
16 |
17 | export const getEtherscanApiKey = (network: string): string => {
18 | const key = process.env[`${network.toUpperCase()}_ETHERSCAN_API_KEY`]
19 | console.log(key)
20 | return key ? key : process.env.ETHERSCAN_API_KEY
21 | }
22 |
23 | export const getFacetMethods = (address: string, abi: any): Method[] => {
24 | const contract = new ethers.Contract(address, abi)
25 |
26 | const methods: Method[] = []
27 | for (const f of contract.interface.fragments.filter(
28 | (f) => f.type === 'function',
29 | ) as FunctionFragment[]) {
30 | const func = f.format('minimal')
31 |
32 | const method: Method = {
33 | signature: func,
34 | selector: f.selector,
35 | fragment: f as FunctionFragment,
36 | }
37 | methods.push(method)
38 | }
39 | return methods
40 | }
41 |
42 | export const getABIMethods = (address: string, abi: any): Method[] => {
43 | const contract = new ethers.Contract(address, abi)
44 |
45 | const methods: Method[] = []
46 | for (const f of contract.interface.fragments.filter(
47 | (f) => f.type === 'function',
48 | ) as FunctionFragment[]) {
49 | const func = f.format('minimal')
50 |
51 | const method: Method = {
52 | signature: func,
53 | selector: f.selector,
54 | fragment: f,
55 | }
56 | methods.push(method)
57 | }
58 | return methods
59 | }
60 |
61 | export const shortProfile = (address: string) => {
62 | return `${address.substring(0, 5)}-${address.substring(address.length - 5)}`
63 | }
64 |
65 | export const shortAddress = (address: string) => {
66 | return `${address.substring(0, 8)}...${address.substring(address.length - 8)}`
67 | }
68 |
69 | export const shortHash = (address: string) => {
70 | return `${address.substring(0, 18)}...`
71 | }
72 |
73 | export const humanTime = (time: number): string => {
74 | const date = new Date()
75 | const timestamp = date.getTime()
76 | const seconds = Math.floor(timestamp / 1000)
77 | const difference = seconds - time
78 | let output = ``
79 | if (difference < 60) {
80 | // Less than a minute has passed:
81 | output = `${difference} seconds ago`
82 | } else if (difference < 3600) {
83 | // Less than an hour has passed:
84 | output = `${Math.floor(difference / 60)} minutes ago`
85 | } else if (difference < 86400) {
86 | // Less than a day has passed:
87 | output = `${Math.floor(difference / 3600)} hours ago`
88 | } else if (difference < 2620800) {
89 | // Less than a month has passed:
90 | output = `${Math.floor(difference / 86400)} days ago`
91 | } else if (difference < 31449600) {
92 | // Less than a year has passed:
93 | output = `${Math.floor(difference / 2620800)} months ago`
94 | } else {
95 | // More than a year has passed:
96 | output = `${Math.floor(difference / 31449600)} years ago`
97 | }
98 | return output
99 | }
100 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Oops! Could not load diamond
10 |
11 |
12 |
13 | Are you sure this is a diamond contract?
14 |
15 |
16 | Please make sure that the contract has a "DiamondLoupeFacet" that implements the "facets()"
17 | method.
18 |
19 |
20 |
Go home
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Louper - The Ethereum Diamond Inspector
40 |
41 |
42 |
43 | Louper
44 |
45 |
46 |
60 |
61 |
62 |
93 |
94 |
95 | {#if $navigationState === 'loading'}
96 |
97 | {:else}
98 |
99 | {/if}
100 |
101 |
102 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/routes/api/contract/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit'
2 | import { NETWORKS } from '$lib/config'
3 | import axios from 'redaxios'
4 | import type { RequestHandler } from '@sveltejs/kit'
5 | import { SUPABASE_URL, SUPABASE_KEY } from '$env/static/private'
6 |
7 | import { createClient } from '@supabase/supabase-js'
8 |
9 | // Create a single supabase client for interacting with your database
10 | const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
11 |
12 | export const POST = (async ({ request }) => {
13 | const body = await request.json()
14 | const network = body.network.toLowerCase() || 'mainnet'
15 | const address = body.address.toLowerCase()
16 |
17 | console.info(`Fetching data for 📝 contract at ${address} on ${network}`)
18 |
19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
20 | const res: any = await fetchCachedAbi(network, address)
21 | if (res.abi) {
22 | return json({
23 | abi: res.abi,
24 | name: res.name,
25 | })
26 | }
27 |
28 | try {
29 | const res = await axios.get(
30 | `https://anyabi.xyz/api/get-abi/${NETWORKS[network].chainId}/${address}`,
31 | )
32 | if (res.data.abi) {
33 | console.log(`ABI for ${res.data.name} fetched from AnyABI. Caching...`)
34 | await cacheAbi(network, address, res.data.name, res.data.abi)
35 | return json({
36 | abi: res.data.abi,
37 | name: res.data.name,
38 | })
39 | }
40 | } catch (e) {
41 | console.log('Nothing found on AnyABI.')
42 | }
43 |
44 | return json({
45 | name: '',
46 | abi: [],
47 | })
48 | }) satisfies RequestHandler
49 |
50 | const fetchCachedAbi = async (network: string, address: string) => {
51 | const { data, error } = await supabase
52 | .from('contracts')
53 | .select()
54 | .eq('network', network)
55 | .eq('address', address)
56 |
57 | if (error) {
58 | console.error(error)
59 | return error
60 | }
61 |
62 | if (!data.length) {
63 | console.log('No cached ABI. Fetching from AnyABI...')
64 | return false
65 | }
66 |
67 | console.log(`ABI for ${data[0].name} fetched from cache.`)
68 | return { abi: data[0].abi, name: data[0].name }
69 | }
70 |
71 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
72 | const cacheAbi = async (network: string, address: string, name: string, abi: any) => {
73 | const { error } = await supabase.from('contracts').insert([
74 | {
75 | network,
76 | address,
77 | name: name || '',
78 | abi,
79 | },
80 | ])
81 |
82 | if (error) {
83 | console.error(error)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/routes/api/events/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit'
2 | import { Result, ethers } from 'ethers'
3 | import { NETWORKS } from '$lib/config'
4 | import type { LouperEvent } from '../../../types/entities'
5 | import type { RequestHandler } from '@sveltejs/kit'
6 | import axios from 'redaxios'
7 | import { getEtherscanApiKey } from '$lib/utils'
8 |
9 | const abi = ['event DiamondCut(tuple(address,uint8,bytes4[])[],address,bytes)']
10 | const topic = '0x8faa70878671ccd212d20771b795c50af8fd3ff6cf27f4bde57e5d4de0aeb673'
11 |
12 | export const POST = (async ({ request }) => {
13 | const body = await request.json()
14 | console.info(`Fetching events for 💎 diamond at ${body.address} on ${body.network || 'mainnet'}`)
15 | const address = body.address.toLowerCase()
16 | const network = body.network.toLowerCase()
17 |
18 | const API_KEY = getEtherscanApiKey(network)
19 |
20 | const apiUrl = NETWORKS[network].explorerApiUrl
21 | if (apiUrl) {
22 | const fullUrl = `${apiUrl}?module=logs&action=getLogs&fromBlock=0&address=${address}&topic0=${topic}&apikey=${API_KEY}`
23 | console.log(fullUrl)
24 | const resp = await axios.get(fullUrl)
25 |
26 | const louperEvents: LouperEvent[] = []
27 |
28 | if (resp.data) {
29 | const iface = new ethers.Interface(abi)
30 | for (let i = 0; i < resp.data.result.length; i++) {
31 | const louperEvent: LouperEvent = {
32 | ...iface.decodeEventLog('DiamondCut', resp.data.result[i].data),
33 | timestamp: parseInt(resp.data.result[i].timeStamp, 16),
34 | txHash: resp.data.result[i].transactionHash as string,
35 | }
36 | louperEvents.push(louperEvent)
37 | }
38 | }
39 | return new Response(
40 | JSON.stringify(louperEvents, (_, v) => (typeof v === 'bigint' ? v.toString() : v)),
41 | )
42 | }
43 | }) satisfies RequestHandler
44 |
--------------------------------------------------------------------------------
/src/routes/api/facets/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit'
2 | import { ethers } from 'ethers'
3 | import { NETWORKS } from '$lib/config'
4 | import type { RequestHandler } from '@sveltejs/kit'
5 | import * as console from 'console'
6 |
7 | const abi = ['function facets() external view returns (tuple(address,bytes4[])[])']
8 |
9 | export const POST = (async ({ request }) => {
10 | const body = await request.json()
11 | console.info(`Fetching data for 💎 diamond at ${body.address} on ${body.network || 'mainnet'}`)
12 | const address = body.address.toLowerCase()
13 |
14 | const rpcUrl = body.network ? NETWORKS[body.network].rpcUrl : NETWORKS['mainnet'].rpcUrl
15 | const provider = new ethers.JsonRpcProvider(rpcUrl)
16 | const diamondContract = new ethers.Contract(address, abi, provider)
17 |
18 | try {
19 | const facets = await diamondContract.facets()
20 | console.log(facets)
21 | return json(facets)
22 | } catch (e) {
23 | console.error(e)
24 | return json([])
25 | }
26 | }) satisfies RequestHandler
27 |
--------------------------------------------------------------------------------
/src/routes/api/leaderboard/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit'
2 | import type { RequestHandler } from '@sveltejs/kit'
3 | import { createClient, type PostgrestError } from '@supabase/supabase-js'
4 | import { SUPABASE_URL, SUPABASE_KEY } from '$env/static/private'
5 |
6 | // Create a single supabase client for interacting with your database
7 | const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
8 |
9 | export const POST = (async ({ request }) => {
10 | const body = await request.json()
11 | const network = body.network.toLowerCase()
12 | const address = body.address.toLowerCase()
13 | const name = body.name
14 |
15 | let record = {
16 | network,
17 | address,
18 | name: name,
19 | hits: 1,
20 | }
21 |
22 | let { data, error } = await supabase
23 | .from('leaderboard')
24 | .select()
25 | .eq('network', network)
26 | .eq('address', address)
27 |
28 | if (error) {
29 | console.error(error)
30 | }
31 |
32 | if (data && data.length) {
33 | record = data[0]
34 | record.hits += 1
35 | }
36 |
37 | ; ({ data, error } = await supabase.from('leaderboard').upsert(record))
38 | if (error) {
39 | console.error(error)
40 | }
41 |
42 | return new Response('OK')
43 | }) satisfies RequestHandler
44 |
45 | export const GET = (async ({ url }) => {
46 | let data: Record[] = []
47 | let error: PostgrestError | null = null
48 |
49 | if (url.searchParams.get('ranked')) {
50 | ; ({ data, error } = await supabase
51 | .from('leaderboard')
52 | .select()
53 | .order('hits', { ascending: false })
54 | .limit(10))
55 | } else {
56 | ; ({ data, error } = await supabase
57 | .from('leaderboard')
58 | .select()
59 | .order('updated_at', { ascending: false })
60 | .limit(10))
61 | }
62 |
63 | if (error) {
64 | console.error(error)
65 | }
66 |
67 | let diamonds = []
68 | if (data && data.length) {
69 | diamonds = data
70 | }
71 |
72 | return json({
73 | diamonds,
74 | })
75 | }) satisfies RequestHandler
76 |
--------------------------------------------------------------------------------
/src/routes/api/readContract/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit'
2 | import { ethers } from 'ethers'
3 | import { NETWORKS } from '$lib/config'
4 | import type { RequestHandler } from '@sveltejs/kit'
5 |
6 | export const POST = (async ({ request }) => {
7 | const body = await request.json()
8 | console.info(
9 | `Reading contract data for 💎 diamond at ${body.address} on ${body.network || 'mainnet'}`,
10 | )
11 |
12 | const address = body.address.toLowerCase()
13 | const abi = []
14 | const fragment = JSON.parse(body.fragment)
15 | abi.push(fragment)
16 | const args = body.args
17 |
18 | const rpcUrl = body.network ? NETWORKS[body.network].rpcUrl : NETWORKS['mainnet'].rpcUrl
19 | const provider = new ethers.JsonRpcProvider(rpcUrl)
20 | const diamondContract = new ethers.Contract(address, abi, provider)
21 |
22 | try {
23 | const funcFragment = ethers.FunctionFragment.from(fragment)
24 | const method = funcFragment.format('minimal').split(' ')[1]
25 | console.log(method)
26 | const data = await diamondContract[method](...args)
27 | return json(data)
28 | } catch (e) {
29 | return json({
30 | reason: e.reason,
31 | code: e.code,
32 | value: e.value,
33 | })
34 | }
35 | }) satisfies RequestHandler
36 |
--------------------------------------------------------------------------------
/src/routes/api/stats/+server.ts:
--------------------------------------------------------------------------------
1 | import { json } from '@sveltejs/kit'
2 | import type { RequestHandler } from '@sveltejs/kit'
3 | import { createClient } from '@supabase/supabase-js'
4 | import axios from 'redaxios'
5 | import { SUPABASE_URL, SUPABASE_KEY, SIMPLE_ANALYTICS_API_KEY } from '$env/static/private'
6 |
7 | // Create a single supabase client for interacting with your database
8 | const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
9 | const SA_KEY = SIMPLE_ANALYTICS_API_KEY
10 |
11 | export const GET = (async () => {
12 | let { error, count } = await supabase
13 | .from('contracts')
14 | .select('name', { count: 'exact', head: true })
15 |
16 | if (error) {
17 | console.error(error)
18 | }
19 |
20 | let contractCount = 0
21 | if (count) {
22 | contractCount = count
23 | }
24 |
25 | ; ({ error, count } = await supabase
26 | .from('leaderboard')
27 | .select('name', { count: 'exact', head: true }))
28 |
29 | if (error) {
30 | console.error(error)
31 | }
32 |
33 | let diamondCount = 0
34 | if (count) {
35 | diamondCount = count
36 | }
37 |
38 | const res = await axios.get(
39 | 'https://simpleanalytics.com/louper.dev.json?version=5&fields=pageviews&end=today',
40 | {
41 | headers: {
42 | 'Api-key': SA_KEY,
43 | },
44 | },
45 | )
46 |
47 | let pageViews = 0
48 | if (res.data) {
49 | pageViews = res.data.pageviews
50 | }
51 |
52 | return json({
53 | contractCount,
54 | diamondCount,
55 | pageViews,
56 | })
57 | }) satisfies RequestHandler
58 |
--------------------------------------------------------------------------------
/src/routes/api/transactions/+server.ts:
--------------------------------------------------------------------------------
1 | import { NETWORKS } from '$lib/config'
2 | import { getEtherscanApiKey } from '$lib/utils'
3 | import { json, type RequestHandler } from '@sveltejs/kit'
4 | import axios from 'redaxios'
5 |
6 | export const POST = (async ({ request }) => {
7 | const body = await request.json()
8 | const address = body.address.toLowerCase()
9 | const network = body.network.toLowerCase()
10 | const page = body.page || 1
11 |
12 | console.info(`Fetching transactions for 💎 diamond at ${address} on ${network || 'mainnet'}`)
13 |
14 | const API_KEY = getEtherscanApiKey(network)
15 |
16 | const apiUrl = NETWORKS[network].explorerApiUrl
17 | if (apiUrl) {
18 | const fullUrl = `${apiUrl}?module=account&action=txlist&page=${page}&offset=50&&sort=desc&address=${address}&apikey=${API_KEY}`
19 | const res = await axios.get(fullUrl)
20 | return json(res.data.result)
21 | }
22 |
23 | return json([])
24 | }) satisfies RequestHandler
25 |
--------------------------------------------------------------------------------
/src/routes/bookmarks/+page.js:
--------------------------------------------------------------------------------
1 | import user from '$lib/stores/user'
2 | import { redirect } from '@sveltejs/kit'
3 |
4 | export async function load() {
5 | let userValue
6 |
7 | user.subscribe((u) => {
8 | userValue = u
9 | })
10 |
11 | if (!userValue) {
12 | throw redirect(307, '/')
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/routes/bookmarks/+page.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 |
30 |
Bookmarked Diamonds
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Name
40 | Address
41 | Network
42 |
43 |
44 |
45 | {#each bookmarks as diamond}
46 | goto(`/diamond/${diamond.address}?network=${diamond.network}`)}
49 | >
50 | {diamond.name.substring(0, 25)}
51 | {diamond.address.substring(0, 20)}...{diamond.address.substring(36)}
52 |
53 | {NETWORKS[diamond.network].emoji}
54 | {NETWORKS[diamond.network].title}
55 |
56 |
57 | {/each}
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/routes/diamond/[address]/+page.svelte:
--------------------------------------------------------------------------------
1 |
52 |
53 |
54 | Louper - {diamond.name || 'UNKNOWN'}
55 |
56 |
57 |
58 |
59 |
60 |
{diamond.name || 'UNKNOWN'}
61 |
62 |
window.open(getExplorerAddressUrl(diamond.address, diamond.network))}
65 | >
66 | {diamond.address}
67 |
73 |
76 |
79 |
80 |
81 |
{
84 | await navigator.clipboard.writeText(`${diamond.address}`)
85 | alert('Address copied!')
86 | }}
87 | >
88 |
97 |
102 |
103 |
104 |
105 | {#if !showReadContract && !showWriteContract && !showAddFacet && !showRemoveFacet}
106 |
107 | {#if diamond.isFinal}
108 |
109 | Immutable
110 |
111 | {:else}
112 |
113 | Upgradable
114 |
115 | {/if}
116 |
162 |
163 | {#if NETWORKS[diamond.network].txHistorySupported || true}
164 |
165 | {/if}
166 |
171 |
172 | {#if diamond.facets.length === 0}
173 |
No facets found. This contract is not a proper diamond...
174 | {/if}
175 |
176 | {#each diamond.facets as facet}
177 |
185 | {/each}
186 |
187 | {/if}
188 |
194 |
200 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/src/routes/diamond/[address]/+page.ts:
--------------------------------------------------------------------------------
1 | import type { PageLoad } from './$types'
2 | import DiamondContract from '$lib/services/diamond'
3 | import { NETWORKS } from '$lib/config'
4 |
5 | export const load: PageLoad = async ({ params, url, fetch }) => {
6 | let network = url.searchParams.get('network') || 'mainnet'
7 |
8 | if (!Number.isNaN(Number(network))) {
9 | network = Object.entries(NETWORKS).find(([_, networkConfig]) => {
10 | return networkConfig.chainId.toString() === network.toString()
11 | })?.[0]
12 | }
13 |
14 | console.log('Fetching diamond details...', network, params.address)
15 | const diamond = await new DiamondContract(params.address, network, fetch).fetchContractDetails()
16 | console.log('Diamond details fetched...')
17 |
18 | return {
19 | diamond,
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/routes/sponsor/+page.svelte:
--------------------------------------------------------------------------------
1 |
43 |
--------------------------------------------------------------------------------
/src/types/entities.ts:
--------------------------------------------------------------------------------
1 | import type { Result } from 'ethers'
2 | import type { FunctionFragment } from 'ethers'
3 |
4 | export interface Method {
5 | signature: string
6 | selector: string
7 | fragment: FunctionFragment | undefined
8 | }
9 |
10 | export interface Facet {
11 | address: string
12 | name: string
13 | methods: Method[]
14 | }
15 |
16 | export interface LouperEvent extends Result {
17 | timestamp: number
18 | txHash: string
19 | }
20 |
21 | export interface Diamond {
22 | address: string
23 | network: string
24 | name: string
25 | facets: Facet[]
26 | events: LouperEvent[]
27 | isFinal: boolean
28 | isVerified: boolean
29 | }
30 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/favicon.ico
--------------------------------------------------------------------------------
/static/img/aavegotchi-mainnet-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/aavegotchi-mainnet-logo.png
--------------------------------------------------------------------------------
/static/img/aavegotchi-polygon-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/aavegotchi-polygon-logo.jpg
--------------------------------------------------------------------------------
/static/img/barnbridge-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/barnbridge-logo.jpg
--------------------------------------------------------------------------------
/static/img/beanstalk-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/beanstalk-logo.png
--------------------------------------------------------------------------------
/static/img/connext-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/connext-logo.png
--------------------------------------------------------------------------------
/static/img/escabro-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/escabro-logo.png
--------------------------------------------------------------------------------
/static/img/gelato-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/gelato-logo.png
--------------------------------------------------------------------------------
/static/img/lifi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/lifi.png
--------------------------------------------------------------------------------
/static/img/mark3labslogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/mark3labslogo.png
--------------------------------------------------------------------------------
/static/img/piedao-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/img/piedao-logo.png
--------------------------------------------------------------------------------
/static/img/quicknode-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/static/louper-logo-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/louper-logo-transparent.png
--------------------------------------------------------------------------------
/static/louper-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/louper-logo.png
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Louper",
3 | "iconPath": "louper-logo.svg",
4 | "description": "The Ethereum Diamond Inspector"
5 | }
6 |
--------------------------------------------------------------------------------
/static/thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mark3labs/louper-v2/fc8fb51f28599c7d922f240fffddaf57ab54e0e5/static/thumbnail.png
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working
2 | # directory name when running `supabase init`.
3 | project_id = "louper-v2"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = []
11 | # Extra schemas to add to the search_path of every request.
12 | extra_search_path = ["extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 1000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
21 | # server_version;` on the remote database to check.
22 | major_version = 14
23 |
24 | [studio]
25 | # Port to use for Supabase Studio.
26 | port = 54323
27 |
28 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
29 | # are monitored, and you can view the emails that would have been sent from the web interface.
30 | [inbucket]
31 | # Port to use for the email testing server web interface.
32 | port = 54324
33 |
34 | [auth]
35 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
36 | # in emails.
37 | site_url = "http://localhost:3000"
38 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
39 | additional_redirect_urls = ["https://localhost:3000"]
40 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
41 | # week).
42 | jwt_expiry = 3600
43 | # Allow/disallow new user signups to your project.
44 | enable_signup = true
45 |
46 | [auth.email]
47 | # Allow/disallow new user signups via email to your project.
48 | enable_signup = true
49 | # If enabled, a user will be required to confirm any email change on both the old, and new email
50 | # addresses. If disabled, only the new email is required to confirm.
51 | double_confirm_changes = true
52 | # If enabled, users need to confirm their email address before signing in.
53 | enable_confirmations = false
54 |
55 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
56 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `twitch`, `twitter`, `slack`, `spotify`.
57 | [auth.external.apple]
58 | enabled = false
59 | client_id = ""
60 | secret = ""
61 |
--------------------------------------------------------------------------------
/supabase/migrations/20220601172756_add_contracts_table.sql:
--------------------------------------------------------------------------------
1 | -- This script was generated by the Schema Diff utility in pgAdmin 4
2 | -- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated
3 | -- and may require manual changes to the script to ensure changes are applied in the correct order.
4 | -- Please report an issue for any failure with the reproduction steps.
5 |
6 | CREATE SEQUENCE contracts_id_seq
7 | START WITH 1
8 | INCREMENT BY 1
9 | NO MINVALUE
10 | NO MAXVALUE
11 | CACHE 1;
12 |
13 | CREATE TABLE IF NOT EXISTS public.contracts
14 | (
15 | id integer NOT NULL DEFAULT nextval('contracts_id_seq'::regclass),
16 | network character varying(15) COLLATE pg_catalog."default" NOT NULL,
17 | address character varying(42) COLLATE pg_catalog."default" NOT NULL,
18 | name character varying(150) COLLATE pg_catalog."default" NOT NULL,
19 | abi json NOT NULL,
20 | CONSTRAINT contracts_pkey PRIMARY KEY (id)
21 | )
22 |
23 | TABLESPACE pg_default;
24 |
25 | ALTER TABLE IF EXISTS public.contracts
26 | OWNER to postgres;
27 |
28 | GRANT ALL ON TABLE public.contracts TO anon;
29 |
30 | GRANT ALL ON TABLE public.contracts TO authenticated;
31 |
32 | GRANT ALL ON TABLE public.contracts TO postgres;
33 |
34 | GRANT ALL ON TABLE public.contracts TO service_role;
35 |
--------------------------------------------------------------------------------
/supabase/migrations/20220805092229_add_leaderboard.sql:
--------------------------------------------------------------------------------
1 | -- This script was generated by the Schema Diff utility in pgAdmin 4
2 | -- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated
3 | -- and may require manual changes to the script to ensure changes are applied in the correct order.
4 | -- Please report an issue for any failure with the reproduction steps.
5 |
6 | CREATE SEQUENCE leaderboard_id_seq
7 | START WITH 1
8 | INCREMENT BY 1
9 | NO MINVALUE
10 | NO MAXVALUE
11 | CACHE 1;
12 |
13 | CREATE TABLE IF NOT EXISTS public.leaderboard
14 | (
15 | id integer NOT NULL DEFAULT nextval('leaderboard_id_seq'::regclass),
16 | network character varying(15) COLLATE pg_catalog."default" NOT NULL,
17 | address character varying(42) COLLATE pg_catalog."default" NOT NULL,
18 | name character varying(150) COLLATE pg_catalog."default" NOT NULL,
19 | hits numeric NOT NULL,
20 | updated_at timestamp NOT NULL DEFAULT now(),
21 | CONSTRAINT leaderboard_pkey PRIMARY KEY (id)
22 | )
23 |
24 | TABLESPACE pg_default;
25 |
26 | ALTER TABLE IF EXISTS public.leaderboard
27 | OWNER to postgres;
28 |
29 | GRANT ALL ON TABLE public.leaderboard TO anon;
30 |
31 | GRANT ALL ON TABLE public.leaderboard TO authenticated;
32 |
33 | GRANT ALL ON TABLE public.leaderboard TO postgres;
34 |
35 | GRANT ALL ON TABLE public.leaderboard TO service_role;
36 |
37 | create extension if not exists moddatetime schema extensions;
38 |
39 | -- assuming the table name is "todos", and a timestamp column "updated_at"
40 | -- this trigger will set the "updated_at" column to the current timestamp for every update
41 | create trigger handle_updated_at before update on leaderboard
42 | for each row execute procedure moddatetime (updated_at);
43 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import preprocess from 'svelte-preprocess'
2 | import vercel from '@sveltejs/adapter-vercel'
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | // Consult https://github.com/sveltejs/svelte-preprocess
7 | // for more information about preprocessors
8 | preprocess: [
9 | preprocess({
10 | postcss: true,
11 | }),
12 | ],
13 |
14 | kit: {
15 | // hydrate the element in src/app.html
16 | adapter: vercel(),
17 | },
18 | }
19 |
20 | export default config
21 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | content: ['./src/**/*.{html,js,svelte,ts}'],
3 |
4 | theme: {
5 | extend: {},
6 | },
7 |
8 | plugins: [require('@tailwindcss/typography'), require('daisyui')],
9 | }
10 |
11 | module.exports = config
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json"
3 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite'
2 | import { defineConfig } from 'vite'
3 | import { nodePolyfills } from 'vite-plugin-node-polyfills'
4 |
5 | export default defineConfig({
6 | plugins: [nodePolyfills(), sveltekit()],
7 | })
8 |
--------------------------------------------------------------------------------