mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-02 02:00:31 +08:00
Add pending projects and anchor signing
Signed-off-by: Sebastian Martinez <sebastinez@me.com>
This commit is contained in:
+1
-1
@@ -49,4 +49,4 @@
|
||||
</style>
|
||||
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<img class="avatar" class:inline src={source} on:error={handleMissingFile} class:glowOnHover />
|
||||
<img class="avatar" class:inline src={source} title={address} on:error={handleMissingFile} class:glowOnHover />
|
||||
|
||||
+67
-7
@@ -7,7 +7,7 @@ import { assert } from '@app/error';
|
||||
import * as utils from '@app/utils';
|
||||
import type { Safe } from '@app/utils';
|
||||
import type { Config } from '@app/config';
|
||||
import type { Project } from '@app/project';
|
||||
import type { PendingProject, Project } from '@app/project';
|
||||
|
||||
const GetProjects = `
|
||||
query GetProjects($org: ID!) {
|
||||
@@ -69,14 +69,16 @@ export const GetSafe = `
|
||||
export class Org {
|
||||
address: string;
|
||||
owner: string;
|
||||
name?: string;
|
||||
name?: string | null;
|
||||
safe?: Safe | null;
|
||||
|
||||
constructor(address: string, owner: string, name?: string) {
|
||||
constructor(address: string, owner: string, name?: string | null, safe?: Safe | null) {
|
||||
assert(ethers.utils.isAddress(address), "address must be valid");
|
||||
|
||||
this.address = address.toLowerCase(); // Don't store address checksum.
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
this.safe = safe;
|
||||
}
|
||||
|
||||
async setName(name: string, config: Config): Promise<TransactionResponse> {
|
||||
@@ -162,7 +164,9 @@ export class Org {
|
||||
}
|
||||
|
||||
async getMembers(config: Config): Promise<Array<string>> {
|
||||
const safe = await utils.getSafe(this.owner, config);
|
||||
if (this.safe) return this.safe.owners;
|
||||
|
||||
const safe = await this.getSafe(config);
|
||||
if (safe) {
|
||||
return safe.owners;
|
||||
}
|
||||
@@ -170,15 +174,17 @@ export class Org {
|
||||
}
|
||||
|
||||
async getSafe(config: Config): Promise<Safe | null> {
|
||||
if (this.safe) return this.safe;
|
||||
|
||||
return utils.getSafe(this.owner, config);
|
||||
}
|
||||
|
||||
async isMember(address: string, config: Config): Promise<boolean> {
|
||||
const members = await this.getMembers(config);
|
||||
return members.includes(ethers.utils.getAddress(address));
|
||||
return members.includes(address.toLowerCase());
|
||||
}
|
||||
|
||||
async getProjects(config: Config): Promise<Array<Project>> {
|
||||
async getProjects(config: Config): Promise<Project[]> {
|
||||
const result = await utils.querySubgraph(
|
||||
config.orgs.subgraph, GetProjects, { org: this.address }
|
||||
);
|
||||
@@ -202,6 +208,39 @@ export class Org {
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getPendingProjects(config: Config): Promise<PendingProject[]> {
|
||||
if (! config.safe.client) return [];
|
||||
|
||||
const orgAddr = ethers.utils.getAddress(this.address);
|
||||
const response = await config.safe.client.getPendingTransactions(
|
||||
ethers.utils.getAddress(this.owner)
|
||||
);
|
||||
const projects: PendingProject[] = [];
|
||||
|
||||
for (const tx of response.results || []) {
|
||||
if (tx.data && tx.to === orgAddr) {
|
||||
const project = parseAnchorTx(tx.data, config);
|
||||
const confirmations = tx.confirmations?.map(t => t.owner) || [];
|
||||
|
||||
if (project) {
|
||||
projects.push({ ...project, confirmations, safeTxHash: tx.safeTxHash });
|
||||
}
|
||||
}
|
||||
}
|
||||
return projects;
|
||||
}
|
||||
|
||||
async getAllProjects(config: Config): Promise<Array<Project | PendingProject>> {
|
||||
const result = await Promise.allSettled([
|
||||
this.getPendingProjects(config),
|
||||
this.getProjects(config),
|
||||
]);
|
||||
|
||||
return result.flatMap(r => {
|
||||
return r.status === "fulfilled" ? r.value : [];
|
||||
});
|
||||
}
|
||||
|
||||
static async getAnchor(orgAddr: string, urn: string, config: Config): Promise<string | null> {
|
||||
const org = new ethers.Contract(
|
||||
orgAddr,
|
||||
@@ -271,9 +310,12 @@ export class Org {
|
||||
org.resolvedAddress,
|
||||
]);
|
||||
|
||||
const safe = await utils.getSafe(owner, config);
|
||||
// If what is resolved is not the same as the input, it's because we
|
||||
// were given a name.
|
||||
if (utils.isAddressEqual(addressOrName, resolved)) {
|
||||
if (utils.isAddressEqual(addressOrName, resolved) && safe) {
|
||||
return new Org(resolved, owner, null, safe);
|
||||
} else if (safe === null) {
|
||||
return new Org(resolved, owner);
|
||||
} else {
|
||||
return new Org(resolved, owner, addressOrName);
|
||||
@@ -336,3 +378,21 @@ export class Org {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAnchorTx(data: string, config: Config): Project | null {
|
||||
const iface = new ethers.utils.Interface(config.abi.org);
|
||||
const parsedTx = iface.parseTransaction({ data });
|
||||
|
||||
if (parsedTx.name === "anchor") {
|
||||
const encodedProjectUrn = parsedTx.args[0];
|
||||
const encodedCommitHash = parsedTx.args[2];
|
||||
const id = utils.formatRadicleId(
|
||||
ethers.utils.arrayify(`${encodedProjectUrn}`)
|
||||
);
|
||||
const byteArray = ethers.utils.arrayify(encodedCommitHash);
|
||||
const stateHash = utils.formatProjectHash(byteArray);
|
||||
|
||||
return { id, anchor: { stateHash } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
import Error from '@app/Error.svelte';
|
||||
import Icon from '@app/Icon.svelte';
|
||||
import SetName from '@app/ens/SetName.svelte';
|
||||
import Project from '@app/base/projects/Widget.svelte';
|
||||
import Address from '@app/Address.svelte';
|
||||
import Avatar from '@app/Avatar.svelte';
|
||||
import Message from '@app/Message.svelte';
|
||||
import * as utils from '@app/utils';
|
||||
|
||||
import { Org } from '@app/base/orgs/Org';
|
||||
import TransferOwnership from '@app/base/orgs/TransferOwnership.svelte';
|
||||
import { Profile, ProfileType } from '@app/profile';
|
||||
import Projects from './View/Projects.svelte';
|
||||
|
||||
export let addressOrName: string;
|
||||
export let config: Config;
|
||||
@@ -137,12 +136,6 @@
|
||||
width: 1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.projects {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.projects .project {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.members {
|
||||
margin-top: 2rem;
|
||||
align-items: center;
|
||||
@@ -326,21 +319,7 @@
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
<div class="projects">
|
||||
{#await org.getProjects(config)}
|
||||
<Loading center />
|
||||
{:then projects}
|
||||
{#each projects as project}
|
||||
<div class="project">
|
||||
<Project {project} org={profile.nameOrAddress} config={profile.config(config)} />
|
||||
</div>
|
||||
{/each}
|
||||
{:catch err}
|
||||
<Message error>
|
||||
<strong>Error: </strong> failed to load projects: {err.message}.
|
||||
</Message>
|
||||
{/await}
|
||||
</div>
|
||||
<Projects {org} {config} />
|
||||
{/await}
|
||||
</main>
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
<script lang="ts">
|
||||
import { ethers } from "ethers";
|
||||
import type { Safe } from "@app/utils";
|
||||
import type { PendingProject } from "@app/project";
|
||||
import type { Config } from "@app/config";
|
||||
import * as utils from "@app/utils";
|
||||
import Modal from "@app/Modal.svelte";
|
||||
import Avatar from "@app/Avatar.svelte";
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let safe: Safe;
|
||||
export let project: PendingProject;
|
||||
export let account: string;
|
||||
export let config: Config;
|
||||
|
||||
enum State {
|
||||
Idle,
|
||||
Confirm,
|
||||
Signing,
|
||||
Submitting,
|
||||
Success,
|
||||
Execute,
|
||||
Failed,
|
||||
}
|
||||
|
||||
enum Action {
|
||||
Sign,
|
||||
Execute,
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let state = State.Idle;
|
||||
let action: null | Action = null;
|
||||
const isSigned = project.confirmations.includes(
|
||||
ethers.utils.getAddress(account)
|
||||
);
|
||||
const close = () => {
|
||||
action = null;
|
||||
state = State.Idle;
|
||||
// Could eventually be a separate function if we want to handle a Cancel event differently.
|
||||
dispatch("success");
|
||||
};
|
||||
const pending = safe.threshold - project.confirmations.length;
|
||||
const executeTransaction = async (safeTxHash: string) => {
|
||||
try {
|
||||
action = Action.Execute;
|
||||
state = State.Confirm;
|
||||
const txResult = await utils.executeSignedSafeTransaction(safe.address, safeTxHash, config);
|
||||
|
||||
state = State.Submitting;
|
||||
await txResult.transactionResponse?.wait();
|
||||
|
||||
state = State.Success;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
const confirmAnchor = async (safeTxHash: string) => {
|
||||
try {
|
||||
action = Action.Sign;
|
||||
state = State.Signing;
|
||||
const signature = await utils.signSafeTransaction(safe.address, safeTxHash, config);
|
||||
|
||||
state = State.Submitting;
|
||||
await config.safe.client?.confirmTransaction(safeTxHash, signature.data);
|
||||
|
||||
state = State.Success;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
state = State.Failed;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.confirmations {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.table {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 4fr;
|
||||
grid-gap: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
.table > *:nth-child(odd) { /* Labels */
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
.table > *:nth-child(even) { /* Values */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
.avatars {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="confirmations">
|
||||
{#if pending > 0}
|
||||
<strong>{pending}</strong> signature(s) pending
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<div class="avatars">
|
||||
{#each project.confirmations as signee}
|
||||
<Avatar inline source={signee} address={signee} glowOnHover />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Check whether the threshold has been matched or passed -->
|
||||
{#if pending <= 0}
|
||||
<button on:click|stopPropagation={() => {
|
||||
action = Action.Execute;
|
||||
state = State.Confirm;
|
||||
}} class="tiny">
|
||||
Execute
|
||||
</button>
|
||||
<!-- Check whether or not we've signed this proposal -->
|
||||
{:else if isSigned}
|
||||
<span class="badge safe no-margin">✓ signed</span>
|
||||
{:else}
|
||||
<button on:click|stopPropagation={() => {
|
||||
action = Action.Sign;
|
||||
state = State.Confirm;
|
||||
}} class="tiny">
|
||||
Confirm
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- We've initiated an action -->
|
||||
{#if state !== State.Idle && action === Action.Sign}
|
||||
<Modal floating>
|
||||
<span slot="title">
|
||||
<div>⚓</div>
|
||||
<div>Anchor project</div>
|
||||
</span>
|
||||
|
||||
<span slot="subtitle">
|
||||
{#if state == State.Confirm}
|
||||
<span>Initiate the transaction...</span>
|
||||
{:else if state == State.Signing}
|
||||
<span>Sign the transaction in your wallet...</span>
|
||||
{:else if state == State.Submitting}
|
||||
<span>Transaction is being confirmed...</span>
|
||||
{:else if state == State.Success}
|
||||
<span>Transaction confirmed.</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span slot="body">
|
||||
{#if state == State.Confirm}
|
||||
<div class="table">
|
||||
<div>Project</div><code>{project.id}</code>
|
||||
<div>Hash</div><code>{project.anchor.stateHash}</code>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span slot="actions">
|
||||
{#if state == State.Confirm}
|
||||
<button class="primary" on:click={() => confirmAnchor(project.safeTxHash)}>
|
||||
Confirm
|
||||
</button>
|
||||
<button class="text" on:click={close}>
|
||||
Cancel
|
||||
</button>
|
||||
{:else if state == State.Success}
|
||||
<button on:click={close}>Done</button>
|
||||
{/if}
|
||||
</span>
|
||||
</Modal>
|
||||
{:else if state !== State.Idle && action === Action.Execute}
|
||||
<Modal floating>
|
||||
<span slot="title">
|
||||
<div>⚡</div>
|
||||
<div>Execute Safe Transaction</div>
|
||||
</span>
|
||||
|
||||
<span slot="subtitle">
|
||||
{#if state == State.Confirm}
|
||||
<span>Sign the transaction in your wallet...</span>
|
||||
{:else if state == State.Submitting}
|
||||
<span>Transaction is being confirmed...</span>
|
||||
{:else if state == State.Success}
|
||||
<span>Transaction confirmed.</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span slot="body">
|
||||
{#if state == State.Confirm}
|
||||
<div class="table">
|
||||
<div>TxHash</div><code>{utils.formatHash(project.safeTxHash)}</code>
|
||||
<div>Quorum</div><code>{project.confirmations.length} of {safe.threshold}</code>
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span slot="actions">
|
||||
{#if state == State.Confirm}
|
||||
<button class="primary" on:click={() => executeTransaction(project.safeTxHash)}>
|
||||
Confirm
|
||||
</button>
|
||||
<button class="text" on:click={close}>
|
||||
Cancel
|
||||
</button>
|
||||
{:else if state == State.Success}
|
||||
<button on:click={() => close}>Done</button>
|
||||
{/if}
|
||||
</span>
|
||||
</Modal>
|
||||
{/if}
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import type { Config } from "@app/config";
|
||||
import type { Org } from "@app/base/orgs/Org";
|
||||
import type { PendingProject, Project } from "@app/project";
|
||||
import { session } from '@app/session';
|
||||
import Loading from "@app/Loading.svelte";
|
||||
import Message from "@app/Message.svelte";
|
||||
import Widget from '@app/base/projects/Widget.svelte';
|
||||
import Anchor from './Anchor.svelte';
|
||||
|
||||
export let org: Org;
|
||||
export let config: Config;
|
||||
|
||||
let getProjects = queryProjects;
|
||||
|
||||
function updateRecords() {
|
||||
getProjects = queryProjects;
|
||||
}
|
||||
|
||||
async function queryProjects(): Promise<(Project | PendingProject)[]> {
|
||||
if ($session) {
|
||||
const result = await org.isMember($session.address, config);
|
||||
return result ? org.getAllProjects(config) : org.getProjects(config);
|
||||
}
|
||||
return org.getProjects(config);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.projects {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.projects .project {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.anchor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="projects">
|
||||
{#await getProjects()}
|
||||
<Loading center />
|
||||
{:then projects}
|
||||
{#each projects as project}
|
||||
<div class="project">
|
||||
{#if "safeTxHash" in project} <!-- Pending project -->
|
||||
<Widget {project} org={org.address} {config} faded>
|
||||
<span class="anchor" slot="actions">
|
||||
{#if org.safe && $session}
|
||||
<Anchor {project} safe={org.safe} on:success={() => updateRecords()} account={$session.address} {config} />
|
||||
<button on:click={() => updateRecords()}>Update projects</button>
|
||||
{/if}
|
||||
</span>
|
||||
</Widget>
|
||||
{:else} <!-- Anchored project -->
|
||||
<Widget {project} org={org.address} {config} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:catch err}
|
||||
<Message error>
|
||||
<strong>Error: </strong> failed to load projects: {err.message}.
|
||||
</Message>
|
||||
{/await}
|
||||
</div>
|
||||
@@ -17,6 +17,7 @@
|
||||
export let config: Config;
|
||||
export let org: string | undefined = undefined;
|
||||
export let user: string | undefined = undefined;
|
||||
export let faded = false;
|
||||
|
||||
let state: State = { status: Status.Loading };
|
||||
let info: proj.Info | null = null;
|
||||
@@ -27,6 +28,7 @@
|
||||
state = { status: Status.Loaded };
|
||||
info = result;
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
state = { status: Status.Error, error: err.message };
|
||||
}
|
||||
});
|
||||
@@ -55,23 +57,38 @@
|
||||
article.has-info {
|
||||
cursor: pointer;
|
||||
}
|
||||
article.project-faded {
|
||||
border: 1px dashed var(--color-foreground-subtle);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
article.has-info:hover {
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
article.project-faded.has-info:hover {
|
||||
border-color: var(--color-foreground-faded);
|
||||
}
|
||||
article .id {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
article .description {
|
||||
margin-bottom: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
article .anchor {
|
||||
color: var(--color-secondary);
|
||||
font-size: 0.75rem;
|
||||
min-height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
article .commit, article .actions {
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
article.project-faded .anchor {
|
||||
color: var(--color-foreground-faded);
|
||||
}
|
||||
article .id, article .anchor {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -97,20 +114,22 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<article on:click={onClick} class:has-info={info}>
|
||||
<article on:click={onClick} class:has-info={info} class:project-faded={faded}>
|
||||
{#if info}
|
||||
<div class="id">
|
||||
<span class="name">{info.meta.name}</span><span class="urn">{project.id}</span>
|
||||
</div>
|
||||
<div class="description">{info.meta.description}</div>
|
||||
<div class="anchor">
|
||||
<span>commit {project.anchor.stateHash}</span>
|
||||
<span>
|
||||
{#each info.meta.maintainers as urn}
|
||||
<span class="avatar">
|
||||
<Blockies address={urn} />
|
||||
</span>
|
||||
{/each}
|
||||
<span class="commit">commit {project.anchor.stateHash}</span>
|
||||
<span class="actions">
|
||||
<slot name="actions">
|
||||
{#each info.meta.maintainers as urn}
|
||||
<span class="avatar">
|
||||
<Blockies address={urn} />
|
||||
</span>
|
||||
{/each}
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -120,6 +139,12 @@
|
||||
<Loading small />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="anchor">commit {project.anchor.stateHash}</div>
|
||||
<div class="anchor">
|
||||
<span class="commit">commit {project.anchor.stateHash}</span>
|
||||
<span class="actions">
|
||||
<slot name="actions">
|
||||
</slot>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"org": [
|
||||
"function owner() view returns (address)",
|
||||
"function anchors(bytes32) view returns (uint32, bytes)",
|
||||
"function anchor(bytes32, uint32, bytes)",
|
||||
"function setOwner(address)",
|
||||
"function setName(string, address) returns (bytes32)"
|
||||
],
|
||||
|
||||
@@ -10,6 +10,11 @@ export interface Project {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PendingProject extends Project {
|
||||
safeTxHash: string; // Safe transaction hash.
|
||||
confirmations: string[]; // Owner addresses who have confirmed.
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
head: string;
|
||||
meta: Meta;
|
||||
|
||||
+70
-1
@@ -2,7 +2,8 @@ import { ethers } from "ethers";
|
||||
import { BigNumber } from "ethers";
|
||||
import multibase from 'multibase';
|
||||
import multihashes from 'multihashes';
|
||||
import EthersSafe, { EthersAdapter } from "@gnosis.pm/safe-core-sdk";
|
||||
import EthersSafe, { EthersAdapter, TransactionResult } from "@gnosis.pm/safe-core-sdk";
|
||||
import type { SafeSignature } from "@gnosis.pm/safe-core-sdk-types";
|
||||
import type { Config } from '@app/config';
|
||||
import config from "@app/config.json";
|
||||
import { assert } from '@app/error';
|
||||
@@ -428,3 +429,71 @@ export async function proposeSafeTransaction(
|
||||
signature
|
||||
);
|
||||
}
|
||||
|
||||
// Sign a Gnosis Safe multi-sig transaction.
|
||||
export async function signSafeTransaction(
|
||||
safeAddress: string,
|
||||
safeTxHash: string,
|
||||
config: Config
|
||||
): Promise<SafeSignature> {
|
||||
assert(config.signer);
|
||||
|
||||
const ethAdapter = new EthersAdapter({
|
||||
ethers, signer: config.signer
|
||||
});
|
||||
const safeSdk = await EthersSafe.create({
|
||||
ethAdapter, safeAddress
|
||||
});
|
||||
return await safeSdk.signTransactionHash(safeTxHash);
|
||||
}
|
||||
|
||||
// Execute a Gnosis Safe signed transaction by safeTxHash.
|
||||
export async function executeSignedSafeTransaction(
|
||||
safeAddress: string,
|
||||
safeTxHash: string,
|
||||
config: Config
|
||||
): Promise<TransactionResult> {
|
||||
assert(config.signer);
|
||||
assert(config.safe.client);
|
||||
|
||||
const ethAdapter = new EthersAdapter({
|
||||
ethers, signer: config.signer
|
||||
});
|
||||
const safeSdk = await EthersSafe.create({
|
||||
ethAdapter, safeAddress
|
||||
});
|
||||
|
||||
const signedTx = await config.safe.client.getTransaction(safeTxHash);
|
||||
|
||||
assert(signedTx.data);
|
||||
assert(signedTx.confirmations);
|
||||
|
||||
const safeTx = await safeSdk.createTransaction({
|
||||
...signedTx,
|
||||
gasPrice: Number(signedTx.gasPrice),
|
||||
data: signedTx.data
|
||||
} );
|
||||
|
||||
signedTx.confirmations.forEach(confirmation => {
|
||||
const signature = new EthSignSignature(confirmation.owner, confirmation.signature);
|
||||
safeTx.addSignature(signature);
|
||||
});
|
||||
|
||||
return await safeSdk.executeTransaction(safeTx);
|
||||
}
|
||||
|
||||
export class EthSignSignature {
|
||||
signer: string;
|
||||
data: string;
|
||||
|
||||
constructor(signer: string, signature: string) {
|
||||
this.signer = signer;
|
||||
this.data = signature;
|
||||
}
|
||||
staticPart(): string {
|
||||
return this.data;
|
||||
}
|
||||
dynamicPart(): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user