PHP WebShell
Текущая директория: /opt/BitGoJS/modules/abstract-eth/src/lib
Просмотр файла: transferBuilder.ts
import assert from 'assert';
import * as ethUtil from 'ethereumjs-util';
import EthereumAbi from 'ethereumjs-abi';
import BN from 'bn.js';
import { coins, BaseCoin, ContractAddressDefinedToken, EthereumNetwork as EthLikeNetwork } from '@bitgo/statics';
import { BuildTransactionError, InvalidParameterValueError } from '@bitgo/sdk-core';
import { decodeTransferData, sendMultiSigData, sendMultiSigTokenData, isValidEthAddress, isValidAmount } from './utils';
import { defaultAbiCoder, keccak256 } from 'ethers/lib/utils';
import { sendMultiSigTokenTypes, sendMultiSigTypes } from './walletUtil';
/** ETH transfer builder */
export class TransferBuilder {
private readonly _EMPTY_HEX_VALUE = '0x';
protected _amount: string;
protected _toAddress: string;
protected _sequenceId: number;
protected _signKey: string | null;
protected _expirationTime: number;
protected _signature: string;
protected _isFirstSigner: boolean | undefined;
private _data: string;
private _tokenContractAddress?: string;
private _coin: Readonly<BaseCoin>;
private _chainId?: string;
private _coinUsesNonPackedEncodingForTxData?: boolean;
private _walletVersion?: number;
constructor(serializedData?: string, isFirstSigner?: boolean) {
this._isFirstSigner = isFirstSigner;
if (serializedData) {
this.decodeTransferData(serializedData);
} else {
// initialize with default values for non mandatory fields
this._expirationTime = this.getExpirationTime();
this._data = this._EMPTY_HEX_VALUE;
this._signature = this._EMPTY_HEX_VALUE;
}
}
/**
* A method to set the native coin or ERC20 token to be transferred.
* This ERC20 token may not be compatible with the network.
*
* @param {string} coin - the native coin or ERC20 token to be set
* @returns {TransferBuilder} the transfer builder instance modified
*/
coin(coin: string): TransferBuilder {
this._coin = coins.get(coin);
if (this._coin instanceof ContractAddressDefinedToken) {
this._tokenContractAddress = this._coin.contractAddress.toString();
}
return this;
}
getIsFirstSigner(): boolean {
return this._isFirstSigner ? this._isFirstSigner : false;
}
walletVersion(version: number): TransferBuilder {
this._walletVersion = version;
return this;
}
data(additionalData: string): TransferBuilder {
this._signature = this._EMPTY_HEX_VALUE;
this._data = additionalData;
return this;
}
amount(amount: string): this {
if (!isValidAmount(amount)) {
throw new InvalidParameterValueError('Invalid amount');
}
this._signature = this._EMPTY_HEX_VALUE;
this._amount = amount;
return this;
}
to(address: string): TransferBuilder {
if (isValidEthAddress(address)) {
this._signature = this._EMPTY_HEX_VALUE;
this._toAddress = address;
return this;
}
throw new InvalidParameterValueError('Invalid address');
}
contractSequenceId(counter: number): TransferBuilder {
if (counter >= 0) {
this._signature = this._EMPTY_HEX_VALUE;
this._sequenceId = counter;
return this;
}
throw new InvalidParameterValueError('Invalid contract sequence id');
}
key(signKey: string): TransferBuilder {
this._signKey = signKey;
return this;
}
expirationTime(date: number): TransferBuilder {
if (date > 0) {
this._signature = this._EMPTY_HEX_VALUE;
this._expirationTime = date;
return this;
}
throw new InvalidParameterValueError('Invalid expiration time');
}
isFirstSigner(isFirstSigner: boolean): TransferBuilder {
this._isFirstSigner = isFirstSigner;
return this;
}
tokenContractAddress(tokenContractAddress: string): TransferBuilder {
this._tokenContractAddress = tokenContractAddress;
return this;
}
setCoinUsesNonPackedEncodingForTxData(isCoinUsesNonPackedEncodingForTxData: boolean): TransferBuilder {
this._coinUsesNonPackedEncodingForTxData = isCoinUsesNonPackedEncodingForTxData;
return this;
}
setSignature(signature: string): TransferBuilder {
this._signKey = null;
this._signature = signature;
return this;
}
signAndBuild(chainId: string, coinUsesNonPackedEncodingForTxData?: boolean): string {
this._chainId = chainId;
// If the coin uses non-packed encoding for tx data, the operation hash is calculated differently
// This new encoding type is applicable only for native coins and not tokens
this._coinUsesNonPackedEncodingForTxData =
coinUsesNonPackedEncodingForTxData && this._tokenContractAddress === undefined;
if (this.hasMandatoryFields()) {
if (this._isFirstSigner) {
// First signer signs different data than the second signer in multisig evm contracts.
return ethUtil.addHexPrefix(this.getSignatureData().toString('hex'));
} else {
if (this._tokenContractAddress !== undefined) {
return sendMultiSigTokenData(
this._toAddress,
this._amount,
this._tokenContractAddress,
this._expirationTime,
this._sequenceId,
this.getSignature()
);
} else {
return sendMultiSigData(
this._toAddress,
this._amount,
this._data,
this._expirationTime,
this._sequenceId,
this.getSignature()
);
}
}
}
throw new BuildTransactionError(
'Missing transfer mandatory fields. Amount, destination (to) address and sequenceID are mandatory'
);
}
private hasMandatoryFields(): boolean {
return this._amount !== undefined && this._toAddress !== undefined && this._sequenceId !== undefined;
}
/**
* Obtains the proper operation hash to sign either a sendMultiSig data
* or a sendMultiSigToken data
*
* @returns {string} the operation hash
*/
public getOperationHash(): string {
const operationData = this.getOperationData();
let operationHash: string;
if (this._coinUsesNonPackedEncodingForTxData) {
const types: string[] = operationData[0] as string[];
const values: (string | number)[] = operationData[1].map((item) =>
typeof item === 'string' || typeof item === 'number' ? item : '0x' + item.toString('hex')
);
operationHash = keccak256(defaultAbiCoder.encode(types, values));
} else {
// If the coin uses packed encoding for tx data or it is a token, the operation hash is calculated using the Ethereum ABI
operationHash = ethUtil.bufferToHex(EthereumAbi.soliditySHA3(...operationData));
}
return operationHash;
}
protected getOperationData(): (string | number | Buffer)[][] {
let operationData;
const prefix = this.getOperationHashPrefix();
if (this._tokenContractAddress !== undefined) {
operationData = [
['string', 'address', 'uint', 'address', 'uint', 'uint'],
[
prefix,
new BN(ethUtil.stripHexPrefix(this._toAddress), 16),
this._amount,
new BN(ethUtil.stripHexPrefix(this._tokenContractAddress), 16),
this._expirationTime,
this._sequenceId,
],
];
} else {
const toAddress = this._coinUsesNonPackedEncodingForTxData
? this._toAddress
: new BN(ethUtil.stripHexPrefix(this._toAddress), 16);
operationData = [
['string', 'address', 'uint', 'bytes', 'uint', 'uint'],
[
prefix,
toAddress,
this._amount,
Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(this._data)) || '', 'hex'),
this._expirationTime,
this._sequenceId,
],
];
}
return operationData;
}
private getOperationHashPrefix(): string {
if (this._walletVersion === 4) {
return this._tokenContractAddress ? `${this._chainId}-ERC20` : `${this._chainId}`;
}
return this._tokenContractAddress ? this.getTokenOperationHashPrefix() : this.getNativeOperationHashPrefix();
}
/**
* Get the prefix used in generating an operation hash for sending tokens
*
* @returns the string prefix
*/
protected getTokenOperationHashPrefix(): string {
return (this._coin?.network as EthLikeNetwork)?.tokenOperationHashPrefix ?? `${this._chainId}-ERC20` ?? 'ERC20';
}
/**
* Get the prefix used in generating an operation hash for sending native coins
*
* @returns the string prefix
*/
protected getNativeOperationHashPrefix(): string {
return (this._coin?.network as EthLikeNetwork)?.nativeCoinOperationHashPrefix ?? `${this._chainId}` ?? 'ETHER';
}
/** Return an expiration time, in seconds, set to one hour from now
*
* @returns {number} expiration time
*/
private getExpirationTime(): number {
const currentDate = new Date();
currentDate.setHours(currentDate.getHours() + 1);
return currentDate.getTime() / 1000;
}
/**
* If a signing key is set for this builder, recalculates the signature
*
* @returns {string} the signature value
*/
protected getSignature(): string {
if (this._signKey) {
this._signature = this.ethSignMsgHash();
}
return this._signature!;
}
protected ethSignMsgHash(): string {
const data = this.getOperationHash();
assert(this._signKey);
const keyBuffer = Buffer.from(ethUtil.padToEven(this._signKey), 'hex');
if (keyBuffer.length !== 32) {
throw new Error('private key length is invalid');
}
const signatureInParts = ethUtil.ecsign(
Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(data)), 'hex'),
keyBuffer
);
// Assemble strings from r, s and v
const r = ethUtil.setLengthLeft(signatureInParts.r, 32).toString('hex');
const s = ethUtil.setLengthLeft(signatureInParts.s, 32).toString('hex');
const v = ethUtil.stripHexPrefix(ethUtil.intToHex(signatureInParts.v));
// Concatenate the r, s and v parts to make the signature string
return ethUtil.addHexPrefix(r.concat(s, v));
}
private decodeTransferData(data: string): void {
const transferData = decodeTransferData(data, this._isFirstSigner);
this._toAddress = transferData.to;
this._amount = transferData.amount;
this._expirationTime = transferData.expireTime;
this._sequenceId = transferData.sequenceId;
this._signature = transferData.signature;
if (transferData.data) {
this._data = transferData.data;
}
if (transferData.tokenContractAddress) {
this._tokenContractAddress = transferData.tokenContractAddress;
}
}
public getSignatureData(): Buffer<ArrayBuffer> {
const method = this._tokenContractAddress
? EthereumAbi.methodID('sendMultiSigToken', sendMultiSigTokenTypes)
: EthereumAbi.methodID('sendMultiSig', sendMultiSigTypes);
const operationData = this.getOperationData();
const rawEncodedOperationData = EthereumAbi.rawEncode(...operationData);
return Buffer.concat([
method,
rawEncodedOperationData,
Buffer.from([this._coinUsesNonPackedEncodingForTxData ? 1 : 0]),
]);
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!