Allow Org::setName to be used through a Safe

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier
2021-06-21 21:09:07 +02:00
parent 389730a4a6
commit e7dd169b24
6 changed files with 128 additions and 23 deletions
+37
View File
@@ -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 }
+27 -13
View File
@@ -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>
+2 -2
View File
@@ -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"
},
+10 -2
View File
@@ -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;
+10 -4
View File
@@ -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);
+42 -2
View File
@@ -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
);
}