PHP WebShell

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

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

import {
  BaseTransaction,
  Entry,
  InvalidTransactionError,
  ParseTransactionError,
  SigningError,
  TransactionRecipient,
  TransactionType,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Blockhash, PublicKey, Signer, Transaction as SolTransaction, SystemInstruction } from '@solana/web3.js';
import BigNumber from 'bignumber.js';
import base58 from 'bs58';
import { KeyPair } from '.';
import {
  InstructionBuilderTypes,
  UNAVAILABLE_TEXT,
  validInstructionData,
  ValidInstructionTypesEnum,
} from './constants';
import {
  DurableNonceParams,
  Memo,
  Nonce,
  StakingActivate,
  StakingAuthorizeParams,
  StakingWithdraw,
  TokenTransfer,
  TransactionExplanation,
  Transfer,
  TxData,
  WalletInit,
} from './iface';
import { instructionParamsFactory } from './instructionParamsFactory';
import {
  getInstructionType,
  getTransactionType,
  isValidRawTransaction,
  requiresAllSignatures,
  validateRawMsgInstruction,
} from './utils';

export class Transaction extends BaseTransaction {
  protected _solTransaction: SolTransaction;
  private _lamportsPerSignature: number | undefined;
  private _tokenAccountRentExemptAmount: string | undefined;
  protected _type: TransactionType;

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

  get solTransaction(): SolTransaction {
    return this._solTransaction;
  }

  set solTransaction(tx: SolTransaction) {
    this._solTransaction = tx;
  }

  private get numberOfRequiredSignatures(): number {
    return this._solTransaction.compileMessage().header.numRequiredSignatures;
  }

  private get numberOfATACreationInstructions(): number {
    return this._solTransaction.instructions.filter(
      (instruction) => getInstructionType(instruction) === ValidInstructionTypesEnum.InitializeAssociatedTokenAccount
    ).length;
  }

  /** @inheritDoc */
  get signablePayload(): Buffer {
    return this._solTransaction.serializeMessage();
  }

  /** @inheritDoc **/
  get id(): string {
    // Solana transaction ID === first signature: https://docs.solana.com/terminology#transaction-id
    if (this._solTransaction.signature) {
      return base58.encode(this._solTransaction.signature);
    } else {
      return UNAVAILABLE_TEXT;
    }
  }

  get lamportsPerSignature(): number | undefined {
    return this._lamportsPerSignature;
  }

  set lamportsPerSignature(lamportsPerSignature: number | undefined) {
    this._lamportsPerSignature = lamportsPerSignature;
  }

  get tokenAccountRentExemptAmount(): string | undefined {
    return this._tokenAccountRentExemptAmount;
  }

  set tokenAccountRentExemptAmount(tokenAccountRentExemptAmount: string | undefined) {
    this._tokenAccountRentExemptAmount = tokenAccountRentExemptAmount;
  }

  /** @inheritDoc */
  get signature(): string[] {
    const signatures: string[] = [];

    for (const solSignature of this._solTransaction.signatures) {
      if (solSignature.signature) {
        signatures.push(base58.encode(solSignature.signature));
      }
    }

    return signatures;
  }

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

  /** @inheritdoc */
  canSign(): boolean {
    return true;
  }

  /**
   * Signs transaction.
   *
   * @param {KeyPair} keyPair Signer keys.
   */
  async sign(keyPair: KeyPair[] | KeyPair): Promise<void> {
    if (!this._solTransaction || !this._solTransaction.recentBlockhash) {
      throw new SigningError('Nonce is required before signing');
    }
    if (!this._solTransaction || !this._solTransaction.feePayer) {
      throw new SigningError('feePayer is required before signing');
    }
    const keyPairs = keyPair instanceof Array ? keyPair : [keyPair];
    const signers: Signer[] = [];
    for (const kp of keyPairs) {
      const keys = kp.getKeys(true);
      if (!keys.prv) {
        throw new SigningError('Missing private key');
      }
      signers.push({ publicKey: new PublicKey(keys.pub), secretKey: keys.prv as Uint8Array });
    }
    try {
      this._solTransaction.partialSign(...signers);
    } catch (e) {
      throw e;
    }
  }

