PHP WebShell
Текущая директория: /opt/BitGoJS/modules/abstract-eth/src/lib
Просмотр файла: transactionBuilder.ts
import { BaseCoin as CoinConfig, EthereumNetwork, CoinFeature } from '@bitgo/statics';
import EthereumCommon from '@ethereumjs/common';
import EthereumAbi from 'ethereumjs-abi';
import BigNumber from 'bignumber.js';
import * as ethUtil from 'ethereumjs-util';
import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx';
import {
BaseAddress,
BaseKey,
BaseTransaction,
BaseTransactionBuilder,
BuildTransactionError,
InvalidTransactionError,
isValidPrv,
isValidXprv,
ParseTransactionError,
SigningError,
TransactionType,
} from '@bitgo/sdk-core';
import { KeyPair } from './keyPair';
import { ETHTransactionType, Fee, SignatureParts, TxData } from './iface';
import {
calculateForwarderAddress,
calculateForwarderV1Address,
classifyTransaction,
decodeForwarderCreationData,
decodeFlushTokensData,
decodeWalletCreationData,
flushCoinsData,
flushTokensData,
getAddressInitDataAllForwarderVersions,
getCommon,
getProxyInitcode,
hasSignature,
isValidEthAddress,
getV1WalletInitializationData,
getCreateForwarderParamsAndTypes,
} from './utils';
import { defaultWalletVersion, walletSimpleConstructor } from './walletUtil';
import { ERC1155TransferBuilder } from './transferBuilders/transferBuilderERC1155';
import { ERC721TransferBuilder } from './transferBuilders/transferBuilderERC721';
import { Transaction } from './transaction';
import { TransferBuilder } from './transferBuilder';
const DEFAULT_M = 3;
/**
* EthereumLike transaction builder.
*/
export abstract class TransactionBuilder extends BaseTransactionBuilder {
protected _type: TransactionType;
// Specifies common chain and hardfork parameters.
protected _common: EthereumCommon;
protected _sourceKeyPair: KeyPair;
private _transaction: Transaction;
private _counter: number;
private _fee: Fee;
protected _value: string;
// the signature on the external ETH transaction
private _txSignature: SignatureParts;
// Wallet initialization transaction parameters
private _walletOwnerAddresses: string[];
protected _walletVersion: number;
// flush tokens parameters
private _forwarderAddress: string;
private _tokenAddress: string;
// Send and AddressInitialization transaction specific parameters
protected _transfer: TransferBuilder | ERC721TransferBuilder | ERC1155TransferBuilder;
private _contractAddress: string;
private _contractCounter: number;
private _forwarderVersion: number;
private _initCode: string;
private _baseAddress: string;
private _feeAddress: string;
// generic contract call builder
// encoded contract call hex
private _data: string;
// Common parameter for wallet initialization and address initialization transaction
private _salt: string;
// walletsimplebytecode
protected _walletSimpleByteCode: string;
/**
* Public constructor.
*
* @param _coinConfig
*/
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._common = getCommon(this._coinConfig.network as EthereumNetwork);
this._type = TransactionType.Send;
this._counter = 0;
this._value = '0';
this._walletOwnerAddresses = [];
this._forwarderVersion = 0;
this._walletVersion = 0;
this.transaction = new Transaction(this._coinConfig, this._common);
this._walletSimpleByteCode = '';
}
/** @inheritdoc */
protected async buildImplementation(): Promise<BaseTransaction> {
const transactionData = this.getTransactionData();
if (this._txSignature) {
Object.assign(transactionData, this._txSignature);
}
this.transaction.setTransactionType(this._type);
transactionData.from = this._sourceKeyPair ? this._sourceKeyPair.getAddress() : undefined;
this.transaction.setTransactionData(
transactionData,
this._transfer ? this._transfer.getIsFirstSigner() : undefined
);
// Build and sign a new transaction based on the latest changes
if (this._sourceKeyPair && this._sourceKeyPair.getKeys().prv) {
await this.transaction.sign(this._sourceKeyPair);
}
return this.transaction;
}
protected getTransactionData(): TxData {
switch (this._type) {
case TransactionType.WalletInitialization:
return this.buildWalletInitializationTransaction(this._walletVersion);
case TransactionType.RecoveryWalletDeployment:
return this.buildBase(this._data);
case TransactionType.Send:
case TransactionType.SendERC721:
case TransactionType.SendERC1155:
return this.buildSendTransaction();
case TransactionType.AddressInitialization:
return this.buildAddressInitializationTransaction();
case TransactionType.FlushTokens:
return this.buildFlushTokensTransaction();
case TransactionType.FlushCoins:
return this.buildFlushCoinsTransaction();
case TransactionType.SingleSigSend:
return this.buildBase('0x');
case TransactionType.ContractCall:
return this.buildGenericContractCallTransaction();
default:
throw new BuildTransactionError('Unsupported transaction type');
}
}
/** @inheritdoc */
protected fromImplementation(rawTransaction: string, isFirstSigner?: boolean): Transaction {
let tx: Transaction;
if (/^0x?[0-9a-f]{1,}$/.test(rawTransaction.toLowerCase())) {
tx = Transaction.fromSerialized(this._coinConfig, this._common, rawTransaction, isFirstSigner);
this.loadBuilderInput(tx.toJson(), isFirstSigner);
} else {
const txData = JSON.parse(rawTransaction);
tx = new Transaction(this._coinConfig, txData);
}
return tx;
}
/**
* Load the builder data using the deserialized transaction
*
* @param {TxData} transactionJson the deserialized transaction json
* @param {boolean} isFirstSigner if the transaction is being signed by the first signer
*/
protected loadBuilderInput(transactionJson: TxData, isFirstSigner?: boolean): void {
const decodedType = classifyTransaction(transactionJson.data);
this.type(decodedType);
this.counter(transactionJson.nonce);
this.value(transactionJson.value);
if (transactionJson._type === ETHTransactionType.LEGACY) {
this.fee({
fee: transactionJson.gasPrice,
gasPrice: transactionJson.gasPrice,
gasLimit: transactionJson.gasLimit,
});
} else {
this.fee({
gasLimit: transactionJson.gasLimit,
fee: transactionJson.maxFeePerGas,
eip1559: {
maxFeePerGas: transactionJson.maxFeePerGas,
maxPriorityFeePerGas: transactionJson.maxPriorityFeePerGas,
},
});
}
if (hasSignature(transactionJson)) {
this._txSignature = { v: transactionJson.v!, r: transactionJson.r!, s: transactionJson.s! };
}
this.setTransactionTypeFields(decodedType, transactionJson, isFirstSigner);
}
protected setTransactionTypeFields(
decodedType: TransactionType,
transactionJson: TxData,
isFirstSigner?: boolean
): void {
switch (decodedType) {
case TransactionType.WalletInitialization:
const { owners, salt } = decodeWalletCreationData(transactionJson.data);
owners.forEach((element) => {
this.owner(element);
});
if (salt) {
this.salt(salt as string);
this.walletVersion(1);
this.setContract(transactionJson.to);
}
break;
case TransactionType.RecoveryWalletDeployment:
this.data(transactionJson.data);
break;
case TransactionType.FlushTokens:
this.setContract(transactionJson.to);
const { forwarderAddress, tokenAddress, forwarderVersion } = decodeFlushTokensData(
transactionJson.data,
transactionJson.to
);
if (forwarderVersion === 4) {
this.forwarderVersion(4);
}
this.forwarderAddress(forwarderAddress);
this.tokenAddress(tokenAddress);
break;
case TransactionType.FlushCoins:
this.setContract(transactionJson.to);
break;
case TransactionType.Send:
case TransactionType.SendERC1155:
case TransactionType.SendERC721:
this.setContract(transactionJson.to);
this._transfer = this.transfer(transactionJson.data, isFirstSigner);
break;
case TransactionType.AddressInitialization:
this.setContract(transactionJson.to);
const { baseAddress, addressCreationSalt, feeAddress } = decodeForwarderCreationData(transactionJson.data);
if (baseAddress && addressCreationSalt) {
this.baseAddress(baseAddress);
this.salt(addressCreationSalt);
if (feeAddress) {
this.feeAddress(feeAddress);
this.forwarderVersion(4);
} else {
this.forwarderVersion(1);
}
const forwarderImplementationAddress = (this._coinConfig.network as EthereumNetwork)
.forwarderImplementationAddress as string;
if (forwarderImplementationAddress) {
this.initCode(forwarderImplementationAddress);
}
}
break;
case TransactionType.SingleSigSend:
this.setContract(transactionJson.to);
break;
case TransactionType.ContractCall:
this.setContract(transactionJson.to);
this.data(transactionJson.data);
break;
default:
throw new BuildTransactionError('Unsupported transaction type');
// TODO: Add other cases of deserialization
}
}
/** @inheritdoc */
protected signImplementation(key: BaseKey): BaseTransaction {
const signer = new KeyPair({ prv: key.key });
if (this._type === TransactionType.WalletInitialization && this._walletOwnerAddresses.length === 0) {
throw new SigningError('Cannot sign an wallet initialization transaction without owners');
}
if (this._sourceKeyPair) {
throw new SigningError('Cannot sign multiple times a non send-type transaction');
}
// Signing the transaction is an async operation, so save the source and leave the actual
// signing for the build step
this._sourceKeyPair = signer;
return this.transaction;
}
/** @inheritdoc */
validateAddress(address: BaseAddress): void {
if (!isValidEthAddress(address.address)) {
throw new BuildTransactionError('Invalid address ' + address.address);
}
}
/** @inheritdoc */
validateKey(key: BaseKey): void {
if (!(isValidXprv(key.key) || isValidPrv(key.key))) {
throw new BuildTransactionError('Invalid key');
}
}
/**
* Validate the raw transaction is either a JSON or
* a hex encoded transaction
*
* @param {any} rawTransaction The raw transaction to be validated
*/
validateRawTransaction(rawTransaction: any): void {
if (!rawTransaction) {
throw new InvalidTransactionError('Raw transaction is empty');
}
if (typeof rawTransaction === 'string') {
if (/^0x?[0-9a-f]{1,}$/.test(rawTransaction.toLowerCase())) {
const txBytes = ethUtil.toBuffer(ethUtil.addHexPrefix(rawTransaction.toLowerCase()));
if (!this.isEip1559Txn(txBytes) && !this.isRLPDecodable(txBytes)) {
throw new ParseTransactionError('There was error in decoding the hex string');
}
} else {
try {
JSON.parse(rawTransaction);
} catch (e) {
throw new ParseTransactionError('There was error in parsing the JSON string');
}
}
} else {
throw new InvalidTransactionError('Transaction is not a hex string or stringified json');
}
}
private isEip1559Txn(txn: Buffer): boolean {
try {
FeeMarketEIP1559Transaction.fromSerializedTx(txn);
return true;
} catch (_) {
return false;
}
}
private isRLPDecodable(bytes: Buffer): boolean {
try {
ethUtil.rlp.decode(bytes);
return true;
} catch (_) {
return false;
}
}
protected validateBaseTransactionFields(): void {
if (this._fee === undefined || (!this._fee.fee && !this._fee.gasPrice && !this._fee.eip1559)) {
throw new BuildTransactionError('Invalid transaction: missing fee');
}
if (this._common === undefined) {
throw new BuildTransactionError('Invalid transaction: network common');
}
if (this._counter === undefined) {
throw new BuildTransactionError('Invalid transaction: missing address counter');
}
}
/** @inheritdoc */
validateTransaction(transaction: BaseTransaction): void {
this.validateBaseTransactionFields();
switch (this._type) {
case TransactionType.WalletInitialization:
this.validateWalletInitializationFields();
break;
case TransactionType.RecoveryWalletDeployment:
this.validateDataField();
break;
case TransactionType.Send:
case TransactionType.SendERC721:
case TransactionType.SendERC1155:
this.validateContractAddress();
break;
case TransactionType.AddressInitialization:
this.validateContractAddress();
break;
case TransactionType.FlushCoins:
this.validateContractAddress();
break;
case TransactionType.FlushTokens:
this.validateContractAddress();
this.validateForwarderAddress();
this.validateTokenAddress();
break;
case TransactionType.SingleSigSend:
// for single sig sends, the contract address is actually the recipient
this.validateContractAddress();
break;
case TransactionType.StakingLock:
case TransactionType.StakingUnlock:
case TransactionType.StakingVote:
case TransactionType.StakingUnvote:
case TransactionType.StakingActivate:
case TransactionType.StakingWithdraw:
break;
case TransactionType.ContractCall:
this.validateContractAddress();
this.validateDataField();
break;
default:
throw new BuildTransactionError('Unsupported transaction type');
}
}
/**
* Check wallet owner addresses for wallet initialization transactions are valid or throw.
*/
private validateWalletInitializationFields(): void {
if (this._walletOwnerAddresses === undefined) {
throw new BuildTransactionError('Invalid transaction: missing wallet owners');
}
if (this._walletOwnerAddresses.length !== 3) {
throw new BuildTransactionError(
`Invalid transaction: wrong number of owners -- required: 3, found: ${this._walletOwnerAddresses.length}`
);
}
}
/**
* Check if a token address for the tx was defined or throw.
*/
private validateTokenAddress(): void {
if (this._tokenAddress === undefined) {
throw new BuildTransactionError('Invalid transaction: missing token address');
}
}
/**
* Check if a forwarder address for the tx was defined or throw.
*/
private validateForwarderAddress(): void {
if (this._forwarderAddress === undefined) {
throw new BuildTransactionError('Invalid transaction: missing forwarder address');
}
}
/**
* Check if a contract address for the wallet was defined or throw.
*/
private validateContractAddress(): void {
if (this._contractAddress === undefined) {
throw new BuildTransactionError('Invalid transaction: missing contract address');
}
}
/**
* Checks if a contract call data field was defined or throws otherwise
*/
private validateDataField(): void {
if (!this._data) {
throw new BuildTransactionError('Invalid transaction: missing contract call data field');
}
}
private setContract(address: string | undefined): void {
if (address === undefined) {
throw new BuildTransactionError('Undefined recipient address');
}
this.contract(address);
}
validateValue(value: BigNumber): void {
if (value.isLessThan(0)) {
throw new BuildTransactionError('Value cannot be below less than zero');
}
// TODO: validate the amount is not bigger than the max amount in each Eth family coin
}
// region Common builder methods
/**
* The type of transaction being built.
*
* @param {TransactionType} type
*/
type(type: TransactionType): void {
this._type = type;
}
/**
* Set the transaction fees. Low fees may get a transaction rejected or never picked up by bakers.
*
* @param {Fee} fee Baker fees. May also include the maximum gas to pay
*/
fee(fee: Fee): void {
this.validateValue(new BigNumber(fee.fee));
if (fee.gasLimit) {
this.validateValue(new BigNumber(fee.gasLimit));
}
if (fee.eip1559) {
this.validateValue(new BigNumber(fee.eip1559.maxFeePerGas));
this.validateValue(new BigNumber(fee.eip1559.maxPriorityFeePerGas));
}
if (fee.gasPrice) {
this.validateValue(new BigNumber(fee.gasPrice));
}
this._fee = fee;
}
/**
* Set the transaction counter to prevent submitting repeated transactions.
*
* @param {number} counter The counter to use
*/
counter(counter: number): void {
if (counter < 0) {
throw new BuildTransactionError(`Invalid counter: ${counter}`);
}
this._counter = counter;
}
/**
* The value to send along with this transaction. 0 by default
*
* @param {string} value The value to send along with this transaction
*/
value(value: string): void {
this._value = value;
}
// set args that are required for all types of eth transactions
protected buildBase(data: string): TxData {
const baseParams = {
gasLimit: this._fee.gasLimit,
nonce: this._counter,
data: data,
chainId: this._common.chainIdBN().toString(),
value: this._value,
to: this._contractAddress,
};
if (this._fee.eip1559) {
return {
...baseParams,
_type: ETHTransactionType.EIP1559,
maxFeePerGas: this._fee.eip1559.maxFeePerGas,
maxPriorityFeePerGas: this._fee.eip1559.maxPriorityFeePerGas,
};
} else {
return {
...baseParams,
_type: ETHTransactionType.LEGACY,
gasPrice: this._fee?.gasPrice ?? this._fee.fee,
v: this.getFinalV(),
};
}
}
// endregion
// region WalletInitialization builder methods
/**
* Set one of the owners of the multisig wallet.
*
* @param {string} address An Ethereum address
*/
owner(address: string): void {
if (this._type !== TransactionType.WalletInitialization) {
throw new BuildTransactionError('Multisig wallet owner can only be set for initialization transactions');
}
if (this._walletOwnerAddresses.length >= DEFAULT_M) {
throw new BuildTransactionError('A maximum of ' + DEFAULT_M + ' owners can be set for a multisig wallet');
}
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
if (this._walletOwnerAddresses.includes(address)) {
throw new BuildTransactionError('Repeated owner address: ' + address);
}
this._walletOwnerAddresses.push(address);
}
/**
* Build a transaction for a generic multisig contract.
*
* @returns {TxData} The Ethereum transaction data
*/
protected buildWalletInitializationTransaction(walletVersion?: number): TxData {
const walletInitData =
walletVersion === defaultWalletVersion
? this.getContractData(this._walletOwnerAddresses)
: getV1WalletInitializationData(this._walletOwnerAddresses, this._salt);
return this.buildBase(walletInitData);
}
/**
* Returns the smart contract encoded data
*
* @param {string[]} addresses - the contract signers
* @returns {string} - the smart contract encoded data
*/
protected getContractData(addresses: string[]): string {
const params = [addresses];
const resultEncodedParameters = EthereumAbi.rawEncode(walletSimpleConstructor, params)
.toString('hex')
.replace('0x', '');
return this._walletSimpleByteCode + resultEncodedParameters;
}
// endregion
// region Send builder methods
contract(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._contractAddress = address;
}
/**
* Gets the transfer funds builder if exist, or creates a new one for this transaction and returns it
*
* @param {string} data transfer data to initialize the transfer builder with, empty if none given
* @param {boolean} isFirstSigner whether the transaction is being signed by the first signer
* @returns {TransferBuilder | ERC721TransferBuilder | ERC1155TransferBuilder} the transfer builder
*/
abstract transfer(
data?: string,
isFirstSigner?: boolean
): TransferBuilder | ERC721TransferBuilder | ERC1155TransferBuilder;
/**
* Returns the serialized sendMultiSig contract method data
*
* @returns {string} serialized sendMultiSig data
*/
public getSendData(): string {
if (!this._transfer) {
throw new BuildTransactionError('Missing transfer information');
}
const chainId = this._common.chainIdBN().toString();
this._transfer.walletVersion(this._walletVersion);
// This change is made to support new contracts with different encoding type
return this._transfer.signAndBuild(chainId, this.coinUsesNonPackedEncodingForTxData());
}
/**
* Decide if the coin uses non-packed encoding for tx data
*
* @returns {boolean} true if the coin uses non-packed encoding for tx data
*/
public coinUsesNonPackedEncodingForTxData(): boolean {
return (
this._walletVersion === 4 || this._coinConfig.features.includes(CoinFeature.USES_NON_PACKED_ENCODING_FOR_TXDATA)
);
}
private buildSendTransaction(): TxData {
const sendData = this.getSendData();
const tx: TxData = this.buildBase(sendData);
tx.to = this._contractAddress;
return tx;
}
// endregion
// region AddressInitialization builder methods
/**
* Set the contract transaction nonce to calculate the forwarder address.
*
* @param {number} contractCounter The counter to use
*/
contractCounter(contractCounter: number): void {
if (contractCounter < 0) {
throw new BuildTransactionError(`Invalid contract counter: ${contractCounter}`);
}
this._contractCounter = contractCounter;
}
/**
* Build a transaction to create a forwarder.
*
* @returns {TxData} The Ethereum transaction data
*/
private buildAddressInitializationTransaction(): TxData {
const addressInitData = getAddressInitDataAllForwarderVersions(
this._forwarderVersion,
this._baseAddress,
this._salt,
this._feeAddress
);
const tx: TxData = this.buildBase(addressInitData);
tx.to = this._contractAddress;
if (this._contractCounter) {
tx.deployedAddress = calculateForwarderAddress(this._contractAddress, this._contractCounter);
}
if (this._salt && this._initCode) {
const saltBuffer = ethUtil.setLengthLeft(ethUtil.toBuffer(this._salt), 32);
const { createForwarderParams, createForwarderTypes } = getCreateForwarderParamsAndTypes(
this._baseAddress,
saltBuffer,
this._feeAddress
);
// Hash the wallet base address and fee address if present with the given salt, so the address directly relies on the base address and fee address
const calculationSalt = ethUtil.bufferToHex(
EthereumAbi.soliditySHA3(createForwarderTypes, createForwarderParams)
);
tx.deployedAddress = calculateForwarderV1Address(this._contractAddress, calculationSalt, this._initCode);
}
return tx;
}
// endregion
// region flush methods
/**
* Set the forwarder address to flush
*
* @param {string} address The address to flush
*/
forwarderAddress(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._forwarderAddress = address;
}
/**
* Set the address of the ERC20 token contract that we are flushing tokens for
*
* @param {string} address the contract address of the token to flush
*/
tokenAddress(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._tokenAddress = address;
}
/**
* Build a transaction to flush tokens from a forwarder.
*
* @returns {TxData} The Ethereum transaction data
*/
private buildFlushTokensTransaction(): TxData {
if (this._forwarderVersion >= 4 && this._contractAddress !== this._forwarderAddress) {
throw new BuildTransactionError('Invalid contract address: ' + this._contractAddress);
}
return this.buildBase(flushTokensData(this._forwarderAddress, this._tokenAddress, this._forwarderVersion));
}
/**
* Build a transaction to flush tokens from a forwarder.
*
* @returns {TxData} The Ethereum transaction data
*/
private buildFlushCoinsTransaction(): TxData {
return this.buildBase(flushCoinsData());
}
// endregion
// region generic contract call
data(encodedCall: string): void {
const supportedTransactionTypes = [TransactionType.ContractCall, TransactionType.RecoveryWalletDeployment];
if (!supportedTransactionTypes.includes(this._type)) {
throw new BuildTransactionError('data can only be set for contract call transaction types');
}
this._data = encodedCall;
}
private buildGenericContractCallTransaction(): TxData {
return this.buildBase(this._data);
}
// endregion
/** @inheritdoc */
protected get transaction(): Transaction {
return this._transaction;
}
/** @inheritdoc */
protected set transaction(transaction: Transaction) {
this._transaction = transaction;
}
/**
* Get the final v value. Final v is described in EIP-155.
*
* @protected for internal use when the enableFinalVField flag is true.
*/
protected getFinalV(): string {
return ethUtil.addHexPrefix(this._common.chainIdBN().muln(2).addn(35).toString(16));
}
/**
* Set the forwarder version for address to be initialized
*
* @param {number} version forwarder version
*/
forwarderVersion(version: number): void {
if (version < 0 || version > 4 || version === 3) {
throw new BuildTransactionError(`Invalid forwarder version: ${version}`);
}
this._forwarderVersion = version;
}
/**
* Set the salt to create the address using create2
*
* @param {string} salt The salt to create the address using create2, hex string
*/
salt(salt: string): void {
this._salt = salt;
}
/**
* Take the implementation address for the proxy contract, and get the binary initcode for the associated proxy
*
* @param {string} implementationAddress The address of the implementation contract
*/
initCode(implementationAddress: string): void {
if (!isValidEthAddress(implementationAddress)) {
throw new BuildTransactionError('Invalid address: ' + implementationAddress);
}
this._initCode = getProxyInitcode(implementationAddress);
}
/**
* Set the wallet version for wallet to be initialized
*
* @param {number} version wallet version
*/
walletVersion(version: number): void {
if (version < 0 || version > 4 || version === 3) {
throw new BuildTransactionError(`Invalid wallet version: ${version}`);
}
this._walletVersion = version;
}
/**
* Set the base address of the wallet
*
* @param {string} address The wallet contract address
*/
baseAddress(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._baseAddress = address;
}
/**
* Set the fee address of the wallet
*
* @param {string} address The fee address of the wallet
*/
feeAddress(address: string): void {
if (!isValidEthAddress(address)) {
throw new BuildTransactionError('Invalid address: ' + address);
}
this._feeAddress = address;
}
/**
* Get the wallet version for wallet
*/
public getWalletVersion(): number {
return this._walletVersion;
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!