PHP WebShell

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

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

import {
  Address,
  avaxSerial,
  utils as AvaxUtils,
  BigIntPr,
  Credential,
  Id,
  Input,
  Int,
  networkIDs,
  OutputOwners,
  pvmSerial,
  TransferInput,
  TransferOutput,
  TypeSymbols,
  UnsignedTx,
  Utxo,
  utils as avaxUtils,
} from '@bitgo-forks/avalanchejs';
import {
  BaseAddress,
  BaseKey,
  BuildTransactionError,
  isValidBLSPublicKey,
  isValidBLSSignature,
  NotSupported,
  TransactionType,
} from '@bitgo/sdk-core';

import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Buffer as BufferAvax } from 'avalanche';
import BigNumber from 'bignumber.js';
import { DecodedUtxoObj, SECP256K1_Transfer_Output, Tx } from './iface';
import { KeyPair } from './keyPair';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import utils from './utils';
import { recoverUtxos } from './utxoEngine';

export class PermissionlessValidatorTxBuilder extends TransactionBuilder {
  public _signer: KeyPair[] = [];
  protected _nodeID: string;
  protected _blsPublicKey: string;
  protected _blsSignature: string;
  protected _startTime: bigint;
  protected _endTime: bigint;
  protected _stakeAmount: bigint;
  protected recoverSigner = false;
  protected _delegationFeeRate: number;

  /**
   *
   * @param coinConfig
   */
  constructor(coinConfig: Readonly<CoinConfig>) {
    super(coinConfig);
    this.transaction._fee.fee = this.transaction._network.txFee;
  }

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

  // region Validators
  /**
   * Validates locktime
   * @param locktime
   */
  validateLocktime(locktime: bigint): void {
    if (locktime < BigInt(0)) {
      throw new BuildTransactionError('Invalid transaction: locktime must be 0 or higher');
    }
  }

  /**
   * Validate that the delegation fee is at least the minDelegationFee
   * @param delegationFeeRate number
   */
  validateDelegationFeeRate(delegationFeeRate: number): void {
    if (delegationFeeRate < Number(this.transaction._network.minDelegationFee)) {
      throw new BuildTransactionError(
        `Delegation fee cannot be less than ${this.transaction._network.minDelegationFee}`
      );
    }
  }

  /**
   * Check the UTXO has expected fields.
   * @param UTXO
   */
  validateUtxo(value: DecodedUtxoObj): void {
    ['outputID', 'amount', 'txid', 'outputidx'].forEach((field) => {
      if (!value.hasOwnProperty(field)) throw new BuildTransactionError(`Utxos required ${field}`);
    });
  }
  // endregion

  /**
   * 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;
  }

  /** @inheritdoc */
  protected fromImplementation(rawTransaction: string): Transaction {
    const manager = AvaxUtils.getManagerForVM('PVM');
    const [codec, rest] = manager.getCodecFromBuffer(AvaxUtils.hexToBuffer(rawTransaction));
    const tx = codec.UnpackPrefix<pvmSerial.AddPermissionlessValidatorTx>(rest)[0];
    this.initBuilder(tx);
    return this.transaction;
  }

  /** @inheritdoc */
  protected async buildImplementation(): Promise<Transaction> {
    this.buildAvaxTransaction();
    this.transaction.setTransactionType(this.transactionType);
    if (this.hasSigner()) {
      for (const keyPair of this._signer) {
        await this.transaction.sign(keyPair);
      }
    }
    return this.transaction;
  }

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

  /**
   *
   * @param blsPublicKey
   */
  blsPublicKey(blsPublicKey: string): this {
    isValidBLSPublicKey(blsPublicKey);
    this._blsPublicKey = blsPublicKey;
    return this;
  }

  /**
   *
   * @param blsSignature
   */
  blsSignature(blsSignature: string): this {
    isValidBLSSignature(blsSignature);
    this._blsSignature = blsSignature;
    return this;
  }

  /**
   * Locktime is a long that contains the unix timestamp that this output can be spent after.
   * The unix timestamp is specific to the second.
   * @param value
   */
  locktime(value: string | number): this {
    this.validateLocktime(BigInt(value));
    this._transaction._locktime = BigInt(value);
    return this;
  }

  /**
   * set the delegationFeeRate
   * @param value number
   */
  delegationFeeRate(value: number): this {
    this.validateDelegationFeeRate(value);
    this._delegationFeeRate = value;
    return this;
  }

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

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

