PHP WebShell
Текущая директория: /opt/BitGoJS/modules/abstract-eth/src
Просмотр файла: ethLikeToken.ts
/**
* @prettier
*/
import { coins, EthLikeTokenConfig, tokens, EthereumNetwork as EthLikeNetwork, ethGasConfigs } from '@bitgo/statics';
import _ from 'lodash';
import { bip32 } from '@bitgo/secp256k1';
import { BigNumber } from 'bignumber.js';
import { BitGoBase, CoinConstructor, NamedCoinConstructor, getIsUnsignedSweep, Util } from '@bitgo/sdk-core';
import {
TransactionBuilder as EthLikeTransactionBuilder,
TransferBuilder as EthLikeTransferBuilder,
KeyPair as KeyPairLib,
} from './lib';
import {
AbstractEthLikeNewCoins,
optionalDeps,
TransactionPrebuild,
RecoverOptions,
RecoveryInfo,
OfflineVaultTxInfo,
} from './abstractEthLikeNewCoins';
export type CoinNames = {
[network: string]: string;
};
export class EthLikeToken extends AbstractEthLikeNewCoins {
public readonly tokenConfig: EthLikeTokenConfig;
protected readonly sendMethodName: 'sendMultiSig' | 'sendMultiSigToken';
protected constructor(bitgo: BitGoBase, tokenConfig: EthLikeTokenConfig, coinNames: CoinNames) {
const staticsCoin = coins.get(coinNames[tokenConfig.network]);
super(bitgo, staticsCoin);
this.tokenConfig = tokenConfig;
this.sendMethodName = 'sendMultiSigToken';
}
static createTokenConstructor(config: EthLikeTokenConfig, coinNames: CoinNames): CoinConstructor {
return (bitgo: BitGoBase) => new this(bitgo, config, coinNames);
}
static createTokenConstructors(
coinNames: CoinNames,
tokenConfigs: EthLikeTokenConfig[] = [
...tokens.bitcoin[coinNames.Mainnet].tokens,
...tokens.testnet[coinNames.Mainnet].tokens,
]
): NamedCoinConstructor[] {
const tokensCtors: NamedCoinConstructor[] = [];
for (const token of tokenConfigs) {
const tokenConstructor = this.createTokenConstructor(token, coinNames);
tokensCtors.push({ name: token.type, coinConstructor: tokenConstructor });
tokensCtors.push({ name: token.tokenContractAddress, coinConstructor: tokenConstructor });
}
return tokensCtors;
}
get type(): string {
return this.tokenConfig.type;
}
get name(): string {
return this.tokenConfig.name;
}
get coin(): string {
return this.tokenConfig.coin;
}
get network(): string {
return this.tokenConfig.network;
}
get tokenContractAddress(): string {
return this.tokenConfig.tokenContractAddress;
}
get decimalPlaces(): number {
return this.tokenConfig.decimalPlaces;
}
getChain(): string {
return this.tokenConfig.type;
}
getBaseChain(): string {
return this.coin;
}
getFullName(): string {
return 'Eth Like Token';
}
getBaseFactor(): number {
return Math.pow(10, this.tokenConfig.decimalPlaces);
}
/**
* Flag for sending value of 0
* @returns {boolean} True if okay to send 0 value, false otherwise
*/
valuelessTransferAllowed(): boolean {
return false;
}
/**
* Flag for sending data along with transactions
* @returns {boolean} True if okay to send tx data (ETH), false otherwise
*/
transactionDataAllowed(): boolean {
return false;
}
isToken(): boolean {
return true;
}
getOperation(recipient, expireTime, contractSequenceId) {
const network = this.getNetwork() as EthLikeNetwork;
return [
['string', 'address', 'uint', 'address', 'uint', 'uint'],
[
network.tokenOperationHashPrefix,
new optionalDeps.ethUtil.BN(optionalDeps.ethUtil.stripHexPrefix(recipient.address), 16),
recipient.amount,
new optionalDeps.ethUtil.BN(optionalDeps.ethUtil.stripHexPrefix(this.tokenContractAddress), 16),
expireTime,
contractSequenceId,
],
];
}
getSendMethodArgs(txInfo) {
// Method signature is
// sendMultiSigToken(address toAddress, uint value, address tokenContractAddress, uint expireTime, uint sequenceId, bytes signature)
return [
{
name: 'toAddress',
type: 'address',
value: txInfo.recipient.address,
},
{
name: 'value',
type: 'uint',
value: txInfo.recipient.amount,
},
{
name: 'tokenContractAddress',
type: 'address',
value: this.tokenContractAddress,
},
{
name: 'expireTime',
type: 'uint',
value: txInfo.expireTime,
},
{
name: 'sequenceId',
type: 'uint',
value: txInfo.contractSequenceId,
},
{
name: 'signature',
type: 'bytes',
value: optionalDeps.ethUtil.toBuffer(optionalDeps.ethUtil.addHexPrefix(txInfo.signature)),
},
];
}
/**
* Builds a token recovery transaction without BitGo
* @param params
* @param params.userKey {String} [encrypted] xprv
* @param params.backupKey {String} [encrypted] xprv or xpub if the xprv is held by a KRS providers
* @param params.walletPassphrase {String} used to decrypt userKey and backupKey
* @param params.walletContractAddress {String} the ETH address of the wallet contract
* @param params.recoveryDestination {String} target address to send recovered funds to
* @param params.krsProvider {String} necessary if backup key is held by KRS
* @param params.tokenContractAddress {String} contract address for token to recover
*/
async recover(params: RecoverOptions): Promise<RecoveryInfo | OfflineVaultTxInfo> {
if (_.isUndefined(params.userKey)) {
throw new Error('missing userKey');
}
if (_.isUndefined(params.backupKey)) {
throw new Error('missing backupKey');
}
if (_.isUndefined(params.walletPassphrase) && !params.userKey.startsWith('xpub')) {
throw new Error('missing wallet passphrase');
}
if (_.isUndefined(params.walletContractAddress) || !this.isValidAddress(params.walletContractAddress)) {
throw new Error('invalid walletContractAddress');
}
if (_.isUndefined(params.tokenContractAddress) || !this.isValidAddress(params.tokenContractAddress)) {
throw new Error('invalid tokenContractAddress');
}
if (_.isUndefined(params.recoveryDestination) || !this.isValidAddress(params.recoveryDestination)) {
throw new Error('invalid recoveryDestination');
}
const isUnsignedSweep = getIsUnsignedSweep(params);
// Clean up whitespace from entered values
let userKey = params.userKey.replace(/\s/g, '');
const backupKey = params.backupKey.replace(/\s/g, '');
const gasLimit = new optionalDeps.ethUtil.BN(this.setGasLimit(params.gasLimit));
const gasPrice = params.eip1559
? new optionalDeps.ethUtil.BN(params.eip1559.maxFeePerGas)
: new optionalDeps.ethUtil.BN(this.setGasPrice(params.gasPrice));
// Decrypt private keys from KeyCard values
if (!userKey.startsWith('xpub') && !userKey.startsWith('xprv')) {
try {
userKey = this.bitgo.decrypt({
input: userKey,
password: params.walletPassphrase,
});
} catch (e) {
throw new Error(`Error decrypting user keychain: ${e.message}`);
}
}
let backupKeyAddress;
let backupSigningKey;
if (isUnsignedSweep) {
const backupHDNode = bip32.fromBase58(backupKey);
backupSigningKey = backupHDNode.publicKey;
backupKeyAddress = `0x${optionalDeps.ethUtil.publicToAddress(backupSigningKey, true).toString('hex')}`;
} else {
let backupPrv;
try {
backupPrv = this.bitgo.decrypt({
input: backupKey,
password: params.walletPassphrase,
});
} catch (e) {
throw new Error(`Error decrypting backup keychain: ${e.message}`);
}
const keyPair = new KeyPairLib({ prv: backupPrv });
backupSigningKey = keyPair.getKeys().prv;
if (!backupSigningKey) {
throw new Error('no private key');
}
backupKeyAddress = keyPair.getAddress();
}
// Get nonce for backup key (should be 0)
let backupKeyNonce = 0;
const result = await this.recoveryBlockchainExplorerQuery({
module: 'account',
action: 'txlist',
address: backupKeyAddress,
});
const backupKeyTxList = result.result;
if (backupKeyTxList.length > 0) {
// Calculate last nonce used
const outgoingTxs = backupKeyTxList.filter((tx) => tx.from === backupKeyAddress);
backupKeyNonce = outgoingTxs.length;
}
// get balance of backup key and make sure we can afford gas
const backupKeyBalance = await this.queryAddressBalance(backupKeyAddress);
let totalGasNeeded = gasPrice.mul(gasLimit);
// On optimism chain, L1 fees is to be paid as well apart from L2 fees
// So we are adding the amount that can be used up as l1 fees
if (this.staticsCoin?.family === 'opeth') {
totalGasNeeded = totalGasNeeded.add(new optionalDeps.ethUtil.BN(ethGasConfigs.opethGasL1Fees));
}
const weiToGwei = 10 ** 9;
if (backupKeyBalance.lt(totalGasNeeded)) {
throw new Error(
`Backup key address ${backupKeyAddress} has balance ${(backupKeyBalance / weiToGwei).toString()} Gwei.` +
`This address must have a balance of at least ${(totalGasNeeded / weiToGwei).toString()}` +
` Gwei to perform recoveries. Try sending some funds to this address then retry.`
);
}
// get token balance of wallet
const txAmount = await this.queryAddressTokenBalance(
params.tokenContractAddress as string,
params.walletContractAddress
);
if (new BigNumber(txAmount).isLessThanOrEqualTo(0)) {
throw new Error('Wallet does not have enough funds to recover');
}
// build recipients object
const recipients = [
{
address: params.recoveryDestination,
amount: txAmount.toString(10),
},
];
// Get sequence ID using contract call
await new Promise((resolve) => setTimeout(resolve, 1000));
const sequenceId = await this.querySequenceId(params.walletContractAddress);
let operationHash, signature;
if (!isUnsignedSweep) {
// Get operation hash and sign it
operationHash = this.getOperationSha3ForExecuteAndConfirm(recipients, this.getDefaultExpireTime(), sequenceId);
signature = Util.ethSignMsgHash(operationHash, Util.xprvToEthPrivateKey(userKey));
try {
Util.ecRecoverEthAddress(operationHash, signature);
} catch (e) {
throw new Error('Invalid signature');
}
}
const txInfo = {
recipient: recipients[0],
expireTime: this.getDefaultExpireTime(),
contractSequenceId: sequenceId,
operationHash: operationHash,
signature: signature,
gasLimit: gasLimit.toString(10),
tokenContractAddress: params.tokenContractAddress,
};
const txBuilder = this.getTransactionBuilder() as EthLikeTransactionBuilder;
txBuilder.counter(backupKeyNonce);
txBuilder.contract(params.walletContractAddress);
let txFee;
if (params.eip1559) {
txFee = {
eip1559: {
maxPriorityFeePerGas: params.eip1559.maxPriorityFeePerGas,
maxFeePerGas: params.eip1559.maxFeePerGas,
},
};
} else {
txFee = { fee: gasPrice.toString() };
}
txBuilder.fee({
...txFee,
gasLimit: gasLimit.toString(),
});
const transferBuilder = txBuilder.transfer() as EthLikeTransferBuilder;
transferBuilder
.coin(this.tokenConfig.type)
.amount(recipients[0].amount)
.contractSequenceId(sequenceId)
.expirationTime(this.getDefaultExpireTime())
.to(params.recoveryDestination);
const tx = await txBuilder.build();
if (isUnsignedSweep) {
const response: OfflineVaultTxInfo = {
txHex: tx.toBroadcastFormat(),
userKey,
backupKey,
coin: this.getChain(),
gasPrice: optionalDeps.ethUtil.bufferToInt(gasPrice).toFixed(),
gasLimit,
recipients: [txInfo.recipient],
walletContractAddress: tx.toJson().to,
amount: txInfo.recipient.amount,
backupKeyNonce,
eip1559: params.eip1559,
};
_.extend(response, txInfo);
response.nextContractSequenceId = response.contractSequenceId;
return response;
}
txBuilder
.transfer()
.coin(this.tokenConfig.type)
.key(new KeyPairLib({ prv: userKey }).getKeys().prv as string);
txBuilder.sign({ key: backupSigningKey });
const signedTx = await txBuilder.build();
return {
id: signedTx.toJson().id,
tx: signedTx.toBroadcastFormat(),
};
}
verifyCoin(txPrebuild: TransactionPrebuild): boolean {
return txPrebuild.coin === this.tokenConfig.coin && txPrebuild.token === this.tokenConfig.type;
}
/**
* Create a new transaction builder for the current chain
* @return a new transaction builder
*/
protected getTransactionBuilder(): EthLikeTransactionBuilder {
throw new Error('Method not implemented');
}
/**
* Check whether gas limit passed in by user are within our max and min bounds
* If they are not set, set them to the defaults
* @param {number} userGasLimit user defined gas limit
* @returns {number} the gas limit to use for this transaction
*/
setGasLimit(userGasLimit?: number): number {
if (!userGasLimit) {
return ethGasConfigs.defaultGasLimit;
}
const gasLimitMax = ethGasConfigs.maximumGasLimit;
const gasLimitMin = ethGasConfigs.newEthLikeCoinsMinGasLimit;
if (userGasLimit < gasLimitMin || userGasLimit > gasLimitMax) {
throw new Error(`Gas limit must be between ${gasLimitMin} and ${gasLimitMax}`);
}
return userGasLimit;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!