PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-coin-dot/src/lib

Просмотр файла: transaction.ts

import {
  BaseKey,
  BaseTransaction,
  DotAssetTypes,
  InvalidTransactionError,
  ParseTransactionError,
  SigningError,
  toUint8Array,
  TransactionRecipient,
  TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import Keyring, { decodeAddress, encodeAddress } from '@polkadot/keyring';
import { u8aToBuffer } from '@polkadot/util';
import { construct, decode } from '@substrate/txwrapper-polkadot';
import { UnsignedTransaction } from '@substrate/txwrapper-core';
import { TypeRegistry } from '@substrate/txwrapper-core/lib/types';
import { KeyPair } from './keyPair';
import {
  AddAnonymousProxyArgs,
  AddProxyArgs,
  AddProxyBatchCallArgs,
  BatchArgs,
  BatchCallObject,
  ClaimArgs,
  DecodedTx,
  HexString,
  MethodNames,
  SectionNames,
  StakeArgsPayeeRaw,
  StakeBatchCallArgs,
  StakeMoreArgs,
  StakeMoreCallArgs,
  TransactionExplanation,
  TxData,
  UnstakeArgs,
  WithdrawUnstakedArgs,
} from './iface';
import { getAddress, getDelegateAddress } from './iface_utils';
import utils from './utils';
import BigNumber from 'bignumber.js';
import { Vec } from '@polkadot/types';
import { PalletConstantMetadataV14 } from '@polkadot/types/interfaces';
import { EXTRINSIC_VERSION } from '@polkadot/types/extrinsic/v4/Extrinsic';

/**
 * Use a dummy address as the destination of a bond or bondExtra because our inputs and outputs model
 * doesn't seem to handle the concept of locking funds within a wallet as a method of transferring coins.
 */
export const STAKING_DESTINATION = encodeAddress('0x0000000000000000000000000000000000000000000000000000000000000000');

export class Transaction extends BaseTransaction {
  protected _dotTransaction: UnsignedTransaction;
  private _signedTransaction?: string;
  private _registry: TypeRegistry;
  private _chainName: string;
  private _sender: string;

  private static FAKE_SIGNATURE = `0x${Buffer.from(new Uint8Array(256).fill(1)).toString('hex')}`;

  constructor(coinConfig: Readonly<CoinConfig>) {
    super(coinConfig);
  }

  /** @inheritdoc */
  canSign({ key }: BaseKey): boolean {
    const kp = new KeyPair({ prv: key });
    const addr = kp.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
    return addr === this._sender;
  }

  /**
   * Sign a polkadot transaction and update the transaction hex
   *
   * @param {KeyPair} keyPair - ed signature
   */
  sign(keyPair: KeyPair): void {
    if (!this._dotTransaction) {
      throw new InvalidTransactionError('No transaction data to sign');
    }
    const { prv, pub } = keyPair.getKeys();
    if (!prv) {
      throw new SigningError('Missing private key');
    }
    const signingPayload = construct.signingPayload(this._dotTransaction, {
      registry: this._registry,
    });
    // Sign a payload. This operation should be performed on an offline device.
    const keyring = new Keyring({ type: 'ed25519' });
    const secretKey = new Uint8Array(Buffer.from(prv, 'hex'));
    const publicKey = new Uint8Array(Buffer.from(pub, 'hex'));
    const signingKeyPair = keyring.addFromPair({ secretKey, publicKey });
    const txHex = utils.createSignedTx(signingKeyPair, signingPayload, this._dotTransaction, {
      metadataRpc: this._dotTransaction.metadataRpc,
      registry: this._registry,
    });

    // get signature from signed txHex generated above
    this._signatures = [utils.recoverSignatureFromRawTx(txHex, { registry: this._registry })];
    this._signedTransaction = txHex;
  }

  /**
   * Adds the signature to the DOT Transaction
   * @param {string} signature
   */
  addSignature(signature: string): void {
    this._signedTransaction = utils.serializeSignedTransaction(
      this._dotTransaction,
      signature,
      this._dotTransaction.metadataRpc,
      this._registry
    );
  }

  /**
   * Returns a serialized representation of this transaction with a fake signature attached which
   * can be used to estimate transaction fees.
   */
  fakeSign(): string {
    return utils.serializeSignedTransaction(
      this._dotTransaction,
      Transaction.FAKE_SIGNATURE,
      this._dotTransaction.metadataRpc,
      this._registry
    );
  }

  registry(registry: TypeRegistry): void {
    this._registry = registry;
  }

  chainName(chainName: string): void {
    this._chainName = chainName;
  }

  sender(sender: string): void {
    this._sender = sender;
  }

  /** @inheritdoc */
  toBroadcastFormat(): string {
    if (!this._dotTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }
    if (this._signedTransaction && this._signedTransaction.length > 0) {
      return this._signedTransaction;
    } else {
      return construct.signingPayload(this._dotTransaction, {
        registry: this._registry,
      });
    }
  }

  transactionSize(): number {
    return this.toBroadcastFormat().length / 2;
  }

  /** @inheritdoc */
  toJson(): TxData {
    if (!this._dotTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }
    const decodedTx = decode(this._dotTransaction, {
      metadataRpc: this._dotTransaction.metadataRpc,
      registry: this._registry,
      isImmortalEra: utils.isZeroHex(this._dotTransaction.era),
    }) as unknown as DecodedTx;

    const result: TxData = {
      id: construct.txHash(this.toBroadcastFormat()),
      sender: decodedTx.address,
      referenceBlock: decodedTx.blockHash,
      blockNumber: decodedTx.blockNumber,
      genesisHash: decodedTx.genesisHash,
      nonce: decodedTx.nonce,
      specVersion: decodedTx.specVersion,
      transactionVersion: decodedTx.transactionVersion,
      eraPeriod: decodedTx.eraPeriod,
      chainName: this._chainName,
      tip: decodedTx.tip ? Number(decodedTx.tip) : 0,
    };

    if (this.type === TransactionType.Send) {
      const txMethod = decodedTx.method.args;
      if (utils.isProxyTransfer(txMethod)) {
        const keypairReal = new KeyPair({
          pub: Buffer.from(decodeAddress(getAddress(txMethod))).toString('hex'),
        });
        result.owner = keypairReal.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
        result.forceProxyType = txMethod.forceProxyType;
        const decodedCall = utils.decodeCallMethod(this._dotTransaction, {
          metadataRpc: this._dotTransaction.metadataRpc,
          registry: this._registry,
        });
        const keypairDest = new KeyPair({
          pub: Buffer.from(decodeAddress(decodedCall.dest.id)).toString('hex'),
        });
        result.to = keypairDest.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
        result.amount = decodedCall.value;
      } else if (utils.isTransfer(txMethod)) {
        const keypairDest = new KeyPair({
          pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
        });
        result.to = keypairDest.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
        result.amount = txMethod.value;
      } else if (utils.isTransferAll(txMethod)) {
        const keypairDest = new KeyPair({
          pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
        });
        result.to = keypairDest.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
        result.keepAlive = txMethod.keepAlive;
      } else {
        throw new ParseTransactionError(`Serializing unknown Transfer type parameters`);
      }
    }

    if (this.type === TransactionType.StakingActivate) {
      const txMethod = decodedTx.method.args;
      if (utils.isBond(txMethod)) {
        const keypair = new KeyPair({
          pub: Buffer.from(decodeAddress(this._sender, false, this._registry.chainSS58)).toString('hex'),
        });

        result.controller = keypair.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
        result.amount = txMethod.value;

        const payee = txMethod.payee as StakeArgsPayeeRaw;
        if (payee.account) {
          const keypair = new KeyPair({
            pub: Buffer.from(decodeAddress(payee.account, false, this._registry.chainSS58)).toString('hex'),
          });
          result.payee = keypair.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
        } else {
          const payeeType = utils.capitalizeFirstLetter(Object.keys(payee)[0]) as string;
          result.payee = payeeType;
        }
      } else if (utils.isBondExtra(decodedTx.method.args)) {
        result.amount = decodedTx.method.args.maxAdditional;
      }
    }

    if (this.type === TransactionType.AddressInitialization) {
      let txMethod: AddAnonymousProxyArgs | AddProxyArgs;
      if ((decodedTx.method?.args as AddProxyArgs).delegate) {
        txMethod = decodedTx.method.args as AddProxyArgs;
        const delegateAddress = getDelegateAddress(txMethod);
        const decodedAddress = decodeAddress(delegateAddress, false, this._registry.chainSS58);
        const keypair = new KeyPair({ pub: Buffer.from(decodedAddress).toString('hex') });
        result.owner = keypair.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
      } else {
        txMethod = decodedTx.method.args as AddAnonymousProxyArgs;
        result.index = txMethod.index;
      }
      result.method = this._dotTransaction.method;
      result.proxyType = txMethod.proxyType;
      result.delay = txMethod.delay;
    }

    if (this.type === TransactionType.StakingUnlock) {
      const txMethod = decodedTx.method.args as UnstakeArgs;
      result.amount = txMethod.value;
    }

    if (this.type === TransactionType.StakingWithdraw) {
      const txMethod = decodedTx.method.args as WithdrawUnstakedArgs;
      result.numSlashingSpans = txMethod.numSlashingSpans;
    }

    if (this.type === TransactionType.StakingClaim) {
      const txMethod = decodedTx.method.args as ClaimArgs;
      result.validatorStash = txMethod.validatorStash;
      result.claimEra = txMethod.era;
    }

    if (this.type === TransactionType.Batch) {
      const txMethod = decodedTx.method.args as BatchArgs;
      result.batchCalls = txMethod.calls;
    }

    return result;
  }

  explainTransferTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
    explanationResult.displayOrder?.push('owner', 'forceProxyType');
    return {
      ...explanationResult,
      outputs: [
        {
          address: json.to?.toString() || '',
          amount: json.amount?.toString() || '',
        },
      ],
      owner: json.owner,
      forceProxyType: json.forceProxyType,
    };
  }

  explainStakingActivateTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
    explanationResult.displayOrder?.push('payee', 'forceProxyType');
    return {
      ...explanationResult,
      outputs: [
        {
          address: json.controller?.toString() || '',
          amount: json.amount || '',
        },
      ],
      payee: json.payee,
      forceProxyType: json.forceProxyType,
    };
  }

  explainAddressInitializationTransaction(
    json: TxData,
    explanationResult: TransactionExplanation
  ): TransactionExplanation {
    explanationResult.displayOrder?.push('owner', 'proxyType', 'delay');
    return {
      ...explanationResult,
      owner: json.owner,
      proxyType: json.proxyType,
      delay: json.delay,
    };
  }

  explainStakingUnlockTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
    return {
      ...explanationResult,
      outputs: [
        {
          address: json.sender.toString(),
          amount: json.amount || '',
        },
      ],
    };
  }

  /** @inheritdoc */
  explainTransaction(): TransactionExplanation {
    const result = this.toJson();
    const displayOrder = ['outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'type'];
    const outputs: TransactionRecipient[] = [];
    const explanationResult: TransactionExplanation = {
      // txhash used to identify the transactions
      id: result.id,
      displayOrder,
      outputAmount: result.amount?.toString() || '0',
      changeAmount: '0',
      changeOutputs: [],
      outputs,
      fee: {
        fee: result.tip?.toString() || '',
        type: 'tip',
      },
      type: this.type,
    };
    switch (this.type) {
      case TransactionType.Send:
        return this.explainTransferTransaction(result, explanationResult);
      case TransactionType.StakingActivate:
        return this.explainStakingActivateTransaction(result, explanationResult);
      case TransactionType.AddressInitialization:
        return this.explainAddressInitializationTransaction(result, explanationResult);
      case TransactionType.StakingUnlock:
        return this.explainStakingUnlockTransaction(result, explanationResult);
      default:
        throw new InvalidTransactionError('Transaction type not supported');
    }
  }

  /**
   * Load the input and output data on this transaction.
   */
  loadInputsAndOutputs(): void {
    if (!this._dotTransaction) {
      return;
    }
    const decodedTx = decode(this._dotTransaction, {
      metadataRpc: this._dotTransaction.metadataRpc,
      registry: this._registry,
      isImmortalEra: utils.isZeroHex(this._dotTransaction.era),
    }) as unknown as DecodedTx;

    if (this.type === TransactionType.Send) {
      this.decodeInputsAndOutputsForSend(decodedTx);
    } else if (this.type === TransactionType.Batch) {
      this.decodeInputsAndOutputsForBatch(decodedTx);
    } else if (this.type === TransactionType.StakingActivate) {
      this.decodeInputsAndOutputsForBond(decodedTx);
    } else if (this.type === TransactionType.StakingUnlock) {
      this.decodeInputsAndOutputsForUnbond(decodedTx);
    } else if (this.type === TransactionType.StakingWithdraw) {
      this.decodeInputsAndOutputsForWithdrawUnbond(decodedTx);
    }
  }

  private decodeInputsAndOutputsForSend(decodedTx: DecodedTx) {
    const txMethod = decodedTx.method.args;
    let to: string;
    let value: string;
    let from: string;
    if (utils.isProxyTransfer(txMethod)) {
      const decodedCall = utils.decodeCallMethod(this._dotTransaction, {
        metadataRpc: this._dotTransaction.metadataRpc,
        registry: this._registry,
      });
      const keypairDest = new KeyPair({
        pub: Buffer.from(decodeAddress(decodedCall.dest.id)).toString('hex'),
      });
      const keypairFrom = new KeyPair({
        pub: Buffer.from(decodeAddress(getAddress(txMethod))).toString('hex'),
      });
      to = keypairDest.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
      value = `${decodedCall.value}`;
      from = keypairFrom.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
    } else if (utils.isTransferAll(txMethod)) {
      const keypairDest = new KeyPair({
        pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
      });
      to = keypairDest.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
      value = '0'; // DOT transferAll's do not deserialize amounts
      from = decodedTx.address;
    } else if (utils.isTransfer(txMethod)) {
      const keypairDest = new KeyPair({
        pub: Buffer.from(decodeAddress(txMethod.dest.id)).toString('hex'),
      });
      to = keypairDest.getAddress(utils.getAddressFormat(this._coinConfig.name as DotAssetTypes));
      value = txMethod.value;
      from = decodedTx.address;
    } else {
      throw new ParseTransactionError(`Loading inputs of unknown Transfer type parameters`);
    }
    this._outputs = [
      {
        address: to,
        value,
        coin: this._coinConfig.name,
      },
    ];

    this._inputs = [
      {
        address: from,
        value,
        coin: this._coinConfig.name,
      },
    ];
  }

  private decodeInputsAndOutputsForBatch(decodedTx: DecodedTx) {
    const sender = decodedTx.address;
    this._inputs = [];
    this._outputs = [];

    const txMethod = decodedTx.method.args;
    if (utils.isStakingBatch(txMethod)) {
      if (!txMethod.calls) {
        throw new InvalidTransactionError('failed to decode calls from batch transaction');
      }

      const bondMethod = (txMethod.calls[0] as BatchCallObject).callIndex;
      const decodedBondCall = this._registry.findMetaCall(toUint8Array(utils.stripHexPrefix(bondMethod)));
      if (
        decodedBondCall.section !== SectionNames.Staking ||
        (decodedBondCall.method !== MethodNames.Bond && decodedBondCall.method !== MethodNames.BondExtra)
      ) {
        throw new InvalidTransactionError(
          'Invalid batch transaction, only staking batch calls are supported, expected first call to be bond or bond exta.'
        );
      }
      const addProxyMethod = (txMethod.calls[1] as BatchCallObject).callIndex;
      const decodedAddProxyCall = this._registry.findMetaCall(toUint8Array(utils.stripHexPrefix(addProxyMethod)));
      if (decodedAddProxyCall.section !== SectionNames.Proxy || decodedAddProxyCall.method !== MethodNames.AddProxy) {
        throw new InvalidTransactionError(
          'Invalid batch transaction, only staking batch calls are supported, expected second call to be addProxy.'
        );
      }

      let bondValue;
      if (decodedBondCall.method === MethodNames.BondExtra && utils.isBondBatchExtra(txMethod.calls[0].args)) {
        bondValue = `${(txMethod.calls[0].args as StakeMoreCallArgs).max_additional}`;
      } else if (decodedBondCall.method === MethodNames.BondExtra && utils.isBondExtra(txMethod.calls[0].args)) {
        bondValue = `${(txMethod.calls[0].args as StakeMoreArgs).maxAdditional}`;
      } else {
        bondValue = `${(txMethod.calls[0].args as StakeBatchCallArgs).value}`;
      }
      const addProxyArgs = txMethod.calls[1].args as AddProxyBatchCallArgs;
      const proxyAddress = getDelegateAddress(addProxyArgs);

      this._inputs.push({
        address: sender,
        value: bondValue,
        coin: this._coinConfig.name,
      });
      this._outputs.push({
        address: STAKING_DESTINATION,
        value: bondValue,
        coin: this._coinConfig.name,
      });

      const addProxyCost = this.getAddProxyCost().toString(10);
      this._inputs.push({
        address: sender,
        value: addProxyCost,
        coin: this._coinConfig.name,
      });
      this._outputs.push({
        address: proxyAddress,
        value: addProxyCost,
        coin: this._coinConfig.name,
      });
    } else if (utils.isUnstakingBatch(txMethod)) {
      if (!txMethod.calls) {
        throw new InvalidTransactionError('failed to decode calls from batch transaction');
      }

      const removeProxyMethod = (txMethod.calls[0] as BatchCallObject).callIndex;
      const decodedRemoveProxyCall = this._registry.findMetaCall(toUint8Array(utils.stripHexPrefix(removeProxyMethod)));
      if (
        decodedRemoveProxyCall.section !== SectionNames.Proxy ||
        decodedRemoveProxyCall.method !== MethodNames.RemoveProxy
      ) {
        throw new InvalidTransactionError(
          'Invalid batch transaction, only staking batch calls are supported, expected first call to be removeProxy.'
        );
      }
      const chillMethod = (txMethod.calls[1] as BatchCallObject).callIndex;
      const decodedChillCall = this._registry.findMetaCall(toUint8Array(utils.stripHexPrefix(chillMethod)));
      if (decodedChillCall.section !== SectionNames.Staking || decodedChillCall.method !== MethodNames.Chill) {
        throw new InvalidTransactionError(
          'Invalid batch transaction, only staking batch calls are supported, expected second call to be chill.'
        );
      }
      const unstakeMethod = (txMethod.calls[2] as BatchCallObject).callIndex;
      const decodedUnstakeCall = this._registry.findMetaCall(toUint8Array(utils.stripHexPrefix(unstakeMethod)));
      if (decodedUnstakeCall.section !== SectionNames.Staking || decodedUnstakeCall.method !== MethodNames.Unbond) {
        throw new InvalidTransactionError(
          'Invalid batch transaction, only staking batch calls are supported, expected third call to be unbond.'
        );
      }

      const removeProxyArgs = txMethod.calls[0].args as AddProxyBatchCallArgs;
      const proxyAddress = getDelegateAddress(removeProxyArgs);

      const removeProxyCost = this.getRemoveProxyCost().toString(10);
      this._inputs.push({
        address: proxyAddress,
        value: removeProxyCost,
        coin: this._coinConfig.name,
      });
      this._outputs.push({
        address: sender,
        value: removeProxyCost,
        coin: this._coinConfig.name,
      });
    }
  }

  private getRemoveProxyCost(): BigNumber {
    return this.getAddProxyCost();
  }

  private getAddProxyCost(): BigNumber {
    const proxyPallet = this._registry.metadata.pallets.find(
      (p) => p.name.toString().toLowerCase() === SectionNames.Proxy
    );
    if (proxyPallet) {
      const proxyDepositBase = this.getConstant('ProxyDepositBase', proxyPallet.constants);
      const proxyDepositFactor = this.getConstant('ProxyDepositFactor', proxyPallet.constants);
      return proxyDepositBase.plus(proxyDepositFactor);
    } else {
      const palletNames = this._registry.metadata.pallets.map((p) => p.name.toString().toLowerCase());
      throw new Error(`Could not find ${SectionNames.Proxy} pallet in [${palletNames}]`);
    }
  }

  private getConstant(name: string, constants: Vec<PalletConstantMetadataV14>): BigNumber {
    const constant = constants.find((c) => c.name.toString() === name);
    if (constant === undefined) {
      const constantNames = constants.map((p) => p.name.toString());
      throw new Error(`Could not find constant ${name} in [${constantNames}]`);
    } else {
      // Convert from Little-Endian to Big-Endian
      const valueBe = Buffer.from(constant.value.toU8a(true).reverse()).toString('hex');
      return BigNumber(valueBe, 16);
    }
  }

  private decodeInputsAndOutputsForBond(decodedTx: DecodedTx) {
    const sender = decodedTx.address;
    this._inputs = [];
    this._outputs = [];

    const txMethod = decodedTx.method.args;
    if (decodedTx.method.pallet === SectionNames.Staking) {
      let bondValue = '0';
      if (decodedTx.method.name === MethodNames.Bond && utils.isBond(txMethod)) {
        bondValue = txMethod.value;
      } else if (decodedTx.method.name === MethodNames.BondExtra && utils.isBondExtra(txMethod)) {
        bondValue = txMethod.maxAdditional;
      } else {
        throw new ParseTransactionError(`Loading inputs of unknown StakingActivate type parameters`);
      }
      this._inputs.push({
        address: sender,
        value: bondValue,
        coin: this._coinConfig.name,
      });
      this._outputs.push({
        address: STAKING_DESTINATION,
        value: bondValue,
        coin: this._coinConfig.name,
      });
    }
  }

  private decodeInputsAndOutputsForUnbond(decodedTx: DecodedTx) {
    this._inputs = [];
    this._outputs = [];
  }

  private decodeInputsAndOutputsForWithdrawUnbond(decodedTx: DecodedTx) {
    this._inputs = [];
    this._outputs = [];
  }

  /**
   * Constructs a signed payload using construct.signTx
   * This method will be called during the build step if a TSS signature
   * is added and will set the signTransaction which is the txHex that will be broadcasted
   * As well as add the signature used to sign to the signature array in hex format
   *
   * @param {Buffer} signature The signature to be added to a dot transaction
   */
  constructSignedPayload(signature: Buffer): void {
    // 0x00 means its an ED25519 signature
    const edSignature = `0x00${signature.toString('hex')}` as HexString;

    try {
      this._signedTransaction = construct.signedTx(this._dotTransaction, edSignature, {
        registry: this._registry,
        metadataRpc: this._dotTransaction.metadataRpc,
      });
    } catch (e) {
      throw new SigningError(`Unable to sign dot transaction with signature ${edSignature} ` + e);
    }

    this._signatures = [signature.toString('hex')];
  }

  setTransaction(tx: UnsignedTransaction): void {
    this._dotTransaction = tx;
  }

  /** @inheritdoc **/
  get signablePayload(): Buffer {
    const extrinsicPayload = this._registry.createType('ExtrinsicPayload', this._dotTransaction, {
      version: EXTRINSIC_VERSION,
    });
    return u8aToBuffer(extrinsicPayload.toU8a({ method: true }));
  }

  /**
   * Set the transaction type.
   *
   * @param {TransactionType} transactionType The transaction type to be set.
   */
  transactionType(transactionType: TransactionType): void {
    this._type = transactionType;
  }
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!