  /**
   *
   * @param value
   */
  stakeAmount(value: bigint | string): this {
    const valueBigInt = typeof value === 'bigint' ? value : BigInt(value);
    this.validateStakeAmount(valueBigInt);
    this._stakeAmount = valueBigInt;
    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');
    }
    if (!(AvaxUtils.base58.decode(nodeID.slice(7)).length === 24)) {
      throw new BuildTransactionError('Invalid transaction: NodeID is not in cb58 format');
    }
  }

  /**
   * Validate stake duration
   * @param startTime
   * @param endTime
   */
  validateStakeDuration(startTime: bigint, endTime: bigint): void {
    if (endTime < startTime) {
      throw new BuildTransactionError('End date cannot be less than start date');
    }
  }

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

  // endregion

  /** @inheritdoc */
  initBuilder(tx: Tx): this {
    super.initBuilder(tx);
    const permissionlessValidatorTx = (tx as UnsignedTx).tx as pvmSerial.AddPermissionlessValidatorTx;
    if (!this.verifyTxType(permissionlessValidatorTx)) {
      throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
    }

    const outputs = permissionlessValidatorTx.baseTx.outputs;
    if (outputs.length !== 1) {
      throw new BuildTransactionError('Transaction can have one external output');
    }

    const output = outputs[0].output as TransferOutput;
    if (outputs[0].getAssetId() !== this.transaction._assetId) {
      throw new Error('The Asset ID of the output does not match the transaction');
    }

    this.transaction._blsPublicKey = AvaxUtils.bufferToHex(
      (permissionlessValidatorTx.signer as pvmSerial.Signer).proof.publicKey
    );
    this._blsPublicKey = this.transaction._blsPublicKey;
    this.transaction._blsSignature = AvaxUtils.bufferToHex(
      (permissionlessValidatorTx.signer as pvmSerial.Signer).proof.signature
    );
    this._blsSignature = this.transaction._blsSignature;

    this.transaction._locktime = output.outputOwners.locktime.value();
    this.transaction._threshold = output.outputOwners.threshold.value();
    this.transaction._nodeID = permissionlessValidatorTx.subnetValidator.validator.nodeId.toString();
    this._nodeID = this.transaction._nodeID;
    this.transaction._startTime = permissionlessValidatorTx.subnetValidator.validator.startTime.value();
    this._startTime = this.transaction._startTime;
    this.transaction._endTime = permissionlessValidatorTx.subnetValidator.validator.endTime.value();
    this._endTime = this.transaction._endTime;
    this.transaction._fromAddresses = output.outputOwners.addrs.map((a) => a.toBytes());
    this.transaction._stakeAmount = permissionlessValidatorTx.stake[0].output.amount();
    this.stakeAmount(this.transaction._stakeAmount);
    this.transaction._utxos = recoverUtxos(permissionlessValidatorTx.getInputs());
    // TODO(CR-1073): remove log
    console.log('utxos: ', this.transaction._utxos);
    console.log('fromAddresses: ', this.transaction.fromAddresses);
    return this;
  }

  static verifyTxType(type: TypeSymbols): boolean {
    return type === TypeSymbols.AddPermissionlessValidatorTx;
  }

  verifyTxType(tx: Tx): tx is pvmSerial.AddPermissionlessValidatorTx {
    return PermissionlessValidatorTxBuilder.verifyTxType((tx as pvmSerial.AddPermissionlessValidatorTx)._type);
  }

  /**
   * Since addresses in outputs get reordered, we need to make sure signatures
   * are added in the correct position
   * To find the position, we use the output's addresses to create the
   * signatureIdx in the order needed (i.e. [user, bitgo, backup])
   * @protected
   */
  protected calculateUtxos(): {
    inputs: avaxSerial.TransferableInput[];
    stakeOutputs: avaxSerial.TransferableOutput[];
    changeOutputs: avaxSerial.TransferableOutput[];
    utxos: Utxo[];
    credentials: Credential[];
  } {
    const inputs: avaxSerial.TransferableInput[] = [];
    const stakeOutputs: avaxSerial.TransferableOutput[] = [];
    const changeOutputs: avaxSerial.TransferableOutput[] = [];
    const utxos: Utxo[] = [];

    let currentTotal = BigInt(0);

    // staking tx requires fees after ACP-103
    // https://github.com/avalanche-foundation/ACPs/blob/main/ACPs/103-dynamic-fees/README.md
    const totalTarget = this._stakeAmount + BigInt(this.transaction._network.txFee);

    const credentials: Credential[] = this.transaction.credentials ?? [];
    // Convert fromAddresses to string
    // The order of fromAddresses is determined by the source of the data
    // When building from params, the order is [user, bitgo, backup]
    // The order from tx hex is [bitgo, backup, user]
    const bitgoAddresses = this.transaction._fromAddresses.map((b) =>
      avaxUtils.format(this.transaction._network.alias, this.transaction._network.hrp, b)
    );
    // TODO(CR-1073): remove log
    console.log(`bitgoAddress: ${bitgoAddresses}`);

    // 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[0].addressesIndex?.length !== 0;

    const assetId = Id.fromString(this.transaction._assetId);
    this.transaction._utxos.forEach((utxo, index) => {
      // validate the utxos
      if (!utxo) {
        throw new BuildTransactionError('Utxo is undefined');
      }
      // addressesIndex should never 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');
      }

      const bitgoIndexToOnChainIndex = new Map();
      // 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));
      }
      // utxo.addresses is null when build from raw
      // but utxo.addressesIndex has only 2 elements when build from raw
      // so the bitgoIndexToOnChainIndex map will be empty
      utxo.addresses.forEach((a) => {
        bitgoIndexToOnChainIndex.set(bitgoAddresses.indexOf(a), utxo.addresses.indexOf(a));
      });
      // TODO(CR-1073): remove log
      console.log(`utxo.addresses: ${utxo.addresses}`);
      console.log(`bitgoIndexToOnChainIndex: ${Array.from(bitgoIndexToOnChainIndex)}`);
      // in OVC, output.addressesIndex is defined correctly from the previous iteration

      if (utxo.outputID === SECP256K1_Transfer_Output) {
        const utxoAmount = BigInt(utxo.amount);
        // either user (0) or recovery (2)
        // On regular mode: [user, bitgo] (i.e. [0, 1])
        // On recovery mode: [backup, bitgo] (i.e. [2, 1])
        const userOrBackupIndex = this.recoverSigner ? 2 : 0;
        const bitgoIndex = 1;

        currentTotal = currentTotal + utxoAmount;

        const utxoId = avaxSerial.UTXOID.fromNative(utxo.txid, Number(utxo.outputidx));

        let addressesIndex: number[] = [];
        if (utxo.addressesIndex && bitgoIndexToOnChainIndex.size === 0) {
          addressesIndex = [...utxo.addressesIndex];
        } else {
          addressesIndex.push(bitgoIndexToOnChainIndex.get(userOrBackupIndex));
          addressesIndex.push(bitgoIndexToOnChainIndex.get(bitgoIndex));
        }

        const transferInputs = new TransferInput(
          new BigIntPr(utxoAmount),
          new Input([...addressesIndex].sort().map((num) => new Int(num)))
        );
        // TODO(CR-1073): remove log
        console.log(`using addressesIndex sorted: ${[...addressesIndex].sort()}`);

        const input = new avaxSerial.TransferableInput(utxoId, assetId, transferInputs);
        utxos.push(new Utxo(utxoId, assetId, transferInputs));

        inputs.push(input);
        if (!this.transaction.credentials || this.transaction.credentials.length == 0) {
          if (buildOutputs) {
            // For the bitgo signature we create an empty signature
            // For the user/backup signature we store the address that matches the key
            // if bitgo address comes before  < user/backup address

            // TODO(CR-1073): remove log
            console.log(`bitgo index on chain: ${utxo.addressesIndex[bitgoIndex]}`);
            console.log(`user Or Backup Index: ${utxo.addressesIndex[userOrBackupIndex]}`);
            if (utxo.addressesIndex[bitgoIndex] < utxo.addressesIndex[userOrBackupIndex]) {
              // TODO(CR-1073): remove log
              console.log(`user or backup credentials after bitgo`);
              credentials.push(
                new Credential([
                  utils.createNewSig(BufferAvax.from('').toString('hex')),
                  utils.createNewSig(
                    BufferAvax.from(this.transaction._fromAddresses[userOrBackupIndex]).toString('hex')
                  ),
                ])
              );
            } else {
              // TODO(CR-1073): remove log
              console.log(`user or backup credentials before bitgo`);
              credentials.push(
                new Credential([
                  utils.createNewSig(
                    BufferAvax.from(this.transaction._fromAddresses[userOrBackupIndex]).toString('hex')
                  ),
                  utils.createNewSig(BufferAvax.from('').toString('hex')),
                ])
              );
            }
          } else {
            // TODO(CR-1073): verify this else case for OVC
            credentials.push(
              new Credential(
                addressesIndex.map((i) =>
                  utils.createNewSig(BufferAvax.from(this.transaction._fromAddresses[i]).toString('hex'))
                )
              )
            );
          }
        } else {
          // TODO(CR-1073): remove log
          console.log(`reusing credentials from transaction`);
        }
      }
    });

    if (buildOutputs) {
      if (currentTotal < totalTarget) {
        throw new BuildTransactionError(
          `Utxo outputs get ${currentTotal.toString()} and ${totalTarget.toString()} is required`
        );
      } else if (currentTotal >= totalTarget) {
        const stakeOutput = new avaxSerial.TransferableOutput(
          assetId,
          new TransferOutput(
            new BigIntPr(this._stakeAmount),
            new OutputOwners(
              new BigIntPr(this.transaction._locktime),
              new Int(this.transaction._threshold),
              [...this.transaction._fromAddresses]
                .sort((a, b) => avaxUtils.bytesCompare(a, b))
                .map((a) => Address.fromBytes(a)[0])
            )
          )
        );
        stakeOutputs.push(stakeOutput);

        if (currentTotal > totalTarget) {
          const changeOutput = new avaxSerial.TransferableOutput(
            assetId,
            new TransferOutput(
              new BigIntPr(currentTotal - totalTarget),
              new OutputOwners(
                new BigIntPr(this.transaction._locktime),
                new Int(this.transaction._threshold),
                [...this.transaction._fromAddresses]
                  .sort((a, b) => avaxUtils.bytesCompare(a, b))
                  .map((a) => Address.fromBytes(a)[0])
              )
            )
          );
          changeOutputs.push(changeOutput);
        }
      }
    }
    inputs.sort((a, b) => {
      if (avaxUtils.bytesEqual(a.utxoID.txID.toBytes(), b.utxoID.txID.toBytes())) {
        return a.utxoID.outputIdx.value() - b.utxoID.outputIdx.value();
      }
      return avaxUtils.bytesCompare(a.utxoID.txID.toBytes(), b.utxoID.txID.toBytes());
    });
    return { inputs, stakeOutputs, changeOutputs, utxos, credentials };
  }

  /**
   * Build the add validator transaction
   * @protected
   */
  protected buildAvaxTransaction(): void {
    this.validateStakeDuration(this.transaction._startTime, this.transaction._endTime);
    const { inputs, stakeOutputs, changeOutputs, utxos, credentials } = this.calculateUtxos();
    const baseTx = avaxSerial.BaseTx.fromNative(
      this.transaction._networkID,
      this.transaction._blockchainID,
      changeOutputs,
      inputs,
      new Uint8Array() // default empty memo
    );

    const subnetValidator = pvmSerial.SubnetValidator.fromNative(
      this._nodeID,
      this._startTime,
      this._endTime,
      this._stakeAmount,
      networkIDs.PrimaryNetworkID
    );

    const signer = new pvmSerial.Signer(
      new pvmSerial.ProofOfPossession(
        AvaxUtils.hexToBuffer(this._blsPublicKey),
        AvaxUtils.hexToBuffer(this._blsSignature)
      )
    );

    const outputOwners = new OutputOwners(
      new BigIntPr(this.transaction._locktime),
      new Int(this.transaction._threshold),
      [...this.transaction._fromAddresses]
        .sort((a, b) => avaxUtils.bytesCompare(a, b))
        .map((a) => Address.fromBytes(a)[0])
    );

    // TODO(CR-1073): check this value
    //  Shares 10,000 times percentage of reward taken from delegators
    //  https://docs.avax.network/reference/avalanchego/p-chain/txn-format#unsigned-add-validator-tx
    const shares = new Int(1e4 * 2);

    const addressMaps = [...this.transaction._fromAddresses]
      .sort((a, b) => avaxUtils.bytesCompare(a, b))
      .map((address) => new AvaxUtils.AddressMap([[new Address(address), 0]]));

    this.transaction.setTransaction(
      new UnsignedTx(
        new pvmSerial.AddPermissionlessValidatorTx(
          baseTx,
          subnetValidator,
          signer,
          stakeOutputs,
          outputOwners,
          outputOwners,
          shares
        ),
        utxos,
        new AvaxUtils.AddressMaps(addressMaps),
        credentials
      )
    );
  }

  /** @inheritdoc */
  protected signImplementation({ key }: BaseKey): Transaction {
    this._signer.push(new KeyPair({ prv: key }));
    return this.transaction;
  }

  /** @inheritdoc */
  validateAddress(address: BaseAddress, addressFormat?: string): void {
    if (!utils.isValidAddress(address.address)) {
      throw new BuildTransactionError('Invalid address');
    }
  }

  /** @inheritdoc */
  protected get transaction(): Transaction {
    return this._transaction;
  }

  protected set transaction(transaction: Transaction) {
    this._transaction = transaction;
  }

  hasSigner(): boolean {
    return this._signer !== undefined && this._signer.length > 0;
  }

  /** @inheritdoc */
  validateKey({ key }: BaseKey): void {
    if (!new KeyPair({ prv: key })) {
      throw new BuildTransactionError('Invalid key');
    }
  }

  /**
   * Check the raw transaction has a valid format in the blockchain context, throw otherwise.
   *
   * @param rawTransaction Transaction in any format
   */
  validateRawTransaction(rawTransaction: string): void {
    utils.validateRawTransaction(rawTransaction);
  }

  /** @inheritdoc */
  validateTransaction(transaction?: Transaction): void {
    // throw new NotImplementedError('validateTransaction not implemented');
  }

  /** @inheritdoc */
  validateValue(value: BigNumber): void {
    if (value.isLessThan(0)) {
      throw new BuildTransactionError('Value cannot be less than zero');
    }
  }
}

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


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