PHP WebShell

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

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

import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core';
import { AvalancheNetwork, BaseCoin as CoinConfig } from '@bitgo/statics';
import { DeprecatedTransactionBuilder } from './deprecatedTransactionBuilder';
import {
  AddDelegatorTx,
  BaseTx as PVMBaseTx,
  ParseableOutput,
  PlatformVMConstants,
  SECPOwnerOutput,
  SECPTransferInput,
  SECPTransferOutput,
  SelectCredentialClass,
  TransferableInput,
  TransferableOutput,
  Tx as PVMTx,
  UnsignedTx,
} from 'avalanche/dist/apis/platformvm';
import { BinTools, BN } from 'avalanche';
import { SECP256K1_Transfer_Output, DeprecatedTx, DeprecatedBaseTx } from './iface';
import utils from './utils';
import { Credential } from 'avalanche/dist/common';
import { deprecatedRecoverUtxos } from './utxoEngine';

export class DelegatorTxBuilder extends DeprecatedTransactionBuilder {
  protected _nodeID: string;
  protected _startTime: BN;
  protected _endTime: BN;
  protected _stakeAmount: BN;

  /**
   *
   * @param coinConfig
   */
  constructor(coinConfig: Readonly<CoinConfig>) {
    super(coinConfig);
    const network = coinConfig.network as AvalancheNetwork;
    this._stakeAmount = new BN(network.minStake);
  }

  /**
   * get transaction type
   * @protected
   */
  protected get transactionType(): TransactionType {
    return TransactionType.AddDelegator;
  }

  /**
   * Addresses where reward should be deposit
   * @param {string | string[]} address - single address or array of addresses to receive rewards
   */
  rewardAddresses(address: string | string[]): this {
    const rewardAddresses = address instanceof Array ? address : [address];
    this.transaction._rewardAddresses = rewardAddresses.map(utils.parseAddress);
    return this;
  }

  /**
   *
   * @param nodeID
   */
  nodeID(value: string): this {
    this.validateNodeID(value);
    this._nodeID = value;
    return this;
  }

  /**
   * start time of staking period
   * @param value
   */
  startTime(value: string | number): this {
    this._startTime = new BN(value);
    return this;
  }

  /**
   * end time of staking period
   * @param value
   */
  endTime(value: string | number): this {
    this._endTime = new BN(value);
    return this;
  }

  /**
   *
   * @param value
   */
  stakeAmount(value: BN | string): this {
    const valueBN = BN.isBN(value) ? value : new BN(value);
    this.validateStakeAmount(valueBN);
    this._stakeAmount = valueBN;
    return this;
  }

  // region Validators
  /**
   * validates a correct NodeID is used
   * @param nodeID
   */
  validateNodeID(nodeID: string): void {
    if (!nodeID) {
      throw new BuildTransactionError('Invalid transaction: missing nodeID');
    }
    if (nodeID.slice(0, 6) !== 'NodeID') {
      throw new BuildTransactionError('Invalid transaction: invalid NodeID tag');
    }
    const bintools = BinTools.getInstance();
    if (!(bintools.b58ToBuffer(nodeID.slice(7)).length === 24)) {
      throw new BuildTransactionError('Invalid transaction: NodeID is not in cb58 format');
    }
  }
  /**
   *
   *   protected _startTime: Date;
   *   protected _endTime: Date;
   *   2 weeks = 1209600
   *   1 year = 31556926
   *   unix time stamp based off seconds
   */
  validateStakeDuration(startTime: BN, endTime: BN): void {
    const oneDayLater = new BN(Date.now()).add(new BN(86400));
    if (!startTime.gt(oneDayLater)) {
      throw new BuildTransactionError('Start time needs to be one day greater than current time');
    }
    if (endTime < startTime) {
      throw new BuildTransactionError('End date cannot be less than start date');
    }
    if (startTime.add(new BN(this.transaction._network.minStakeDuration)).gt(endTime)) {
      throw new BuildTransactionError('End date must be greater than or equal to two weeks');
    }
    if (endTime.gt(startTime.add(new BN(this.transaction._network.maxStakeDuration)))) {
      throw new BuildTransactionError('End date must be less than or equal to one year');
    }
  }

  /**
   *
   * @param amount
   */
  validateStakeAmount(amount: BN): void {
    const minStake = new BN(this.transaction._network.minStake);
    if (amount.lt(minStake)) {
      throw new BuildTransactionError('Minimum staking amount is ' + Number(minStake) / 1000000000 + ' AVAX.');
    }
  }

