mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-01 09:40:28 +08:00
199 lines
5.4 KiB
TypeScript
199 lines
5.4 KiB
TypeScript
import { ethers } from "ethers";
|
|
import type { BigNumber } from "ethers";
|
|
import multibase from 'multibase';
|
|
import type { Config } from '@app/config';
|
|
import { assert } from '@app/error';
|
|
|
|
export enum AddressType {
|
|
Contract,
|
|
Org,
|
|
Safe,
|
|
EOA,
|
|
}
|
|
|
|
export interface Safe {
|
|
address: string
|
|
owners: string[]
|
|
threshold: number
|
|
}
|
|
|
|
export function formatBalance(n: BigNumber) {
|
|
return ethers.utils.commify(parseFloat(ethers.utils.formatUnits(n)).toFixed(2));
|
|
}
|
|
|
|
export function formatAddress(addr: string) {
|
|
return formatHash(addr);
|
|
}
|
|
|
|
export function formatHash(hash: string) {
|
|
return hash.substring(0, 6)
|
|
+ '...'
|
|
+ hash.substring(hash.length - 4, hash.length);
|
|
}
|
|
|
|
export function capitalize(s: string) {
|
|
if (s === "") return s;
|
|
return s[0].toUpperCase() + s.substr(1);
|
|
}
|
|
|
|
// Takes a domain name, eg. 'cloudhead.radicle.eth' and
|
|
// returns the label, eg. 'cloudhead', otherwise `null`.
|
|
export function parseEnsLabel(name: string, config: Config): string {
|
|
let domain = config.registrar.domain.replace(".", "\\.");
|
|
let label = name.replace(new RegExp(`\\.${domain}$`), "");
|
|
|
|
return label;
|
|
}
|
|
|
|
// Return the current unix time.
|
|
export function unixTime(): number {
|
|
return Math.floor(Date.now() / 1000);
|
|
}
|
|
|
|
// Check whether the input is a Radicle ID.
|
|
export function isRadicleId(input: string): boolean {
|
|
return /^rad:[a-z]+:[a-zA-Z0-9]+$/.test(input);
|
|
}
|
|
|
|
// Check whether the input is a URL.
|
|
export function isUrl(input: string): boolean {
|
|
return /^https?:\/\//.test(input);
|
|
}
|
|
|
|
// Check whether the input is an Ethereum address.
|
|
export function isAddress(input: string): boolean {
|
|
return ethers.utils.isAddress(input);
|
|
}
|
|
|
|
// Get search parameters from location.
|
|
export function getSearchParam(key: string, location: RouteLocation): string | null {
|
|
let params = new URLSearchParams(location.search);
|
|
return params.get(key);
|
|
}
|
|
|
|
// Get the explorer link of an address, eg. Etherscan.
|
|
export function explorerLink(addr: string, config: Config): string {
|
|
if (config.network.name == "ropsten") {
|
|
return `https://ropsten.etherscan.io/address/${addr}`;
|
|
} else if (config.network.name == "rinkeby") {
|
|
return `https://rinkeby.etherscan.io/address/${addr}`;
|
|
}
|
|
return `https://etherscan.io/address/${addr}`;
|
|
}
|
|
|
|
// Get the Gnosis Safe link of an address, eg. Etherscan.
|
|
export function safeLink(addr: string, config: Config): string {
|
|
if (config.safe.viewer) {
|
|
return `${config.safe.viewer}/${addr}`;
|
|
}
|
|
return explorerLink(addr, config);
|
|
}
|
|
|
|
// Query a subgraph.
|
|
export async function querySubgraph(
|
|
query: string,
|
|
variables: Record<string, any>,
|
|
config: Config
|
|
): Promise<null | any> {
|
|
const response = await fetch(config.orgs.subgraph, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
query,
|
|
variables,
|
|
})
|
|
});
|
|
const json = await response.json();
|
|
|
|
if (json.errors) {
|
|
for (let e of json.errors) {
|
|
console.error("querySubgraph:", e.message);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return json.data;
|
|
}
|
|
|
|
// Create a Radicle ID from a root hash.
|
|
export function formatRadicleId(hash: Uint8Array): string {
|
|
// Remove any zero-padding from the byte array. SHA1 is 20 bytes long.
|
|
const sha1Bytes = 20;
|
|
const suffix = hash.slice(hash.length - sha1Bytes);
|
|
|
|
// Create a multihash by adding prefix 17 for SHA-1 and 20 for the hash length.
|
|
const multihash = new Uint8Array([17, 20, ...suffix]);
|
|
const payload = multibase.encode("base32z", multihash);
|
|
|
|
return `rad:git:${new TextDecoder().decode(payload)}`;
|
|
}
|
|
|
|
// Create a project hash from a hash and format.
|
|
export function formatProjectHash(hash: Uint8Array, format: number): string {
|
|
assert(format === 0x0, "Only SHA1 commit hashes are supported");
|
|
|
|
// Remove any zero-padding from the byte array. SHA1 is 20 bytes long.
|
|
const sha1Bytes = 20;
|
|
const suffix = hash.slice(hash.length - sha1Bytes);
|
|
return ethers.utils.hexlify(suffix).replace(/^0x/, '');
|
|
}
|
|
|
|
// Identify an address by checking whether it's a contract or an externally-owned address.
|
|
export async function identifyAddress(address: string, config: Config): Promise<AddressType> {
|
|
let safe = await getSafe(address, config);
|
|
if (safe) {
|
|
return AddressType.Safe;
|
|
}
|
|
|
|
let code = await config.provider.getCode(address);
|
|
let bytes = ethers.utils.arrayify(code);
|
|
|
|
if (bytes.length > 0) {
|
|
if (ethers.utils.keccak256(bytes) === config.orgs.contractHash) {
|
|
return AddressType.Org;
|
|
}
|
|
return AddressType.Contract;
|
|
}
|
|
return AddressType.EOA;
|
|
}
|
|
|
|
// Resolve a label under the radicle domain.
|
|
export async function resolveLabel(label: string, config: Config): Promise<string | null> {
|
|
return config.provider.resolveName(`${label}.${config.registrar.domain}`);
|
|
}
|
|
|
|
// Get a Gnosis Safe at an address.
|
|
export async function getSafe(address: string, config: Config): Promise<Safe | null> {
|
|
if (! config.safe.api) return null;
|
|
|
|
const response = await fetch(`${config.safe.api}/safes/${address}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
}
|
|
});
|
|
|
|
if (! response.ok) {
|
|
return null;
|
|
}
|
|
const json = await response.json();
|
|
|
|
return {
|
|
address: json.address,
|
|
owners: json.owners,
|
|
threshold: json.threshold
|
|
};
|
|
}
|
|
|
|
// Get token balances for an address.
|
|
export async function getTokens(address: string, config: Config):
|
|
Promise<Array<{ tokenName: string, tokenLogo: string }>>
|
|
{
|
|
const result = await config.provider.send("alchemy_getTokenBalances", [address, config.tokens]);
|
|
|
|
// TODO
|
|
return [];
|
|
}
|