All files / evm-wallet-experiment/src/lib signing.ts

100% Statements 16/16
67.39% Branches 31/46
100% Functions 5/5
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167                                                                6x     6x 2x                                 2x       4x 3x                         3x       1x                   6x                                   6x 6x                             8x 8x                             1x 1x                                               6x            
import type {
  SignedAuthorization,
  TransactionSerializableEIP1559,
  TransactionSerializableEIP7702,
  TransactionSerializableLegacy,
} from 'viem';
import type { HDAccount, LocalAccount } from 'viem/accounts';
 
import type {
  Address,
  Eip712TypedData,
  Hex,
  TransactionRequest,
} from '../types.ts';
 
/**
 * Sign a transaction with the given account.
 *
 * Detects the transaction type from the request fields:
 * - `authorizationList` present → EIP-7702 (type 4)
 * - `maxFeePerGas` present → EIP-1559 (type 2)
 * - Otherwise → Legacy (type 0)
 *
 * @param options - Signing options.
 * @param options.account - The local account to sign with.
 * @param options.tx - The transaction request.
 * @returns The signed transaction as a hex string.
 */
export async function signTransaction(options: {
  account: LocalAccount;
  tx: TransactionRequest;
}): Promise<Hex> {
  const { account, tx } = options;
 
  // EIP-7702 (type 4) — authorization list present
  if (tx.authorizationList && tx.authorizationList.length > 0) {
    const eip7702Tx = {
      to: tx.to,
      type: 'eip7702' as const,
      authorizationList:
        tx.authorizationList as unknown as SignedAuthorization[],
      ...(tx.maxFeePerGas === undefined
        ? {}
        : { maxFeePerGas: BigInt(tx.maxFeePerGas) }),
      ...(tx.maxPriorityFeePerGas === undefined
        ? {}
        : { maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas) }),
      ...(tx.value === undefined ? {} : { value: BigInt(tx.value) }),
      ...(tx.data === undefined ? {} : { data: tx.data }),
      ...(tx.nonce === undefined ? {} : { nonce: tx.nonce }),
      ...(tx.gasLimit === undefined ? {} : { gas: BigInt(tx.gasLimit) }),
      ...(tx.chainId === undefined ? {} : { chainId: tx.chainId }),
    } as TransactionSerializableEIP7702;
    return account.signTransaction(eip7702Tx);
  }
 
  // EIP-1559 (type 2)
  if (tx.maxFeePerGas) {
    const eip1559Tx = {
      to: tx.to,
      type: 'eip1559' as const,
      maxFeePerGas: BigInt(tx.maxFeePerGas),
      ...(tx.value === undefined ? {} : { value: BigInt(tx.value) }),
      ...(tx.data === undefined ? {} : { data: tx.data }),
      ...(tx.nonce === undefined ? {} : { nonce: tx.nonce }),
      ...(tx.gasLimit === undefined ? {} : { gas: BigInt(tx.gasLimit) }),
      ...(tx.chainId === undefined ? {} : { chainId: tx.chainId }),
      ...(tx.maxPriorityFeePerGas === undefined
        ? {}
        : { maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas) }),
    } as TransactionSerializableEIP1559;
    return account.signTransaction(eip1559Tx);
  }
 
  // Legacy (type 0)
  const legacyTx = {
    to: tx.to,
    type: 'legacy' as const,
    ...(tx.value === undefined ? {} : { value: BigInt(tx.value) }),
    ...(tx.data === undefined ? {} : { data: tx.data }),
    ...(tx.nonce === undefined ? {} : { nonce: tx.nonce }),
    ...(tx.gasLimit === undefined ? {} : { gas: BigInt(tx.gasLimit) }),
    ...(tx.chainId === undefined ? {} : { chainId: tx.chainId }),
    ...(tx.gasPrice === undefined ? {} : { gasPrice: BigInt(tx.gasPrice) }),
  } as TransactionSerializableLegacy;
  return account.signTransaction(legacyTx);
}
 
/**
 * Sign a raw hash using ECDSA (no EIP-191 prefix).
 *
 * This is used for UserOp hash signing where the EntryPoint expects
 * a raw ECDSA signature over the hash, not a personal_sign envelope.
 *
 * @param options - Signing options.
 * @param options.account - The local account to sign with.
 * @param options.hash - The hash to sign.
 * @returns The signature as a hex string.
 */
export async function signHash(options: {
  account: HDAccount;
  hash: Hex;
}): Promise<Hex> {
  const { account, hash } = options;
  return account.sign({ hash });
}
 
/**
 * Sign a message using EIP-191 personal sign.
 *
 * @param options - Signing options.
 * @param options.account - The local account to sign with.
 * @param options.message - The message to sign.
 * @returns The signature as a hex string.
 */
export async function signMessage(options: {
  account: LocalAccount;
  message: string;
}): Promise<Hex> {
  const { account, message } = options;
  return account.signMessage({ message });
}
 
/**
 * Sign EIP-712 typed data.
 *
 * @param options - Signing options.
 * @param options.account - The local account to sign with.
 * @param options.typedData - The EIP-712 typed data payload.
 * @returns The signature as a hex string.
 */
export async function signTypedData(options: {
  account: LocalAccount;
  typedData: Eip712TypedData;
}): Promise<Hex> {
  const { account, typedData } = options;
  return account.signTypedData({
    domain: typedData.domain as Record<string, unknown>,
    types: typedData.types as Record<string, { name: string; type: string }[]>,
    primaryType: typedData.primaryType,
    message: typedData.message,
  });
}
 
/**
 * Sign an EIP-7702 authorization to delegate an EOA's code to a contract.
 *
 * @param options - Signing options.
 * @param options.account - The local account to sign with.
 * @param options.contractAddress - The implementation contract address.
 * @param options.chainId - The chain ID for the authorization.
 * @param options.nonce - The authorization nonce (required for self-execution: txNonce + 1).
 * @returns The signed authorization.
 */
export async function signAuthorization(options: {
  account: LocalAccount;
  contractAddress: Address;
  chainId: number;
  nonce?: number;
}): Promise<SignedAuthorization> {
  return options.account.signAuthorization({
    contractAddress: options.contractAddress,
    chainId: options.chainId,
    ...(options.nonce === undefined ? {} : { nonce: options.nonce }),
  });
}