  // endregion

  /** @inheritdoc */
  initBuilder(tx: DeprecatedTx): this {
    super.initBuilder(tx);
    const baseTx: DeprecatedBaseTx = tx.getUnsignedTx().getTransaction();
    if (!this.verifyTxType(baseTx)) {
      throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
    }
    // The StakeOuts is a {@link stakeTransferOut} result.
    // It's expected to have only one outputs with the addresses of the sender.
    const outputs = baseTx.getStakeOuts();
    if (outputs.length != 1) {
      throw new BuildTransactionError('Transaction can have one external output');
    }
    const output = outputs[0];
    if (!output.getAssetID().equals(this.transaction._assetId)) {
      throw new Error('The Asset ID of the output does not match the transaction');
    }
    const secpOut = output.getOutput();
    this.transaction._locktime = secpOut.getLocktime();
    this.transaction._threshold = secpOut.getThreshold();
    // output addresses are the sender addresses
    this.transaction._fromAddresses = secpOut.getAddresses();
    this._nodeID = baseTx.getNodeIDString();
    this._startTime = baseTx.getStartTime();
    this._endTime = baseTx.getEndTime();
    this._stakeAmount = baseTx.getStakeAmount();
    this.transaction._utxos = deprecatedRecoverUtxos(baseTx.getIns());
    return this;
  }

  static verifyTxType(baseTx: DeprecatedBaseTx): baseTx is AddDelegatorTx {
    return baseTx.getTypeID() === PlatformVMConstants.ADDVALIDATORTX;
  }

  verifyTxType(baseTx: DeprecatedBaseTx): baseTx is AddDelegatorTx {
    return DelegatorTxBuilder.verifyTxType(baseTx);
  }

  /**
   *
   * @protected
   */
  protected buildAvaxTransaction(): void {
    this.validateStakeDuration(this._startTime, this._endTime);
    const { inputs, outputs, credentials } = this.createInputOutput();
    this.transaction.setTransaction(
      new PVMTx(
        new UnsignedTx(
          new AddDelegatorTx(
            this.transaction._networkID,
            this.transaction._blockchainID,
            outputs,
            inputs,
            undefined,
            utils.NodeIDStringToBuffer(this._nodeID),
            this._startTime,
            this._endTime,
            this._stakeAmount,
            [this.stakeTransferOut()],
            this.rewardOwnersOutput()
          )
        ),
        credentials
      )
    );
  }

  /**
   * Create the StakeOut where the recipient address are the sender.
   * @protected
   *
   */
  protected stakeTransferOut(): TransferableOutput {
    return new TransferableOutput(
      this.transaction._assetId,
      new SECPTransferOutput(
        this._stakeAmount,
        this.transaction._fromAddresses,
        this.transaction._locktime,
        this.transaction._threshold
      )
    );
  }

  protected rewardOwnersOutput(): ParseableOutput {
    // if there are no reward addresses, the sender gets the rewards
    if (!this.transaction._rewardAddresses || this.transaction._rewardAddresses.length === 0) {
      this.transaction._rewardAddresses = this.transaction._fromAddresses;
    }

    return new ParseableOutput(
      new SECPOwnerOutput(this.transaction._rewardAddresses, this.transaction._locktime, this.transaction._threshold)
    );
  }

  /**
   * Threshold must be 2 and since output always get reordered we want to make sure we can always add signatures in the correct location
   * To find the correct location for the signature, we use the ouput's addresses to create the signatureIdx in the order that we desire
   * 0: user key, 1: hsm key, 2: recovery key
   * @protected
   */
  protected createInputOutput(): {
    inputs: TransferableInput[];
    outputs: TransferableOutput[];
    credentials: Credential[];
  } {
    const inputs: TransferableInput[] = [];
    const outputs: TransferableOutput[] = [];

    // amount spent so far
    let currentTotal: BN = new BN(0);

    // delegating and validating have no fees
    const totalTarget = this._stakeAmount.clone();

    const credentials: Credential[] = [];

    // convert fromAddresses to string
    // fromAddresses = bitgo order if we are in WP
    // fromAddresses = onchain order if we are in from
    const bitgoAddresses = this.transaction._fromAddresses.map((b) =>
      utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, b)
    );

