Skip to content

Instantly share code, notes, and snippets.

@valeryz
Created March 17, 2024 19:13
Show Gist options
  • Save valeryz/bdced86266a81cd36c7d917872d32f10 to your computer and use it in GitHub Desktop.
Save valeryz/bdced86266a81cd36c7d917872d32f10 to your computer and use it in GitHub Desktop.
Sending a Safe transaction in typescript (taken from Hardhat Unit test)
it("Should succeed verification of a basic transaction", async function () {
const nonce = await safe.getNonce();
const threshold = await safe.getThreshold();
const safeTransactionData : SafeTransactionData = {
to: ethers.ZeroAddress,
value: "0x0",
data: "0x",
operation: 0,
// default fields below
safeTxGas: "0x0",
baseGas: "0x0",
gasPrice: "0x0",
gasToken: ethers.ZeroAddress,
refundReceiver: ethers.ZeroAddress,
nonce,
}
console.log("transaction", safeTransactionData);
const transaction = await safe.createTransaction({ transactions: [safeTransactionData] });
const txHash = await safe.getTransactionHash(transaction);
console.log("txHash", txHash);
// Let's generate three signatures for the owners of the Safe.
// ok, our siganture is a EIP-712 signature, so we need to sign the hash of the transaction.
let safeTypedData = {
safeAddress: await safe.getAddress(),
safeVersion: await safe.getContractVersion(),
chainId: await ownerAdapters[0].getChainId(),
safeTransactionData: safeTransactionData,
};
const sig1 = await ownerAdapters[0].signTypedData(safeTypedData);
const sig2 = await ownerAdapters[1].signTypedData(safeTypedData);
const sig3 = await ownerAdapters[2].signTypedData(safeTypedData);
const nil_pubkey = {
x: Array.from(ethers.getBytes("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")),
y: Array.from(ethers.getBytes("0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"))
};
// Our Nil signature is a signature with r and s set to
const nil_signature = Array.from(
ethers.getBytes("0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8"));
const zero_address = new Array(20).fill(0);
const signatures = [sig2, sig3]; // sig1 is not included, threshold of 2 should be enough.
// Sort signatures by address - this is how the Safe contract does it.
signatures.sort((sig1, sig2) => ethers.recoverAddress(txHash, sig1).localeCompare(ethers.recoverAddress(txHash, sig2)));
const input = {
threshold: await safe.getThreshold(),
signers: padArray(signatures.map((sig) => extractCoordinates(ethers.SigningKey.recoverPublicKey(txHash, sig))), 10, nil_pubkey),
signatures: padArray(signatures.map(extractRSFromSignature), 10, nil_signature),
txn_hash: Array.from(ethers.getBytes(txHash)),
owners: padArray((await safe.getOwners()).map(addressToArray), 10, zero_address),
};
correctProof = await noir.generateFinalProof(input);
console.log("correctProof", correctProof);
const verification = await noir.verifyFinalProof(correctProof);
expect(verification).to.be.true;
console.log("verification in JS succeeded");
const safeAddress = await safe.getAddress();
const directVerification = await verifierContract.verify(correctProof["proof"], [...correctProof["publicInputs"].values()]);
console.log("directVerification", directVerification);
const contractVerification = await zkSafeModule.verifyZkSafeTransaction(safeAddress, txHash, correctProof["proof"]);
console.log("contractVerification", contractVerification);
console.log("safe: ", safe);
console.log("transaction: ", transaction);
const txn = await zkSafeModule.sendZkSafeTransaction(
safeAddress,
{ to: transaction["data"]["to"],
value: BigInt(transaction["data"]["value"]),
data: transaction["data"]["data"],
operation: transaction["data"]["operation"],
},
correctProof["proof"],
{ gasLimit: 2000000 }
);
let receipt = txn.wait();
expect(txn).to.not.be.reverted;
let newNonce = await safe.getNonce();
expect(newNonce).to.equal(nonce + 1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment