mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-16 03:40:37 +08:00
Allow Org::setName to be used through a Safe
Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user