truffle-hdwallet-provider-klaytn
Version:
HD Wallet-enabled Web3 provider for Klaytn
335 lines (300 loc) • 10.7 kB
text/typescript
import * as bip39 from "ethereum-cryptography/bip39";
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
import * as EthUtil from "ethereumjs-util";
import ethJSWallet from "ethereumjs-wallet";
import { hdkey as EthereumHDKey } from "ethereumjs-wallet";
// @ts-ignore
import ProviderEngine from "@trufflesuite/web3-provider-engine";
import FiltersSubprovider from "@trufflesuite/web3-provider-engine/subproviders/filters";
import NonceSubProvider from "@trufflesuite/web3-provider-engine/subproviders/nonce-tracker";
import HookedSubprovider from "@trufflesuite/web3-provider-engine/subproviders/hooked-wallet";
// import ProviderSubprovider from "@trufflesuite/web3-provider-engine/subproviders/provider";
const ProviderSubprovider = require("./subproviders/provider.js");
// @ts-ignore
import RpcProvider from "@trufflesuite/web3-provider-engine/subproviders/rpc";
// @ts-ignore
import WebsocketProvider from "@trufflesuite/web3-provider-engine/subproviders/websocket";
import Url from "url";
import type {
JSONRPCRequestPayload,
JSONRPCErrorCallback,
JSONRPCResponsePayload
} from "ethereum-protocol";
import type { Callback, JsonRPCResponse } from "web3/providers";
import type { ConstructorArguments } from "./constructor/ConstructorArguments";
import { getOptions } from "./constructor/getOptions";
import { getPrivateKeys } from "./constructor/getPrivateKeys";
import { getMnemonic } from "./constructor/getMnemonic";
import type { ChainId, ChainSettings, Hardfork } from "./constructor/types";
import type { txResult } from "./constructor/types";
const Caver = require("caver-js");
// Important: do not use debug module. Reason: https://github.com/trufflesuite/truffle/issues/2374#issuecomment-536109086
// This line shares nonce state across multiple provider instances. Necessary
// because within truffle the wallet is repeatedly newed if it's declared in the config within a
// function, resetting nonce from tx to tx. An instance can opt out
// of this behavior by passing `shareNonce=false` to the constructor.
// See issue #65 for more
const singletonNonceSubProvider = new NonceSubProvider();
class HDWalletProvider {
private hdwallet?: EthereumHDKey;
private walletHdpath: string;
private wallets: { [address: string]: ethJSWallet };
private addresses: string[];
private chainId?: ChainId;
private chainSettings: ChainSettings;
private hardfork: Hardfork;
private initialized: Promise<void>;
public engine: ProviderEngine;
constructor(...args: ConstructorArguments) {
const {
providerOrUrl, // required
addressIndex = 0,
numberOfAddresses = 10,
shareNonce = true,
derivationPath = `m/44'/60'/0'/0/`,
pollingInterval = 4000,
chainId,
chainSettings = {},
// what's left is either a mnemonic or a list of private keys
...signingAuthority
} = getOptions(...args);
const mnemonic = getMnemonic(signingAuthority);
const privateKeys = getPrivateKeys(signingAuthority);
this.walletHdpath = derivationPath;
this.wallets = {};
this.addresses = [];
this.chainSettings = chainSettings;
this.engine = new ProviderEngine({
pollingInterval
});
if (!HDWalletProvider.isValidProvider(providerOrUrl)) {
throw new Error(
[
`Malformed provider URL: '${providerOrUrl}'`,
"Please specify a correct URL, using the http, https, ws, or wss protocol.",
""
].join("\n")
);
}
if (mnemonic && mnemonic.phrase) {
this.checkBIP39Mnemonic({
...mnemonic,
addressIndex,
numberOfAddresses
});
} else if (privateKeys) {
const options = Object.assign({}, { privateKeys }, { addressIndex });
this.ethUtilValidation(options);
} // no need to handle else case here, since matchesNewOptions() covers it
if (this.addresses.length === 0) {
throw new Error(
`Could not create addresses from your mnemonic or private key(s). ` +
`Please check that your inputs are correct.`
);
}
const tmpAccounts = this.addresses;
const tmpWallets = this.wallets;
// if user supplied the chain id, use that - otherwise fetch it
if (
typeof chainId !== "undefined" ||
(chainSettings && typeof chainSettings.chainId !== "undefined")
) {
this.chainId = chainId || chainSettings.chainId;
this.initialized = Promise.resolve();
} else {
this.initialized = this.initialize();
}
// EIP155 compliant transactions are enabled for hardforks later
// than or equal to "spurious dragon"
this.hardfork =
chainSettings && chainSettings.hardfork
? chainSettings.hardfork
: "istanbul";
this.engine.addProvider(
new HookedSubprovider({
getAccounts(cb: any) {
cb(null, tmpAccounts);
},
getPrivateKey(address: string, cb: any) {
if (!tmpWallets[address]) {
return cb("Account not found");
} else {
cb(null, tmpWallets[address].getPrivateKey().toString("hex"));
}
},
signTransaction(txParams: any, cb: any) {
let pkey;
const from = txParams.from.toLowerCase();
if (tmpWallets[from]) {
pkey = "0x" + tmpWallets[from].getPrivateKey().toString("hex");
} else {
cb("Account not found");
}
const caver = new Caver(providerOrUrl);
if(!txParams.gas)
txParams.gas = "0x500000";
caver.klay.accounts
.signTransaction(txParams, pkey)
.then((r: txResult) => {
cb(null, r.rawTransaction);
})
.catch(console.log);
},
signMessage({ data, from }: any, cb: any) {
const dataIfExists = data;
if (!dataIfExists) {
cb("No data to sign");
}
if (!tmpWallets[from]) {
cb("Account not found");
}
let pkey = tmpWallets[from].getPrivateKey();
const dataBuff = EthUtil.toBuffer(dataIfExists);
const msgHashBuff = EthUtil.hashPersonalMessage(dataBuff);
const sig = EthUtil.ecsign(msgHashBuff, pkey);
const rpcSig = EthUtil.toRpcSig(sig.v, sig.r, sig.s);
cb(null, rpcSig);
},
signPersonalMessage(...args: any[]) {
this.signMessage(...args);
}
})
);
!shareNonce
? this.engine.addProvider(new NonceSubProvider())
: this.engine.addProvider(singletonNonceSubProvider);
this.engine.addProvider(new FiltersSubprovider());
if (typeof providerOrUrl === "string") {
const url = providerOrUrl;
const providerProtocol = (
Url.parse(url).protocol || "http:"
).toLowerCase();
switch (providerProtocol) {
case "ws:":
case "wss:":
this.engine.addProvider(new WebsocketProvider({ rpcUrl: url }));
break;
default:
this.engine.addProvider(new RpcProvider({ rpcUrl: url }));
}
} else {
const provider = providerOrUrl;
this.engine.addProvider(new ProviderSubprovider(provider));
}
// Required by the provider engine.
this.engine.start((err: any) => {
if (err) throw err;
});
}
private initialize(): Promise<void> {
return new Promise((resolve, reject) => {
this.engine.sendAsync(
{
jsonrpc: "2.0",
id: Date.now(),
method: "eth_chainId",
params: []
},
(error: any, response: JSONRPCResponsePayload & { error?: any }) => {
if (error) {
reject(error);
return;
} else if (response.error) {
reject(response.error);
return;
}
if (isNaN(parseInt(response.result, 16))) {
const message =
"When requesting the chain id from the node, it" +
`returned the malformed result ${response.result}.`;
throw new Error(message);
}
this.chainId = parseInt(response.result, 16);
resolve();
}
);
});
}
// private helper to check if given mnemonic uses BIP39 passphrase protection
private checkBIP39Mnemonic({
addressIndex,
numberOfAddresses,
phrase,
password
}: {
addressIndex: number;
numberOfAddresses: number;
phrase: string;
password?: string;
}) {
this.hdwallet = EthereumHDKey.fromMasterSeed(
bip39.mnemonicToSeedSync(phrase, password)
);
if (!bip39.validateMnemonic(phrase, wordlist)) {
throw new Error("Mnemonic invalid or undefined");
}
// crank the addresses out
for (let i = addressIndex; i < addressIndex + numberOfAddresses; i++) {
const wallet = this.hdwallet
.derivePath(this.walletHdpath + i)
.getWallet();
const addr = `0x${wallet.getAddress().toString("hex")}`;
this.addresses.push(addr);
this.wallets[addr] = wallet;
}
}
// private helper leveraging ethUtils to populate wallets/addresses
private ethUtilValidation({
addressIndex,
privateKeys
}: {
addressIndex: number;
privateKeys: string[];
}) {
// crank the addresses out
for (let i = addressIndex; i < privateKeys.length; i++) {
const privateKey = Buffer.from(privateKeys[i].replace("0x", ""), "hex");
if (EthUtil.isValidPrivate(privateKey)) {
const wallet = ethJSWallet.fromPrivateKey(privateKey);
const address = wallet.getAddressString();
this.addresses.push(address);
this.wallets[address] = wallet;
}
}
}
public send(
payload: JSONRPCRequestPayload,
callback: JSONRPCErrorCallback | Callback<JsonRPCResponse>
): void {
this.initialized.then(() => {
this.engine.send(payload, callback);
});
}
public sendAsync(
payload: JSONRPCRequestPayload,
callback: JSONRPCErrorCallback | Callback<JsonRPCResponse>
): void {
this.initialized.then(() => {
this.engine.sendAsync(payload, callback);
});
}
public getAddress(idx?: number): string {
if (!idx) {
return this.addresses[0];
} else {
return this.addresses[idx];
}
}
public getAddresses(): string[] {
return this.addresses;
}
public static isValidProvider(provider: string | any): boolean {
const validProtocols = ["http:", "https:", "ws:", "wss:"];
if (typeof provider === "string") {
const url = Url.parse(provider.toLowerCase());
return !!(validProtocols.includes(url.protocol || "") && url.slashes);
}
return true;
}
}
export = HDWalletProvider;