|
1 |
| -import { TransactionFactory } from '@ethereumjs/tx'; |
| 1 | +import { |
| 2 | + FeeMarketEIP1559Transaction, |
| 3 | + LegacyTransaction, |
| 4 | + TransactionFactory, |
| 5 | +} from '@ethereumjs/tx'; |
2 | 6 | import { signTypedData, SignTypedDataVersion } from '@metamask/eth-sig-util';
|
3 |
| -import { bigIntToHex, bytesToHex } from '@metamask/utils'; |
| 7 | +import { bigIntToHex, bytesToBigInt, bytesToHex } from '@metamask/utils'; |
| 8 | +import { rlp } from 'ethereumjs-util'; |
4 | 9 | import { Common } from './keyring-utils';
|
5 | 10 |
|
6 | 11 | // BIP32 Public Key: xpub6ELgkkwgfoky9h9fFu4Auvx6oHvJ6XfwiS1NE616fe9Uf4H3JHtLGjCePVkb6RFcyDCqVvjXhNXbDNDqs6Kjoxw7pTAeP1GSEiLHmA5wYa9
|
@@ -174,6 +179,101 @@ export class FakeLedgerBridge extends FakeKeyringBridge {
|
174 | 179 | return Promise.resolve();
|
175 | 180 | }
|
176 | 181 |
|
| 182 | + /** |
| 183 | + * Signs a transaction using a private key. |
| 184 | + * This function supports both legacy (type 0) and EIP-1559 (type 2) transactions. |
| 185 | + * It decodes the RLP-encoded transaction, signs it, and then returns the |
| 186 | + * signature components (v, r, s) as hexadecimal strings. |
| 187 | + * |
| 188 | + * @param {object} params - The parameters object. |
| 189 | + * @param {string} params.tx - The RLP-encoded transaction as a hex string. |
| 190 | + * @returns {Promise<object>} A promise that resolves to an object containing the |
| 191 | + * signature components: |
| 192 | + * - `v`: The recovery id as a hex string. |
| 193 | + * - `r`: The R component of the signature as a hex string. |
| 194 | + * - `s`: The S component of the signature as a hex string. |
| 195 | + * @throws {Error} If the transaction type is unsupported. |
| 196 | + */ |
| 197 | + async deviceSignTransaction({ tx }) { |
| 198 | + const txBuffer = Buffer.from(tx, 'hex'); |
| 199 | + const firstByte = txBuffer[0]; |
| 200 | + let txType; |
| 201 | + let rlpData; |
| 202 | + let parsedChainId; |
| 203 | + |
| 204 | + // Determine the transaction type from the first byte of the buffer |
| 205 | + if (firstByte === 1) { |
| 206 | + txType = 1; // EIP-2930 |
| 207 | + // TODO: Add support for type 1 tx if needed, for now, error out |
| 208 | + throw new Error( |
| 209 | + 'Unsupported transaction type: EIP-2930 (type 1) not yet implemented in FakeLedgerBridge.', |
| 210 | + ); |
| 211 | + } else if (firstByte === 2) { |
| 212 | + txType = 2; // EIP-1559 |
| 213 | + rlpData = txBuffer.slice(1); |
| 214 | + const decodedRlp = rlp.decode(rlpData); |
| 215 | + parsedChainId = bytesToBigInt(decodedRlp[0]); // chainId is the first element |
| 216 | + } else { |
| 217 | + txType = 0; // Legacy |
| 218 | + rlpData = txBuffer; |
| 219 | + const decodedRlp = rlp.decode(rlpData); |
| 220 | + // For legacy tx, getMessageToSign(false) includes chainId as the 7th element (index 6) |
| 221 | + // [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0] |
| 222 | + parsedChainId = bytesToBigInt(decodedRlp[6]); |
| 223 | + } |
| 224 | + |
| 225 | + const common = Common.custom({ |
| 226 | + chain: { |
| 227 | + name: 'localhost', |
| 228 | + // Use the parsed chainId. networkId can be the same. |
| 229 | + chainId: parsedChainId, |
| 230 | + networkId: parsedChainId, |
| 231 | + }, |
| 232 | + // Ensure hardfork is appropriate for the transaction type |
| 233 | + hardfork: txType === 2 ? 'london' : 'muirGlacier', |
| 234 | + }); |
| 235 | + |
| 236 | + // removing r, s, v values from the unsigned tx |
| 237 | + // Ledger uses v to communicate the chain ID, but we're removing it because these values are not a valid signature at this point. |
| 238 | + |
| 239 | + // Type 1 and type 2 transactions have an explicit type set in the first element of the array |
| 240 | + // Type 0 transactions do not have a specific type byte and are identified by their RLP encoding |
| 241 | + |
| 242 | + // TODO: add support to type 1 transactions (already handled by throwing error) |
| 243 | + if (txType === 0) { |
| 244 | + const rlpTx = rlp.decode(rlpData); |
| 245 | + |
| 246 | + // For legacy tx, fromValuesArray expects [nonce, gasPrice, gasLimit, to, value, data] |
| 247 | + // or [nonce, gasPrice, gasLimit, to, value, data, v, r, s] |
| 248 | + // Since our rlpTx is [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0], |
| 249 | + // we should pass the first 6 elements, and `common` will handle EIP-155. |
| 250 | + const signedTx = LegacyTransaction.fromValuesArray(rlpTx.slice(0, 6), { |
| 251 | + common, |
| 252 | + }).sign(Buffer.from(KNOWN_PRIVATE_KEYS[0], 'hex')); |
| 253 | + return { |
| 254 | + v: bigIntToHex(signedTx.v), |
| 255 | + r: bigIntToHex(signedTx.r), |
| 256 | + s: bigIntToHex(signedTx.s), |
| 257 | + }; |
| 258 | + } else if (txType === 2) { |
| 259 | + const rlpTx = rlp.decode(rlpData); |
| 260 | + |
| 261 | + // For EIP-1559 tx, fromValuesArray expects: |
| 262 | + // [chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList] |
| 263 | + // or with v, r, s. Our rlpTx matches the unsigned form. |
| 264 | + const signedTx = FeeMarketEIP1559Transaction.fromValuesArray(rlpTx, { |
| 265 | + common, |
| 266 | + }).sign(Buffer.from(KNOWN_PRIVATE_KEYS[0], 'hex')); |
| 267 | + return { |
| 268 | + v: bigIntToHex(signedTx.v), |
| 269 | + r: bigIntToHex(signedTx.r), |
| 270 | + s: bigIntToHex(signedTx.s), |
| 271 | + }; |
| 272 | + } |
| 273 | + |
| 274 | + throw new Error('Unsupported transaction type.'); |
| 275 | + } |
| 276 | + |
177 | 277 | updateTransportMethod() {
|
178 | 278 | return true;
|
179 | 279 | }
|
|
0 commit comments