Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Allow Org::setName to be used through a Safe
Alexis Sellier committed 4 years ago
commit e7dd169b24bd549b641002ea8da884e64685f713
parent 389730a4a6aac08de5fb3e0df15668033e9d1d2d
6 files changed +128 -23
modified 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<void> {
+
    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<TransactionResponse> {
    assert(config.signer);

@@ -93,6 +125,11 @@ export class Org {
    return [];
  }

+
  async isMember(address: string, config: Config): Promise<boolean> {
+
    const members = await this.getMembers(config);
+
    return members.includes(ethers.utils.getAddress(address));
+
  }
+

  async getProjects(config: Config): Promise<Array<Project>> {
    const result = await utils.querySubgraph(
      config.orgs.subgraph, GetProjects, { org: this.address }
modified 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<boolean> => {
+
    if ($session) {
+
      if (utils.isAddressEqual(org.owner, $session.address)) {
+
        return true;
+
      }
+
      return await org.isMember($session.address, config);
+
    }
+
    return false;
  };
</script>

@@ -171,17 +177,21 @@
        <div class="label">Owner</div>
        <div><Address resolve {config} address={org.owner} /></div>
        <div>
-
          {#if isOwner(org)}
-
            <button class="tiny secondary" on:click={transferOwnership}>
-
              Transfer
-
            </button>
-
          {/if}
+
          {#await isAuthorized(org)}
+
            <!-- Loading -->
+
          {:then authorized}
+
            {#if authorized}
+
              <button class="tiny secondary" on:click={transferOwnership}>
+
                Transfer
+
              </button>
+
            {/if}
+
          {/await}
        </div>
        <!-- Name -->
        <div class="label">Name</div>
        <div>
          {#await org.lookupAddress(config)}
-
            <Loading small />
+
            <div class="loading"><Loading small /></div>
          {:then name}
            {#if name}
              <Link to={`/registrations/${label}`}>{name}</Link>
@@ -191,11 +201,15 @@
          {/await}
        </div>
        <div>
-
          {#if isOwner(org)}
-
            <button class="tiny secondary" on:click={setName}>
-
              Set
-
            </button>
-
          {/if}
+
          {#await isAuthorized(org)}
+
            <!-- Loading -->
+
          {:then authorized}
+
            {#if authorized}
+
              <button class="tiny secondary" on:click={setName}>
+
                Set
+
              </button>
+
            {/if}
+
          {/await}
        </div>
      </div>

modified 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"
    },
modified 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;
modified 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);
modified 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<boolean>
  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<Safe | n
  if (! config.safe.api) return null;

  const addr = ethers.utils.getAddress(address);
-
  const response = await fetch(`${config.safe.api}/safes/${addr}`, {
+
  const response = await fetch(`${config.safe.api}/api/v1/safes/${addr}`, {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
@@ -221,3 +230,34 @@ export async function getTokens(address: string, config: Config):
export function isMarkdownPath(path: string): boolean {
  return /\.(md|mkd|markdown)$/i.test(path);
}
+

+
// Propose a Gnosis Safe multi-sig transaction.
+
export async function proposeSafeTransaction(
+
  safeTx: SafeTransaction,
+
  safeAddress: string,
+
  config: Config
+
): Promise<void> {
+
  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
+
  );
+
}