  /** @inheritdoc */
  toBroadcastFormat(): string {
    if (!this._solTransaction) {
      throw new ParseTransactionError('Empty transaction');
    }
    // The signatures can have null signatures (which means they are required but yet unsigned)
    // In order to be able to serializer the txs, we have to change the requireAllSignatures based
    // on if the TX is fully signed or not
    const requireAllSignatures = requiresAllSignatures(this._solTransaction.signatures);
    try {
      // Based on the recomendation encoding found here https://docs.solana.com/developing/clients/jsonrpc-api#sendtransaction
      // We use base64 encoding
      return this._solTransaction.serialize({ requireAllSignatures }).toString('base64');
    } catch (e) {
      throw e;
    }
  }

  /**
   * Sets this transaction payload
   *
   * @param rawTransaction
   */
  fromRawTransaction(rawTransaction: string): void {
    try {
      isValidRawTransaction(rawTransaction);
      this._solTransaction = SolTransaction.from(Buffer.from(rawTransaction, 'base64'));
      if (this._solTransaction.signature && this._solTransaction.signature !== null) {
        this._id = base58.encode(this._solTransaction.signature);
      }
      const transactionType = getTransactionType(this._solTransaction);
      switch (transactionType) {
        case TransactionType.WalletInitialization:
          this.setTransactionType(TransactionType.WalletInitialization);
          break;
        case TransactionType.Send:
          this.setTransactionType(TransactionType.Send);
          break;
        case TransactionType.StakingActivate:
          this.setTransactionType(TransactionType.StakingActivate);
          break;
        case TransactionType.StakingDeactivate:
          this.setTransactionType(TransactionType.StakingDeactivate);
          break;
        case TransactionType.StakingWithdraw:
          this.setTransactionType(TransactionType.StakingWithdraw);
          break;
        case TransactionType.AssociatedTokenAccountInitialization:
          this.setTransactionType(TransactionType.AssociatedTokenAccountInitialization);
          break;
        case TransactionType.CloseAssociatedTokenAccount:
          this.setTransactionType(TransactionType.CloseAssociatedTokenAccount);
          break;
        case TransactionType.StakingAuthorize:
          this.setTransactionType(TransactionType.StakingAuthorize);
          break;
        case TransactionType.StakingAuthorizeRaw:
          this.setTransactionType(TransactionType.StakingAuthorizeRaw);
          break;
        case TransactionType.StakingDelegate:
          this.setTransactionType(TransactionType.StakingDelegate);
          break;
      }
      if (transactionType !== TransactionType.StakingAuthorizeRaw) {
        this.loadInputsAndOutputs();
      }
    } catch (e) {
      throw e;
    }
  }

  /** @inheritdoc */
  toJson(): TxData {
    if (!this._solTransaction) {
      throw new ParseTransactionError('Empty transaction');
    }

    let durableNonce: DurableNonceParams | undefined;
    if (this._solTransaction.nonceInfo) {
      const nonceInstruction = SystemInstruction.decodeNonceAdvance(this._solTransaction.nonceInfo.nonceInstruction);
      durableNonce = {
        walletNonceAddress: nonceInstruction.noncePubkey.toString(),
        authWalletAddress: nonceInstruction.authorizedPubkey.toString(),
      };
    }
    const instructionData = instructionParamsFactory(
      this._type,
      this._solTransaction.instructions,
      this._coinConfig.name
    );
    if (this._type) {
      if (
        !durableNonce &&
        instructionData.length > 1 &&
        instructionData[0].type === InstructionBuilderTypes.NonceAdvance
      ) {
        durableNonce = instructionData[0].params;
      }
    }
    const result: TxData = {
      id: this._solTransaction.signature ? this.id : undefined,
      feePayer: this._solTransaction.feePayer?.toString(),
      lamportsPerSignature: this.lamportsPerSignature,
      nonce: this.getNonce(),
      durableNonce: durableNonce,
      numSignatures: this.signature.length,
      instructionsData: instructionData,
    };
    return result;
  }

  /**
   * Get the nonce from the Solana Transaction
   * Throws if not set
   */
  private getNonce(): Blockhash {
    if (this._solTransaction.recentBlockhash) {
      return this._solTransaction.recentBlockhash;
    } else if (this._solTransaction.nonceInfo) {
      return this._solTransaction.nonceInfo.nonce;
    } else {
      throw new InvalidTransactionError('Nonce is not set');
    }
  }

