diff --git a/src/Address.svelte b/src/Address.svelte index dd9ff72a9..f1a4c2410 100644 --- a/src/Address.svelte +++ b/src/Address.svelte @@ -57,6 +57,8 @@ {addressLabel} {#if addressType === AddressType.Contract} contract + {:else if addressType === AddressType.Safe} + safe {:else if addressType === AddressType.EOA} {:else} diff --git a/src/config.json b/src/config.json index 09f9101b5..1f2b46d28 100644 --- a/src/config.json +++ b/src/config.json @@ -9,6 +9,9 @@ }, "orgFactory": { "address": "0x0000000000000000000000000000000000000000" + }, + "safe": { + "api": "https://safe-transaction.gnosis.io/api/v1" } }, "ropsten": { @@ -25,6 +28,9 @@ "orgs": { "subgraph": "https://api.thegraph.com/subgraphs/name/radicle-dev/radicle-orgs-ropsten", "contractHash": "0xc5bd5bc3cbcb709540df530c5ad2a9d91463ebb891a789286872938d963c12e9" + }, + "safe": { + "api": null } }, "rinkeby": { @@ -41,6 +47,9 @@ "orgs": { "subgraph": "https://api.thegraph.com/subgraphs/name/radicle-dev/radicle-orgs-rinkeby", "contractHash": "0xc5bd5bc3cbcb709540df530c5ad2a9d91463ebb891a789286872938d963c12e9" + }, + "safe": { + "api": "https://safe-transaction.rinkeby.gnosis.io/api/v1" } }, "seed": { diff --git a/src/config.ts b/src/config.ts index db1488486..6ea40a291 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,6 +25,7 @@ export class Config { provider: ethers.providers.JsonRpcProvider; signer: ethers.Signer & TypedDataSigner | null; seed: { url: string }; + safe: { api: string | null }; constructor( network: { name: string, chainId: number }, @@ -44,6 +45,7 @@ export class Config { this.radToken = cfg.radToken; this.orgFactory = cfg.orgFactory; this.orgs = cfg.orgs; + this.safe = cfg.safe; this.provider = provider; this.signer = signer; this.gasLimits = gasLimits; diff --git a/src/utils.ts b/src/utils.ts index ce214576c..c0a917887 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,9 +7,16 @@ 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)); } @@ -127,6 +134,11 @@ export function formatProjectHash(hash: Uint8Array, format: number): string { // Identify an address by checking whether it's a contract or an externally-owned address. export async function identifyAddress(address: string, config: Config): Promise { + let safe = await getSafe(address, config); + if (safe) { + return AddressType.Safe; + } + let code = await config.provider.getCode(address); let bytes = ethers.utils.arrayify(code); @@ -143,3 +155,26 @@ export async function identifyAddress(address: string, config: Config): Promise< export async function resolveLabel(label: string, config: Config): Promise { return config.provider.resolveName(`${label}.${config.registrar.domain}`); } + +// Get a Gnosis Safe at an address. +export async function getSafe(address: string, config: Config): Promise { + 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 + }; +}