diff --git a/src/base/orgs/Org.ts b/src/base/orgs/Org.ts index c7a0c9bdb..3f120e1f0 100644 --- a/src/base/orgs/Org.ts +++ b/src/base/orgs/Org.ts @@ -1,6 +1,8 @@ import * as ethers from 'ethers'; import type { TransactionResponse } from '@ethersproject/providers'; import type { ContractReceipt } from '@ethersproject/contracts'; +import { OperationType } from "@gnosis.pm/safe-core-sdk-types"; + import { assert } from '@app/error'; import * as utils from '@app/utils'; import type { Config } from '@app/config'; @@ -74,6 +76,36 @@ export class Org { { gasLimit: 200_000 }); } + async setNameMultisig(name: string, config: Config): Promise { + assert(config.signer); + assert(config.safe.client); + + const safeAddress = ethers.utils.getAddress(this.owner); + const orgAddress = ethers.utils.getAddress(this.address); + const org = new ethers.Contract( + this.address, + config.abi.org, + config.signer + ); + const unsignedTx = await org.populateTransaction.setName( + name, + config.provider.network.ensAddress, + ); + + const txData = unsignedTx.data; + if (! txData) { + throw new Error("Org::setNameMultisig: Could not generate transaction for `setName` call"); + } + + const safeTx = { + to: orgAddress, + value: ethers.constants.Zero.toString(), + data: txData, + operation: OperationType.Call, + }; + await utils.proposeSafeTransaction(safeTx, safeAddress, config); + } + async setOwner(address: string, config: Config): Promise { assert(config.signer); @@ -93,6 +125,11 @@ export class Org { return []; } + async isMember(address: string, config: Config): Promise { + const members = await this.getMembers(config); + return members.includes(ethers.utils.getAddress(address)); + } + async getProjects(config: Config): Promise> { const result = await utils.querySubgraph( config.orgs.subgraph, GetProjects, { org: this.address } diff --git a/src/base/orgs/View.svelte b/src/base/orgs/View.svelte index 6b481825b..e442d6923 100644 --- a/src/base/orgs/View.svelte +++ b/src/base/orgs/View.svelte @@ -45,8 +45,14 @@ }; $: label = name && parseEnsLabel(name, config); - $: isOwner = (org: Org): boolean => { - return $session ? utils.isAddressEqual(org.owner, $session.address) : false; + $: isAuthorized = async (org: Org): Promise => { + if ($session) { + if (utils.isAddressEqual(org.owner, $session.address)) { + return true; + } + return await org.isMember($session.address, config); + } + return false; }; @@ -171,17 +177,21 @@
Owner
- {#if isOwner(org)} - - {/if} + {#await isAuthorized(org)} + + {:then authorized} + {#if authorized} + + {/if} + {/await}
Name
{#await org.lookupAddress(config)} - +
{:then name} {#if name} {name} @@ -191,11 +201,15 @@ {/await}
- {#if isOwner(org)} - - {/if} + {#await isAuthorized(org)} + + {:then authorized} + {#if authorized} + + {/if} + {/await}
diff --git a/src/config.json b/src/config.json index e6857c2b8..20b4b4501 100644 --- a/src/config.json +++ b/src/config.json @@ -15,7 +15,7 @@ "contractHash": "0x5c34bb0755876de98e801805e6685456eea739ad0abba1cc450a7ee0f2a70b74" }, "safe": { - "api": "https://safe-transaction.gnosis.io/api/v1", + "api": "https://safe-transaction.gnosis.io", "subgraph": null, "viewer": "https://gnosis-safe.io/app/#/safes" }, @@ -59,7 +59,7 @@ "contractHash": "0x5c34bb0755876de98e801805e6685456eea739ad0abba1cc450a7ee0f2a70b74" }, "safe": { - "api": "https://safe-transaction.rinkeby.gnosis.io/api/v1", + "api": "https://safe-transaction.rinkeby.gnosis.io", "subgraph": "https://api.thegraph.com/subgraphs/name/radicle-dev/gnosis-safe-rinkeby", "viewer": "https://rinkeby.gnosis-safe.io/app/#/safes" }, diff --git a/src/config.ts b/src/config.ts index d2a1ebf18..522e64176 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ import { ethers } from "ethers"; import type { TypedDataSigner } from '@ethersproject/abstract-signer'; +import SafeServiceClient from "@gnosis.pm/safe-service-client"; import config from "@app/config.json"; declare global { @@ -7,7 +8,6 @@ declare global { ethereum: any; registrarState: any; } - } export class Config { @@ -19,7 +19,12 @@ export class Config { gasLimits: { createOrg: number }; provider: ethers.providers.JsonRpcProvider; signer: ethers.Signer & TypedDataSigner | null; - safe: { api: string | null; subgraph: string; viewer: string | null }; + safe: { + api?: string; + client?: SafeServiceClient; + subgraph: string; + viewer: string | null; + }; abi: { [contract: string]: string[] }; seed: { api?: string }; tokens: string[]; @@ -44,6 +49,9 @@ export class Config { this.orgFactory = cfg.orgFactory; this.orgs = cfg.orgs; this.safe = cfg.safe; + this.safe.client = this.safe.api + ? new SafeServiceClient(this.safe.api) + : undefined; this.provider = provider; this.signer = signer; this.gasLimits = gasLimits; diff --git a/src/ens/SetName.svelte b/src/ens/SetName.svelte index 1c93a62f6..74f87301e 100644 --- a/src/ens/SetName.svelte +++ b/src/ens/SetName.svelte @@ -7,6 +7,7 @@ import type { Org } from '@app/base/orgs/Org'; import Loading from '@app/Loading.svelte'; import Error from '@app/Error.svelte'; + import * as utils from '@app/utils'; const dispatch = createEventDispatcher(); @@ -34,11 +35,16 @@ let resolved = await config.provider.resolveName(domain); if (resolved && isAddressEqual(resolved, org.address)) { - state = State.Signing; try { - let tx = await org.setName(domain, config); - state = State.Pending; - await tx.wait(); + if (utils.isSafe(org.owner, config)) { + state = State.Signing; + await org.setNameMultisig(domain, config); + } else { + state = State.Signing; + let tx = await org.setName(domain, config); + state = State.Pending; + await tx.wait(); + } state = State.Success; } catch (e) { console.error(e); diff --git a/src/utils.ts b/src/utils.ts index a77ff8b38..c5ad068c9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,9 @@ import { ethers } from "ethers"; import type { BigNumber } from "ethers"; import multibase from 'multibase'; import multihashes from 'multihashes'; +import EthersSafe from "@gnosis.pm/safe-core-sdk"; import type { Config } from '@app/config'; +import { assert } from '@app/error'; export enum AddressType { Contract, @@ -17,6 +19,13 @@ export interface Safe { threshold: number; } +export interface SafeTransaction { + to: string; + value: string; + data: string; + operation: number; +} + export function isAddressEqual(left: string, right: string): boolean { return left.toLowerCase() === right.toLowerCase(); } @@ -178,7 +187,7 @@ export async function isSafe(address: string, config: Config): Promise if (! config.safe.api) return false; const addr = ethers.utils.getAddress(address); - const response = await fetch(`${config.safe.api}/safes/${addr}`, { method: 'HEAD' }); + const response = await fetch(`${config.safe.api}/api/v1/safes/${addr}`, { method: 'HEAD' }); return response.ok; } @@ -188,7 +197,7 @@ export async function getSafe(address: string, config: Config): Promise { + assert(config.signer); + assert(config.safe.client); + + const safeSdk = await EthersSafe.create({ + ethers, safeAddress, providerOrSigner: config.signer, + }); + const estimation = await config.safe.client.estimateSafeTransaction( + safeAddress, + safeTx + ); + const transaction = await safeSdk.createTransaction({ + ...safeTx, + safeTxGas: Number(estimation.safeTxGas), + }); + const safeTxHash = await safeSdk.getTransactionHash(transaction); + const signature = await safeSdk.signTransactionHash(safeTxHash); + + await config.safe.client.proposeTransaction( + safeAddress, + transaction.data, + safeTxHash, + signature + ); +}