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
+ };
+}