diff --git a/src/base/safe-service/ceramic.service.ts b/src/base/safe-service/ceramic.service.ts index c2858f5..61c6590 100644 --- a/src/base/safe-service/ceramic.service.ts +++ b/src/base/safe-service/ceramic.service.ts @@ -93,4 +93,56 @@ export class CeramicService extends SafeService { return Promise.resolve(); } + + async getTransactionConfirmations( + safeTxHash: string + ): Promise { + if (!this.composeClient) { + await this.init(); + } + try { + const confirmationsIndex: ConfirmationIndexData = (await this + .composeClient.executeQuery(` + query ConfirmationIndex { + confirmationIndex( + first: 99 + filters: { where: { transactionHash: { equalTo: "${safeTxHash}" } } } + ) { + edges { + node { + owner + id + signature + signatureType + submissionDate + transactionHash + confirmationType + } + } + } + } + `)) as ConfirmationIndexData; + const confirmations = confirmationsIndex.data.confirmationIndex.edges + .map((edge) => edge.node) + .filter((confirmation) => { + const { signature } = confirmation; + let signatureV: number = parseInt(signature.slice(-2), 16); + // must be signed by with prefix, otherwise, we can't verify this message + if (signatureV !== 31 && signatureV !== 32) { + return false; + } + signatureV -= 4; + const normalizedSignature = + signature.slice(0, -2) + signatureV.toString(16); + return ( + ethers + .verifyMessage(ethers.getBytes(safeTxHash), normalizedSignature) + .toLowerCase() === confirmation.owner.toLowerCase() + ); + }); + return confirmations as SafeMultisigConfirmationResponse[]; + } catch (error) { + // console.error('Error fetching transaction:', error); + } + } } diff --git a/src/base/safe-service/safe.service.ts b/src/base/safe-service/safe.service.ts index 79338b4..16cb42a 100644 --- a/src/base/safe-service/safe.service.ts +++ b/src/base/safe-service/safe.service.ts @@ -9,4 +9,7 @@ export abstract class SafeService { this.url = url; } abstract proposeTransaction(prop: ProposeTransactionProps): Promise; + abstract getTransactionConfirmations( + safeTxHash: string + ): Promise; } diff --git a/src/base/safe-service/safeglobal.service.ts b/src/base/safe-service/safeglobal.service.ts index c51ba9a..ced4a3f 100644 --- a/src/base/safe-service/safeglobal.service.ts +++ b/src/base/safe-service/safeglobal.service.ts @@ -16,4 +16,12 @@ export class SafeGlobalService extends SafeService { async proposeTransaction(props: ProposeTransactionProps): Promise { await this.safeService.proposeTransaction(props); } + + + async getTransactionConfirmations( + safeTxHash: string + ): Promise { + const transaction = await this.safeService.getTransaction(safeTxHash); + return transaction?.confirmations; + } } diff --git a/src/base/safe-service/single.service.ts b/src/base/safe-service/single.service.ts index ad55b9d..e575654 100644 --- a/src/base/safe-service/single.service.ts +++ b/src/base/safe-service/single.service.ts @@ -11,4 +11,22 @@ export class SingleService extends SafeService { async proposeTransaction(props: ProposeTransactionProps): Promise { this.props = props; } + + async getTransactionConfirmations( + safeTxHash: string + ): Promise { + if (safeTxHash !== this.props?.safeTxHash) { + return []; + } + return [ + { + owner: this.props.senderAddress, + signature: this.props.senderSignature, + signatureType: "ECDSA", + transactionHash: safeTxHash, + submissionDate: new Date().toISOString(), + confirmationType: "approve", + }, + ]; + } } diff --git a/src/base/safewallet.ts b/src/base/safewallet.ts index 242c157..f1457b3 100644 --- a/src/base/safewallet.ts +++ b/src/base/safewallet.ts @@ -2,6 +2,7 @@ import { Logger } from "@nestjs/common"; import { MetaTransactionData, SafeTransaction, + SafeMultisigConfirmationResponse, } from "@safe-global/safe-core-sdk-types"; import Safe, { buildSignatureBytes, EthSafeSignature } from "@safe-global/protocol-kit"; import { ethers, Wallet, HDNodeWallet } from "ethers"; @@ -10,12 +11,20 @@ import { EthereumConnectedWallet } from "./wallet"; export interface TransactionPropose { readyExecute: boolean; - signedTransaction: SafeTransaction; + safeTransaction: SafeTransaction; + signatures: string | null; +} + +export interface SignatureInfo { + size: number; + signatures: string; + selfSigned: boolean; } export class SafeWallet { public address: string; public wallet: EthereumConnectedWallet; + public owners: string[]; public threshold: number; private safeSdk: Safe; private safeService: SafeService; @@ -41,9 +50,49 @@ export class SafeWallet { signer: this.wallet.privateKey, safeAddress: this.address, }); + this.owners = (await this.safeSdk.getOwners()).map((o) => o.toLowerCase()); this.threshold = await this.safeSdk.getThreshold(); } + private concatSignatures( + confirmations: SafeMultisigConfirmationResponse[] + ): SignatureInfo { + // must sort by address + confirmations.sort( + ( + left: SafeMultisigConfirmationResponse, + right: SafeMultisigConfirmationResponse + ) => { + const leftAddress = left.owner.toLowerCase(); + const rightAddress = right.owner.toLowerCase(); + if (leftAddress < rightAddress) { + return -1; + } else { + return 1; + } + } + ); + var signatures = "0x"; + const uniqueOwners = []; + for (const confirmation of confirmations) { + signatures += confirmation.signature.substring(2); + if ( + uniqueOwners.includes(confirmation.owner.toLowerCase()) || + !this.owners.includes(confirmation.owner.toLowerCase()) + ) { + continue; + } + uniqueOwners.push(confirmation.owner.toLowerCase()); + } + return { + size: uniqueOwners.length, + signatures: signatures, + selfSigned: uniqueOwners.includes( + this.wallet.wallet.address.toLowerCase() + ), + }; + } + async proposeTransaction( transactions: MetaTransactionData[], isExecuter: boolean, @@ -52,42 +101,72 @@ export class SafeWallet { this.safeSdk ?? (await this.connect(chainId)); const tx = await this.safeSdk.createTransaction({ transactions }); const txHash = await this.safeSdk.getTransactionHash(tx); + let readyExecute: boolean = false; if (this.threshold === 1) { if (isExecuter) { const signedTransaction = await this.safeSdk.signTransaction(tx); return { readyExecute: true, - signedTransaction: signedTransaction, + safeTransaction: tx, + signatures: signedTransaction.encodedSignatures() }; } else { return null; } } else { - const signedTransaction = await this.safeSdk.signTransaction(tx); - const readyExecute = signedTransaction.signatures.size >= this.threshold; - if (signedTransaction.signatures.size < this.threshold) { - try { - const senderSignature = await this.safeSdk.signHash(txHash) - await this.safeService.proposeTransaction({ - safeAddress: this.address, - safeTransactionData: tx.data, - safeTxHash: txHash, - senderAddress: this.wallet.address, - senderSignature: senderSignature.data, - }); - this.logger.log( - `finish to propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId}` - ); - } catch (err) { - this.logger.warn( - `propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId} failed, err ${err}` - ); + let confirmations: SafeMultisigConfirmationResponse[]; + try { + confirmations = await this.safeService.getTransactionConfirmations( + txHash + ); + } catch { + confirmations = []; + } + + var signatureInfo: SignatureInfo = this.concatSignatures(confirmations); + readyExecute = signatureInfo.size >= this.threshold; + if (signatureInfo.selfSigned) { + return { + readyExecute: readyExecute, + safeTransaction: tx, + signatures: signatureInfo.signatures, + }; + } else { + if (signatureInfo.size < this.threshold) { + try { + const senderSignature = await this.safeSdk.signHash(txHash) + await this.safeService.proposeTransaction({ + safeAddress: this.address, + safeTransactionData: tx.data, + safeTxHash: txHash, + senderAddress: this.wallet.address, + senderSignature: senderSignature.data, + }); + signatureInfo.signatures += senderSignature.data.substring(2); + readyExecute = signatureInfo.size + 1 >= this.threshold; + this.logger.log( + `finish to propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId}` + ); + } catch (err) { + this.logger.warn( + `propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId} failed, err ${err}` + ); + } + } + } + if (!isExecuter) { + return { + readyExecute: false, + safeTransaction: tx, + signatures: signatureInfo.signatures } } + // isExecuter return { readyExecute: readyExecute, - signedTransaction: signedTransaction + safeTransaction: tx, + signatures: signatureInfo.signatures }; } } diff --git a/src/relayer/relayer.service.ts b/src/relayer/relayer.service.ts index fc61f78..2708c8a 100644 --- a/src/relayer/relayer.service.ts +++ b/src/relayer/relayer.service.ts @@ -664,13 +664,13 @@ export class RelayerService implements OnModuleInit { relayer.safeWallet.address, relayer.safeWallet.wallet ); - const safeTransactionData = txInfo.signedTransaction.data; + const safeTransaction = txInfo.safeTransaction.data; const err = await safeContract.tryExecTransaction( - safeTransactionData.to, - safeTransactionData.data, + safeTransaction.to, + safeTransaction.data, BigInt(0), - safeTransactionData.operation, - txInfo.signedTransaction.encodedSignatures() + safeTransaction.operation, + txInfo.signatures ); if (err != null) { this.logger.warn( @@ -681,11 +681,11 @@ export class RelayerService implements OnModuleInit { this.logger.log(`[${chain}] ready to ${hint} using safe tx`); const gasPrice = await this.gasPrice(chainInfo); const tx = await safeContract.execTransaction( - safeTransactionData.to, - safeTransactionData.data, + safeTransaction.to, + safeTransaction.data, BigInt(0), - safeTransactionData.operation, - txInfo.signedTransaction.encodedSignatures(), + safeTransaction.operation, + txInfo.signatures, gasPrice ); await this.store.savePendingTransaction( @@ -1160,13 +1160,13 @@ export class RelayerService implements OnModuleInit { bridge.toBridge.safeWallet.address, bridge.toBridge.safeWallet.wallet ); - const safeTransactionData = txInfo.signedTransaction.data; + const safeTransaction = txInfo.safeTransaction.data; const err = await safeContract.tryExecTransaction( - safeTransactionData.to, - safeTransactionData.data, - BigInt(safeTransactionData.value), - safeTransactionData.operation, - txInfo.signedTransaction.encodedSignatures(), + safeTransaction.to, + safeTransaction.data, + BigInt(safeTransaction.value), + safeTransaction.operation, + txInfo.signatures, relayGasLimit ); if (err != null) { @@ -1185,11 +1185,11 @@ export class RelayerService implements OnModuleInit { )}, gasLimit: ${relayGasLimit}` ); const tx = await safeContract.execTransaction( - safeTransactionData.to, - safeTransactionData.data, - BigInt(safeTransactionData.value), - safeTransactionData.operation, - txInfo.signedTransaction.encodedSignatures(), + safeTransaction.to, + safeTransaction.data, + BigInt(safeTransaction.value), + safeTransaction.operation, + txInfo.signatures, gasPrice, null, relayGasLimit