zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
246 lines • 9.19 kB
JavaScript
import { ethers } from 'ethers';
import { EIP712_TX_TYPE } from './utils';
/**
* Signs the `payload` using an ECDSA private key.
*
* @param payload The payload that needs to be signed.
* @param secret The ECDSA private key.
*
* @example Sign EIP712 transaction hash.
*
* import { EIP712Signer, types, utils } from "zksync-ethers";
*
* const PRIVATE_KEY = "<PRIVATE_KEY>";
*
* const tx: types.TransactionRequest = {
* chainId: 270,
* from: ADDRESS,
* to: "<RECEIVER>",
* value: 7_000_000_000,
* };
*
* const txHash = EIP712Signer.getSignedDigest(tx);
* const result = await utils.signPayloadWithECDSA(txHash, PRIVATE_KEY);
*
* @example Sign message hash.
*
* import { utils } from "zksync-ethers";
* import { hashMessage } from "ethers";
*
* const PRIVATE_KEY = "<PRIVATE_KEY>";
*
* const message = 'Hello World!';
* const messageHash = hashMessage(message);
*
* const result = await utils.signPayloadWithECDSA(messageHash, PRIVATE_KEY);
*
* @example Sign typed data hash.
*
* import { utils } from "zksync-ethers";
* import { TypedDataEncoder } from "ethers";
*
* const PRIVATE_KEY = "<PRIVATE_KEY>";
*
* const typedDataHash = TypedDataEncoder.hash(
* {name: 'Example', version: '1', chainId: 270},
* {
* Person: [
* {name: 'name', type: 'string'},
* {name: 'age', type: 'uint8'},
* ],
* },
* {name: 'John', age: 30}
* );
* const result = await utils.signPayloadWithECDSA(typedDataHash, PRIVATE_KEY);
*/
export const signPayloadWithECDSA = async (payload, secret) => {
return new ethers.Wallet(secret).signingKey.sign(payload).serialized;
};
/**
* Signs the `payload` using multiple ECDSA private keys.
* The signature is generated by concatenating signatures created by signing with each key individually.
* The length of the resulting signature should be `secrets.length * 65 + 2`.
*
* @param payload The payload that needs to be signed.
* @param secret The list of the ECDSA private keys.
*
* @throws {Error} If the `secret` is not an array of at least two elements.
*
* @example Sign EIP712 transaction hash.
*
* import { EIP712Signer, types, utils } from "zksync-ethers";
*
* const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
* const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
*
* const tx: types.TransactionRequest = {
* chainId: 270,
* from: ADDRESS,
* to: "<RECEIVER>",
* value: 7_000_000_000,
* };
*
* const txHash = EIP712Signer.getSignedDigest(tx);
* const result = await utils.signPayloadWithMultipleECDSA(typedDataHash, [PRIVATE_KEY1, PRIVATE_KEY2]);
*
* @example Sign message hash.
*
* import { utils } from "zksync-ethers";
* import { hashMessage } from "ethers";
*
* const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
* const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
*
* const message = 'Hello World!';
* const messageHash = hashMessage(message);
*
* const result = await utils.signPayloadWithMultipleECDSA(typedDataHash, [PRIVATE_KEY1, PRIVATE_KEY2]);
*
* @example Sign typed data hash.
*
* import { utils } from "zksync-ethers";
* import { TypedDataEncoder } from "ethers";
*
* const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
* const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
*
* const typedDataHash = TypedDataEncoder.hash(
* {name: 'Example', version: '1', chainId: 270},
* {
* Person: [
* {name: 'name', type: 'string'},
* {name: 'age', type: 'uint8'},
* ],
* },
* {name: 'John', age: 30}
* );
* const result = await utils.signPayloadWithMultipleECDSA(typedDataHash, [PRIVATE_KEY1, PRIVATE_KEY2]);
*/
export const signPayloadWithMultipleECDSA = async (payload, secret) => {
if (!Array.isArray(secret) || secret.length < 2) {
throw new Error('Multiple keys are required for multisig signing!');
}
const signatures = secret.map(key =>
// Note, that `signMessage` wouldn't work here, since we don't want
// the signed hash to be prefixed with `\x19Ethereum Signed Message:\n`
ethers.Signature.from(new ethers.Wallet(key).signingKey.sign(payload))
.serialized);
return ethers.concat(signatures);
};
/**
* Populates missing properties meant for signing using an ECDSA private key:
*
* - Populates `from` using the address derived from the ECDSA private key.
* - Populates `nonce` via `provider.getTransactionCount(tx.from, "pending")`.
* - Populates `gasLimit` via `provider.estimateGas(tx)`. If `tx.from` is not EOA, the estimation is done with address
* derived from the ECDSA private key.
* - Populates `chainId` via `provider.getNetwork()`.
* - Populates `type` with `utils.EIP712_TX_TYPE`.
* - Populates `value` by converting to `bigint` if set, otherwise to `0n`.
* - Populates `data` with `0x`.
* - Populates `customData` with `{factoryDeps=[], gasPerPubdata=utils.DEFAULT_GAS_PER_PUBDATA_LIMIT}`.
*
* @param tx The transaction that needs to be populated.
* @param [secret] The ECDSA private key used for populating the transaction.
* @param [provider] The provider is used to fetch data from the network if it is required for signing.
*
* @throws {Error} Requires `provider` to be set.
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const PRIVATE_KEY = "<PRIVATE_KEY>";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
*
* const populatedTx = await utils.populateTransactionECDSA(
* {
* chainId: 270,
* to: "<RECEIVER>",
* value: 7_000_000_000,
* },
* PRIVATE_KEY,
* provider
* );
*/
export const populateTransactionECDSA = async (tx, secret, provider) => {
var _a, _b;
if (!provider) {
throw new Error('Provider is required but is not provided!');
}
const populatedTx = { ...tx };
populatedTx.type = EIP712_TX_TYPE;
populatedTx.chainId ?? (populatedTx.chainId = (await provider.getNetwork()).chainId);
populatedTx.value = populatedTx.value ? BigInt(populatedTx.value) : 0n;
populatedTx.data ?? (populatedTx.data = '0x');
populatedTx.customData = tx.customData ?? {};
(_a = populatedTx.customData).factoryDeps ?? (_a.factoryDeps = []);
populatedTx.from ?? (populatedTx.from = new ethers.Wallet(secret).address);
if (populatedTx.gasPrice &&
(populatedTx.maxFeePerGas || populatedTx.maxPriorityFeePerGas)) {
throw new Error('Provide combination of maxFeePerGas and maxPriorityFeePerGas or provide gasPrice. Not both!');
}
if (!populatedTx.gasLimit ||
(!populatedTx.gasPrice &&
(!populatedTx.maxFeePerGas ||
populatedTx.maxPriorityFeePerGas === null ||
populatedTx.maxPriorityFeePerGas === undefined))) {
let fromToUse = populatedTx.from;
const isContractAccount = ethers.getBytes(await provider.getCode(populatedTx.from)).length !== 0;
if (isContractAccount) {
// Gas estimation does not work when initiator is contract account (works only with EOA).
// In order to estimation gas, the transaction's from value is replaced with signer's address.
fromToUse = new ethers.Wallet(secret).address;
}
const fee = await provider.estimateFee({
...populatedTx,
from: fromToUse,
});
populatedTx.gasLimit ?? (populatedTx.gasLimit = fee.gasLimit);
(_b = populatedTx.customData).gasPerPubdata ?? (_b.gasPerPubdata = fee.gasPerPubdataLimit);
if (!populatedTx.gasPrice) {
populatedTx.maxFeePerGas ?? (populatedTx.maxFeePerGas = fee.maxFeePerGas);
populatedTx.maxPriorityFeePerGas ?? (populatedTx.maxPriorityFeePerGas = fee.maxPriorityFeePerGas);
}
}
populatedTx.nonce ?? (populatedTx.nonce = await provider.getTransactionCount(populatedTx.from, 'pending'));
return populatedTx;
};
/**
* Populates missing properties meant for signing using multiple ECDSA private keys.
* It uses {@link populateTransactionECDSA}, where the address of the first ECDSA key is set as the `secret` argument.
*
* @param tx The transaction that needs to be populated.
* @param [secret] The list of the ECDSA private keys used for populating the transaction.
* @param [provider] The provider is used to fetch data from the network if it is required for signing.
*
* @throws {Error} The `secret` must be an array of at least two elements.
*
* @example
*
* import { Provider, types, utils } from "zksync-ethers";
*
* const PRIVATE_KEY1 = "<PRIVATE_KEY1>";
* const PRIVATE_KEY2 = "<PRIVATE_KEY2>";
*
* const provider = Provider.getDefaultProvider(types.Network.Sepolia);
*
* const populatedTx = await utils.populateTransactionMultisigECDSA(
* {
* chainId: 270,
* to: "<RECEIVER>",
* value: 7_000_000_000,
* },
* [PRIVATE_KEY1, PRIVATE_KEY2],
* provider
* );
*/
export const populateTransactionMultisigECDSA = async (tx, secret, provider) => {
if (!Array.isArray(secret) || secret.length < 2) {
throw new Error('Multiple keys are required to build the transaction!');
}
// estimates gas accepts only one address, so the first signer is chosen.
return populateTransactionECDSA(tx, secret[0], provider);
};
//# sourceMappingURL=smart-account-utils.js.map