    /*
    A = user key
    B = hsm key
    C = backup key
    bitgoAddresses = bitgo addresses [ A, B, C ]
    utxo.addresses = IMS addresses [ B, C, A ]
    utxo.addressesIndex = [ 2, 0, 1 ]
    we pick 0, 1 for non-recovery
    we pick 1, 2 for recovery
    */
    this.transaction._utxos.forEach((utxo) => {
      // in WP, output.addressesIndex is empty, so fill it
      if (!utxo.addressesIndex || utxo.addressesIndex.length === 0) {
        utxo.addressesIndex = bitgoAddresses.map((a) => utxo.addresses.indexOf(a));
      }
      // in OVC, output.addressesIndex is defined correctly from the previous iteration
    });

    // validate the utxos
    this.transaction._utxos.forEach((utxo) => {
      if (!utxo) {
        throw new BuildTransactionError('Utxo is undefined');
      }
      // addressesIndex should neve have a mismatch
      if (utxo.addressesIndex?.includes(-1)) {
        throw new BuildTransactionError('Addresses are inconsistent');
      }
      if (utxo.threshold !== this.transaction._threshold) {
        throw new BuildTransactionError('Threshold is inconsistent');
      }
    });

    // if we are in OVC, none of the utxos will have addresses since they come from
    // deserialized inputs (which don't have addresses), not the IMS
    const buildOutputs = this.transaction._utxos[0].addresses.length !== 0;

    this.transaction._utxos.forEach((utxo, i) => {
      if (utxo.outputID === SECP256K1_Transfer_Output) {
        const txidBuf = utils.cb58Decode(utxo.txid);
        const amt: BN = new BN(utxo.amount);
        const outputidx = utils.outputidxNumberToBuffer(utxo.outputidx);
        const addressesIndex = utxo.addressesIndex ?? [];

        // either user (0) or recovery (2)
        const firstIndex = this.recoverSigner ? 2 : 0;
        const bitgoIndex = 1;
        currentTotal = currentTotal.add(amt);

        const secpTransferInput = new SECPTransferInput(amt);

        if (!buildOutputs) {
          addressesIndex.forEach((i) => secpTransferInput.addSignatureIdx(i, this.transaction._fromAddresses[i]));
        } else {
          // if user/backup > bitgo
          if (addressesIndex[bitgoIndex] < addressesIndex[firstIndex]) {
            secpTransferInput.addSignatureIdx(addressesIndex[bitgoIndex], this.transaction._fromAddresses[bitgoIndex]);
            secpTransferInput.addSignatureIdx(addressesIndex[firstIndex], this.transaction._fromAddresses[firstIndex]);
            credentials.push(
              SelectCredentialClass(
                secpTransferInput.getCredentialID(), // 9
                ['', this.transaction._fromAddresses[firstIndex].toString('hex')].map(utils.createSig)
              )
            );
          } else {
            secpTransferInput.addSignatureIdx(addressesIndex[firstIndex], this.transaction._fromAddresses[firstIndex]);
            secpTransferInput.addSignatureIdx(addressesIndex[bitgoIndex], this.transaction._fromAddresses[bitgoIndex]);
            credentials.push(
              SelectCredentialClass(
                secpTransferInput.getCredentialID(),
                [this.transaction._fromAddresses[firstIndex].toString('hex'), ''].map(utils.createSig)
              )
            );
          }
        }

        const input: TransferableInput = new TransferableInput(
          txidBuf,
          outputidx,
          this.transaction._assetId,
          secpTransferInput
        );
        inputs.push(input);
      }
    });

    if (buildOutputs) {
      if (currentTotal.lt(totalTarget)) {
        throw new BuildTransactionError(
          `Utxo outputs get ${currentTotal.toString()} and ${totalTarget.toString()} is required`
        );
      } else if (currentTotal.gt(totalTarget)) {
        outputs.push(
          new TransferableOutput(
            this.transaction._assetId,
            new SECPTransferOutput(
              currentTotal.sub(totalTarget),
              this.transaction._fromAddresses,
              this.transaction._locktime,
              this.transaction._threshold
            )
          )
        );
      }
    }
    // get outputs and credentials from the deserialized transaction if we are in OVC
    return {
      inputs,
      outputs: !buildOutputs ? (this.transaction.avaxPTransaction as PVMBaseTx).getOuts() : outputs,
      credentials: credentials.length === 0 ? this.transaction.credentials : credentials,
    };
  }
}

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


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