|
| 1 | +import { |
| 2 | + ChainId, |
| 3 | + SignedTransaction, |
| 4 | + Transaction, |
| 5 | +} from "@etherdata-blockchain/etherdata-sdk-common"; |
| 6 | +import utils from "web3-utils"; |
| 7 | +import { TransactionFactory } from "@ethereumjs/tx"; |
| 8 | +import elliptic from "elliptic"; |
| 9 | +import { Wallet } from "@ethersproject/wallet"; |
| 10 | +import { keccak256 } from "@ethersproject/keccak256"; |
| 11 | +import { getAddress, getIcapAddress } from "@ethersproject/address"; |
| 12 | + |
| 13 | +/** |
| 14 | + * Create an account object which can be used for signing |
| 15 | + */ |
| 16 | +export class Account { |
| 17 | + privateKey: string; |
| 18 | + address: string; |
| 19 | + |
| 20 | + constructor(privateKey: string) { |
| 21 | + const secp256k1 = new elliptic.ec("secp256k1"); |
| 22 | + |
| 23 | + const buffer = new Buffer(privateKey.slice(2), "hex"); |
| 24 | + const ecKey = secp256k1.keyFromPrivate(buffer); |
| 25 | + const publicKey = "0x" + ecKey.getPublic(false, "hex").slice(2); |
| 26 | + const publicHash = keccak256(publicKey); |
| 27 | + this.address = this.toChecksum("0x" + publicHash.slice(-40)); |
| 28 | + this.privateKey = privateKey; |
| 29 | + } |
| 30 | + |
| 31 | + static fromMnemonic(mnemonic: string): Account { |
| 32 | + const privateKey = Wallet.fromMnemonic(mnemonic).privateKey; |
| 33 | + return new Account(privateKey); |
| 34 | + } |
| 35 | + |
| 36 | + static randomCreate() { |
| 37 | + const privateKey = Wallet.createRandom().privateKey; |
| 38 | + return new Account(privateKey); |
| 39 | + } |
| 40 | + |
| 41 | + /** |
| 42 | + * Sign transaction. Will automatically use current wallet address to as the from field. |
| 43 | + * @param tx |
| 44 | + */ |
| 45 | + signTransaction(tx: Transaction): SignedTransaction { |
| 46 | + if (tx.chainId === undefined) { |
| 47 | + tx.chainId = ChainId.MainNet; |
| 48 | + } |
| 49 | + let privateKey = this.privateKey; |
| 50 | + this.validateTransactionForSigning(tx); |
| 51 | + |
| 52 | + tx.from = this.addressFormatter(tx.from ?? this.address); |
| 53 | + tx.data = tx.data || "0x"; |
| 54 | + tx.value = this.toHex(tx.value); |
| 55 | + tx.gasLimit = this.toHex(tx.gasLimit || tx.gas); |
| 56 | + tx.gas = this.toHex(tx.gas); |
| 57 | + tx.type = this.toHex(tx.type ?? 0); |
| 58 | + tx.nonce = this.toHex(tx.nonce); |
| 59 | + tx.chainId = this.toHex(tx.chainId); |
| 60 | + |
| 61 | + if (tx.type === "0x1" && tx.accessList === undefined) tx.accessList = []; |
| 62 | + |
| 63 | + if (privateKey.startsWith("0x")) { |
| 64 | + privateKey = privateKey.substring(2); |
| 65 | + } |
| 66 | + let newTx = TransactionFactory.fromTxData(tx); |
| 67 | + let signedTx = newTx.sign(Buffer.from(privateKey, "hex")); |
| 68 | + let validationErrors = signedTx.validate(true); |
| 69 | + if (validationErrors.length > 0) { |
| 70 | + let errorString = "Signer Error: "; |
| 71 | + for (const validationError of validationErrors) { |
| 72 | + errorString += `${errorString} ${validationError}.`; |
| 73 | + } |
| 74 | + throw new Error(errorString); |
| 75 | + } |
| 76 | + let rlpEncoded = signedTx.serialize().toString("hex"); |
| 77 | + let rawTransaction = "0x" + rlpEncoded; |
| 78 | + let transactionHash = utils.keccak256(rawTransaction); |
| 79 | + return { |
| 80 | + messageHash: |
| 81 | + "0x" + Buffer.from(signedTx.getMessageToSign(true)).toString("hex"), |
| 82 | + v: "0x" + signedTx.v!.toString(16), |
| 83 | + r: "0x" + signedTx.r!.toString(16), |
| 84 | + s: "0x" + signedTx.s!.toString(16), |
| 85 | + rawTransaction: rawTransaction, |
| 86 | + transactionHash: transactionHash, |
| 87 | + }; |
| 88 | + } |
| 89 | + |
| 90 | + private toChecksum(address: string) { |
| 91 | + return getAddress(address); |
| 92 | + } |
| 93 | + |
| 94 | + private toHex(value: string | number) { |
| 95 | + let newValue = value; |
| 96 | + // handle base16 value or base10's value in string format |
| 97 | + if (typeof newValue === "string") { |
| 98 | + if (!newValue.startsWith("0x")) { |
| 99 | + newValue = parseInt(newValue).toString(16); |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + // handle base 10's value |
| 104 | + if (typeof newValue === "number") { |
| 105 | + newValue = newValue.toString(16); |
| 106 | + } |
| 107 | + |
| 108 | + if (!newValue.startsWith("0x")) { |
| 109 | + newValue = `0x${newValue}`; |
| 110 | + } |
| 111 | + |
| 112 | + return newValue; |
| 113 | + } |
| 114 | + |
| 115 | + private addressFormatter(address: string) { |
| 116 | + return getIcapAddress(address); |
| 117 | + } |
| 118 | + |
| 119 | + private validateTransactionForSigning(tx: Transaction) { |
| 120 | + if ( |
| 121 | + !tx.gas && |
| 122 | + !tx.gasLimit && |
| 123 | + !tx.maxPriorityFeePerGas && |
| 124 | + !tx.maxFeePerGas |
| 125 | + ) { |
| 126 | + throw new Error('"gas" is missing'); |
| 127 | + } |
| 128 | + |
| 129 | + if (tx.chainId === undefined) { |
| 130 | + throw new Error('"chainId" is missing'); |
| 131 | + } |
| 132 | + |
| 133 | + if (tx.gas && tx.gasPrice) { |
| 134 | + if (tx.gas < 0 || tx.gasPrice < 0) { |
| 135 | + throw new Error("Gas or gasPrice is lower than 0"); |
| 136 | + } |
| 137 | + } else { |
| 138 | + if ((tx.maxPriorityFeePerGas ?? 0) < 0 || (tx.maxFeePerGas ?? 0) < 0) { |
| 139 | + throw new Error("maxPriorityFeePerGas or maxFeePerGas is lower than 0"); |
| 140 | + } |
| 141 | + } |
| 142 | + if (tx.nonce < 0 || tx.chainId < 0) { |
| 143 | + throw new Error("Nonce or chainId is lower than 0"); |
| 144 | + } |
| 145 | + return; |
| 146 | + } |
| 147 | +} |
0 commit comments