forked from otterscan/otterscan
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add initial inline address labeling tool
This tool uses localStorage to store address label changes and has support for loading labels from external lists once a UI is created for it.
- Loading branch information
Showing
5 changed files
with
228 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { JsonRpcApiProvider } from "ethers"; | ||
import { BasicAddressResolver } from "./address-resolver"; | ||
|
||
type AddressMap = Record<string, string | undefined>; | ||
|
||
/* | ||
A singleton class so addresses aren't fetched more than once | ||
*/ | ||
export class CustomLabelFetcher { | ||
private static instance: CustomLabelFetcher; | ||
private fetchedLabels: Map<string, string> = new Map(); | ||
private localStorageLabels: Map<string, string> = new Map(); | ||
private fetched: boolean = false; | ||
// List of URLs from which address-label mappings are fetched | ||
// TODO: Potentially populate from the config file | ||
private defaultLabelSources: string[] = []; | ||
|
||
private constructor() {} | ||
|
||
public static getInstance(): CustomLabelFetcher { | ||
if (!CustomLabelFetcher.instance) { | ||
CustomLabelFetcher.instance = new CustomLabelFetcher(); | ||
} | ||
return CustomLabelFetcher.instance; | ||
} | ||
|
||
public async fetchLabels(localOnly: boolean = false) { | ||
// Fetch labels from label sources | ||
if (!localOnly) { | ||
const _this = this; | ||
async function fetchLabels(url: string) { | ||
try { | ||
const response = await fetch(url); | ||
const data = (await response.json()) as { | ||
[key: string]: string; | ||
}; | ||
Object.entries(data).forEach(([key, value]: [string, string]) => | ||
_this.fetchedLabels.set(key, value), | ||
); | ||
} catch (e) { | ||
console.error(`Error loading address labels from ${url}:`, e); | ||
} | ||
} | ||
|
||
await Promise.all( | ||
this.defaultLabelSources.map((url: string) => fetchLabels(url)), | ||
); | ||
} | ||
|
||
// Load labels from localStorage | ||
this.localStorageLabels.clear(); | ||
const localStorageAddrsString = localStorage.getItem("customAddressLabels"); | ||
if (typeof localStorageAddrsString === "string") { | ||
try { | ||
const localLabels = JSON.parse(localStorageAddrsString) as [ | ||
string, | ||
string, | ||
][]; | ||
for (let addressTag of localLabels) { | ||
this.localStorageLabels.set(addressTag[0], addressTag[1]); | ||
} | ||
} catch (e) { | ||
console.error(e); | ||
} | ||
} | ||
|
||
if (!localOnly) { | ||
this.fetched = true; | ||
} | ||
} | ||
|
||
public async updateLabels(newItem: { [address: string]: string }) { | ||
// Update our view of the localStorage addresses | ||
await this.fetchLabels(true); | ||
Object.entries(newItem).forEach(([key, value]) => { | ||
if (value === "") { | ||
this.localStorageLabels.delete(key); | ||
} else { | ||
this.localStorageLabels.set(key, value); | ||
} | ||
}); | ||
localStorage.setItem( | ||
"customAddressLabels", | ||
JSON.stringify([...this.localStorageLabels]), | ||
); | ||
} | ||
|
||
public async getItem(key: string): Promise<string | undefined> { | ||
if (!this.fetched) { | ||
await this.fetchLabels(); | ||
} | ||
// localStorage labels have priority | ||
if (this.localStorageLabels.has(key)) { | ||
return this.localStorageLabels.get(key); | ||
} else if (this.fetchedLabels.has(key)) { | ||
return this.fetchedLabels.get(key); | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
} | ||
|
||
export class CustomLabelResolver extends BasicAddressResolver { | ||
async resolveAddress( | ||
provider: JsonRpcApiProvider, | ||
address: string, | ||
): Promise<string | undefined> { | ||
const labelFetcher = CustomLabelFetcher.getInstance(); | ||
const label = await labelFetcher.getItem(address); | ||
return label; | ||
} | ||
|
||
trusted(resolvedAddress: string | undefined): boolean | undefined { | ||
return true; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"0x67b1d87101671b127f5f8714789C7192f7ad340e": "Erigon devnet address" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { faCheck, faTag } from "@fortawesome/free-solid-svg-icons"; | ||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||
import React, { FC, useEffect } from "react"; | ||
import { mutate } from "swr"; | ||
import { customLabelResolver } from "../../api/address-resolver"; | ||
import { CustomLabelFetcher } from "../../api/address-resolver/CustomLabelResolver"; | ||
import { AddressAwareComponentProps } from "../types"; | ||
|
||
type EditableAddressTagProps = AddressAwareComponentProps & { | ||
defaultTag: string | undefined; | ||
editedCallback?: (newLabel: string) => void; | ||
}; | ||
|
||
async function setAddressLabel(address: string, label: string | null) { | ||
if (label === null) { | ||
return; | ||
} | ||
const trimmedLabel = label.trim(); | ||
await CustomLabelFetcher.getInstance().updateLabels({ | ||
[address]: trimmedLabel, | ||
}); | ||
// Update the SWR entry so that all components using this label are invalidated | ||
mutate(address, [customLabelResolver, trimmedLabel]); | ||
} | ||
|
||
const EditableAddressTag: FC<EditableAddressTagProps> = ({ | ||
address, | ||
defaultTag, | ||
editedCallback, | ||
}) => { | ||
const inputRef = React.createRef<HTMLInputElement>(); | ||
const formRef = React.createRef<HTMLFormElement>(); | ||
|
||
useEffect(() => { | ||
if (inputRef.current) { | ||
inputRef.current.focus(); | ||
} | ||
}, []); | ||
|
||
return ( | ||
<form | ||
onSubmit={(event) => { | ||
event.preventDefault(); | ||
setAddressLabel( | ||
address, | ||
inputRef.current ? inputRef.current.value : null, | ||
); | ||
if (editedCallback && inputRef.current) { | ||
editedCallback(inputRef.current.value); | ||
} | ||
}} | ||
className="flex space-x-1 text-sm" | ||
ref={formRef} | ||
> | ||
<div className="rounded-lg bg-gray-200 px-2 py-1 text-sm text-gray-500 space-x-1"> | ||
<FontAwesomeIcon icon={faTag} size="1x" /> | ||
<input | ||
type="text" | ||
data-address={address} | ||
placeholder={defaultTag ?? "Address"} | ||
defaultValue={defaultTag} | ||
ref={inputRef} | ||
/> | ||
</div> | ||
<button | ||
className={`flex-no-wrap flex items-center justify-center space-x-1 self-center text-gray-500 ${"transition-shadows h-7 w-7 rounded-full bg-gray-200 text-xs transition-colors hover:bg-gray-500 hover:text-gray-200 hover:shadow"}`} | ||
title="Submit address label" | ||
type="submit" | ||
> | ||
<FontAwesomeIcon icon={faCheck} size="1x" /> | ||
</button> | ||
</form> | ||
); | ||
}; | ||
|
||
export default EditableAddressTag; |