  /**
   * Load the input and output data on this transaction.
   */
  loadInputsAndOutputs(): void {
    if (!this._solTransaction || this._solTransaction.instructions?.length === 0) {
      return;
    }
    const outputs: Entry[] = [];
    const inputs: Entry[] = [];
    const instructionParams = instructionParamsFactory(
      this.type,
      this._solTransaction.instructions,
      this._coinConfig.name
    );

    for (const instruction of instructionParams) {
      switch (instruction.type) {
        case InstructionBuilderTypes.CreateNonceAccount:
          inputs.push({
            address: instruction.params.fromAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          break;
        case InstructionBuilderTypes.Transfer:
          inputs.push({
            address: instruction.params.fromAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          outputs.push({
            address: instruction.params.toAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          break;
        case InstructionBuilderTypes.TokenTransfer:
          inputs.push({
            address: instruction.params.fromAddress,
            value: instruction.params.amount,
            coin: instruction.params.tokenName,
          });
          outputs.push({
            address: instruction.params.toAddress,
            value: instruction.params.amount,
            coin: instruction.params.tokenName,
          });
          break;
        case InstructionBuilderTypes.StakingActivate:
          inputs.push({
            address: instruction.params.fromAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          outputs.push({
            address: instruction.params.stakingAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          break;
        case InstructionBuilderTypes.StakingDeactivate:
          if (instruction.params.amount && instruction.params.unstakingAddress) {
            inputs.push({
              address: instruction.params.stakingAddress,
              value: instruction.params.amount,
              coin: this._coinConfig.name,
            });
            outputs.push({
              address: instruction.params.unstakingAddress,
              value: instruction.params.amount,
              coin: this._coinConfig.name,
            });
          }
          break;
        case InstructionBuilderTypes.StakingWithdraw:
          inputs.push({
            address: instruction.params.stakingAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          outputs.push({
            address: instruction.params.fromAddress,
            value: instruction.params.amount,
            coin: this._coinConfig.name,
          });
          break;
        case InstructionBuilderTypes.CreateAssociatedTokenAccount:
          break;
        case InstructionBuilderTypes.CloseAssociatedTokenAccount:
          break;
        case InstructionBuilderTypes.StakingAuthorize:
          break;
        case InstructionBuilderTypes.StakingDelegate:
          break;
        case InstructionBuilderTypes.SetPriorityFee:
          break;
      }
    }
    this._outputs = outputs;
    this._inputs = inputs;
  }

  /** @inheritDoc */
  explainTransaction(): TransactionExplanation {
    if (validateRawMsgInstruction(this._solTransaction.instructions)) {
      return this.explainRawMsgAuthorizeTransaction();
    }
    const decodedInstructions = instructionParamsFactory(
      this._type,
      this._solTransaction.instructions,
      this._coinConfig.name
    );

    let memo: string | undefined = undefined;
    let durableNonce: DurableNonceParams | undefined = undefined;

    let outputAmount = new BigNumber(0);
    const outputs: TransactionRecipient[] = [];

    for (const instruction of decodedInstructions) {
      switch (instruction.type) {
        case InstructionBuilderTypes.NonceAdvance:
          durableNonce = (instruction as Nonce).params;
          break;
        case InstructionBuilderTypes.Memo:
          memo = (instruction as Memo).params.memo;
          break;
        case InstructionBuilderTypes.Transfer:
          const transferInstruction = instruction as Transfer;
          outputs.push({
            address: transferInstruction.params.toAddress,
            amount: transferInstruction.params.amount,
          });
          outputAmount = outputAmount.plus(transferInstruction.params.amount);
          break;
        case InstructionBuilderTypes.TokenTransfer:
          const tokenTransferInstruction = instruction as TokenTransfer;
          outputs.push({
            address: tokenTransferInstruction.params.toAddress,
            amount: tokenTransferInstruction.params.amount,
            tokenName: tokenTransferInstruction.params.tokenName,
          });
          break;
        case InstructionBuilderTypes.CreateNonceAccount:
          const createInstruction = instruction as WalletInit;
          outputs.push({
            address: createInstruction.params.nonceAddress,
            amount: createInstruction.params.amount,
          });
          outputAmount = outputAmount.plus(createInstruction.params.amount);
          break;
        case InstructionBuilderTypes.StakingActivate:
          const stakingActivateInstruction = instruction as StakingActivate;
          outputs.push({
            address: stakingActivateInstruction.params.stakingAddress,
            amount: stakingActivateInstruction.params.amount,
          });
          outputAmount = outputAmount.plus(stakingActivateInstruction.params.amount);
          break;
        case InstructionBuilderTypes.StakingWithdraw:
          const stakingWithdrawInstruction = instruction as StakingWithdraw;
          outputs.push({
            address: stakingWithdrawInstruction.params.fromAddress,
            amount: stakingWithdrawInstruction.params.amount,
          });
          outputAmount = outputAmount.plus(stakingWithdrawInstruction.params.amount);
          break;
        case InstructionBuilderTypes.CreateAssociatedTokenAccount:
          break;
        default:
          continue;
      }

      // After deserializing a transaction, durable nonce details are populated in the nonceInfo field
      if (!durableNonce && this._solTransaction.nonceInfo) {
        const nonceAdvanceInstruction = SystemInstruction.decodeNonceAdvance(
          this._solTransaction.nonceInfo.nonceInstruction
        );
        durableNonce = {
          authWalletAddress: nonceAdvanceInstruction.authorizedPubkey.toString(),
          walletNonceAddress: nonceAdvanceInstruction.noncePubkey.toString(),
        };
      }
    }

    return this.getExplainedTransaction(outputAmount, outputs, memo, durableNonce);
  }

  private calculateFee(): string {
    if (this.lamportsPerSignature || this.tokenAccountRentExemptAmount) {
      const signatureFees = this.lamportsPerSignature
        ? new BigNumber(this.lamportsPerSignature).multipliedBy(this.numberOfRequiredSignatures).toFixed(0)
        : 0;
      const rentFees = this.tokenAccountRentExemptAmount
        ? new BigNumber(this.tokenAccountRentExemptAmount).multipliedBy(this.numberOfATACreationInstructions).toFixed(0)
        : 0;
      return new BigNumber(signatureFees).plus(rentFees).toFixed(0);
    }
    return UNAVAILABLE_TEXT;
  }

  protected getExplainedTransaction(
    outputAmount: BigNumber,
    outputs: TransactionRecipient[],
    memo: undefined | string = undefined,
    durableNonce: undefined | DurableNonceParams = undefined
  ): TransactionExplanation {
    const feeString = this.calculateFee();
    return {
      displayOrder: [
        'id',
        'type',
        'blockhash',
        'durableNonce',
        'outputAmount',
        'changeAmount',
        'outputs',
        'changeOutputs',
        'fee',
        'memo',
      ],
      id: this.id,
      type: TransactionType[this.type].toString(),
      changeOutputs: [],
      changeAmount: '0',
      outputAmount: outputAmount.toFixed(0),
      outputs: outputs,
      fee: {
        fee: feeString,
        feeRate: this.lamportsPerSignature,
      },
      memo: memo,
      blockhash: this.getNonce(),
      durableNonce: durableNonce,
    };
  }

  private explainRawMsgAuthorizeTransaction(): TransactionExplanation {
    const { instructions } = this._solTransaction;
    const nonceInstruction = SystemInstruction.decodeNonceAdvance(instructions[0]);
    const durableNonce = {
      walletNonceAddress: nonceInstruction.noncePubkey.toString(),
      authWalletAddress: nonceInstruction.authorizedPubkey.toString(),
    };
    const data = instructions[1].data.toString('hex');
    const stakingAuthorizeParams: StakingAuthorizeParams =
      data === validInstructionData
        ? {
            stakingAddress: instructions[1].keys[0].pubkey.toString(),
            oldWithdrawAddress: instructions[1].keys[2].pubkey.toString(),
            newWithdrawAddress: instructions[1].keys[3].pubkey.toString(),
            custodianAddress: instructions[1].keys[4].pubkey.toString(),
          }
        : {
            stakingAddress: instructions[1].keys[0].pubkey.toString(),
            oldWithdrawAddress: '',
            newWithdrawAddress: '',
            oldStakingAuthorityAddress: instructions[1].keys[2].pubkey.toString(),
            newStakingAuthorityAddress: instructions[1].keys[3].pubkey.toString(),
          };
    const feeString = this.calculateFee();
    return {
      displayOrder: [
        'id',
        'type',
        'blockhash',
        'durableNonce',
        'outputAmount',
        'changeAmount',
        'outputs',
        'changeOutputs',
        'fee',
        'memo',
      ],
      id: this.id,
      type: TransactionType[this.type].toString(),
      changeOutputs: [],
      changeAmount: '0',
      outputAmount: 0,
      outputs: [],
      fee: {
        fee: feeString,
        feeRate: this.lamportsPerSignature,
      },
      blockhash: this.getNonce(),
      durableNonce: durableNonce,
      stakingAuthorize: stakingAuthorizeParams,
    };
  }
}

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


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