zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
284 lines (271 loc) • 9.09 kB
text/typescript
import {ethers, SigningKey} from 'ethers';
import {EIP712_TX_TYPE} from './utils';
import {TransactionLike, TransactionBuilder, PayloadSigner} from './types';
/**
* 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: PayloadSigner = async (
payload,
secret: string | SigningKey
) => {
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: PayloadSigner = async (
payload,
secret: string[] | SigningKey[]
) => {
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: TransactionBuilder = async (
tx,
secret: string | SigningKey,
provider
) => {
if (!provider) {
throw new Error('Provider is required but is not provided!');
}
const populatedTx = {...tx};
populatedTx.type = EIP712_TX_TYPE;
populatedTx.chainId ??= (await provider.getNetwork()).chainId;
populatedTx.value = populatedTx.value ? BigInt(populatedTx.value) : 0n;
populatedTx.data ??= '0x';
populatedTx.customData = tx.customData ?? {};
populatedTx.customData.factoryDeps ??= [];
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 ??= fee.gasLimit;
populatedTx.customData.gasPerPubdata ??= fee.gasPerPubdataLimit;
if (!populatedTx.gasPrice) {
populatedTx.maxFeePerGas ??= fee.maxFeePerGas;
populatedTx.maxPriorityFeePerGas ??= fee.maxPriorityFeePerGas;
}
}
populatedTx.nonce ??= await provider.getTransactionCount(
populatedTx.from,
'pending'
);
return populatedTx as TransactionLike;
};
/**
* 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: TransactionBuilder = async (
tx,
secret: string[] | SigningKey[],
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);
};