PHP WebShell

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

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

import {
  AliasEnvironments,
  BaseCoin,
  bitcoin,
  BitGoBase,
  BitGoRequest,
  CoinConstructor,
  common,
  DecryptKeysOptions,
  DecryptOptions,
  defaultConstants,
  EcdhDerivedKeypair,
  EncryptOptions,
  EnvironmentName,
  generateRandomPassword,
  getAddressP2PKH,
  getSharedSecret,
  GetSharingKeyOptions,
  GetSigningKeyApi,
  GlobalCoinFactory,
  IRequestTracer,
  makeRandomKey,
  sanitizeLegacyPath,
} from '@bitgo/sdk-core';
import * as sdkHmac from '@bitgo/sdk-hmac';
import * as utxolib from '@bitgo/utxo-lib';
import { bip32, ECPairInterface } from '@bitgo/utxo-lib';
import * as bitcoinMessage from 'bitcoinjs-message';
import { type Agent } from 'http';
import debugLib from 'debug';
import * as _ from 'lodash';
import * as secp256k1 from 'secp256k1';
import * as superagent from 'superagent';
import {
  handleResponseError,
  handleResponseResult,
  serializeRequestData,
  setRequestQueryString,
  toBitgoRequest,
  verifyResponse,
} from './api';
import { decrypt, encrypt } from './encrypt';
import { verifyAddress } from './v1/verifyAddress';
import {
  AccessTokenOptions,
  AddAccessTokenOptions,
  AddAccessTokenResponse,
  AdditionalHeadersCallback,
  AuthenticateOptions,
  AuthenticateWithAuthCodeOptions,
  BitGoAPIOptions,
  BitGoJson,
  BitGoSimulateWebhookOptions,
  CalculateHmacSubjectOptions,
  CalculateRequestHeadersOptions,
  CalculateRequestHmacOptions,
  ChangePasswordOptions,
  DeprecatedVerifyAddressOptions,
  EstimateFeeOptions,
  ExtendTokenOptions,
  GetEcdhSecretOptions,
  GetUserOptions,
  ListWebhookNotificationsOptions,
  LoginResponse,
  PingOptions,
  ProcessedAuthenticationOptions,
  ReconstitutedSecret,
  ReconstituteSecretOptions,
  RegisterPushTokenOptions,
  RemoveAccessTokenOptions,
  RequestHeaders,
  RequestMethods,
  SplitSecret,
  SplitSecretOptions,
  TokenIssuance,
  TokenIssuanceResponse,
  UnlockOptions,
  User,
  VerifyPasswordOptions,
  VerifyPushTokenOptions,
  VerifyResponseInfo,
  VerifyResponseOptions,
  VerifyShardsOptions,
  WebhookOptions,
} from './types';
import shamir = require('secrets.js-grempe');
import pjson = require('../package.json');
const debug = debugLib('bitgo:api');

const Blockchain = require('./v1/blockchain');
const Keychains = require('./v1/keychains');
import Wallet = require('./v1/wallet');

const Wallets = require('./v1/wallets');
const Markets = require('./v1/markets');
const PendingApprovals = require('./v1/pendingapprovals');
const TravelRule = require('./v1/travelRule');
const TransactionBuilder = require('./v1/transactionBuilder');

export class BitGoAPI implements BitGoBase {
  // v1 types
  protected _keychains: any;
  protected _wallets: any;
  protected _markets?: any;
  protected _blockchain?: any;
  protected _travelRule?: any;
  protected _pendingApprovals?: any;

  protected static _constants: any;
  protected static _constantsExpire: any;
  protected static _testnetWarningMessage = false;
  public readonly env: EnvironmentName;
  protected readonly _baseUrl: string;
  protected readonly _baseApiUrl: string;
  protected readonly _baseApiUrlV2: string;
  protected readonly _baseApiUrlV3: string;
  protected readonly _env: EnvironmentName;
  protected readonly _authVersion: Exclude<BitGoAPIOptions['authVersion'], undefined> = 2;
  protected _hmacVerification = true;
  protected readonly _proxy?: string;
  protected _user?: User;
  protected _extensionKey?: ECPairInterface;
  protected _reqId?: IRequestTracer;
  protected _token?: string;
  protected _version = pjson.version;
  protected _userAgent?: string;
  protected _ecdhXprv?: string;
  protected _refreshToken?: string;
  protected readonly _clientId?: string;
  protected readonly _clientSecret?: string;
  protected _validate: boolean;
  public readonly cookiesPropagationEnabled: boolean;
  private _customProxyAgent?: Agent;
  private getAdditionalHeadersCb?: AdditionalHeadersCallback;

  constructor(params: BitGoAPIOptions = {}) {
    this.getAdditionalHeadersCb = params.getAdditionalHeadersCb;
    this.cookiesPropagationEnabled = false;
    if (
      !common.validateParams(
        params,
        [],
        [
          'accessToken',
          'userAgent',
          'customRootURI',
          'customBitcoinNetwork',
          'serverXpub',
          'stellarFederationServerUrl',
        ]
      ) ||
      (params.useProduction && !_.isBoolean(params.useProduction))
    ) {
      throw new Error('invalid argument');
    }

    // By default, we operate on the test server.
    // Deprecate useProduction in the future
    let env: EnvironmentName;

    if (params.useProduction) {
      if (params.env && params.env !== 'prod') {
        throw new Error('cannot use useProduction when env=' + params.env);
      }
      env = 'prod';
    } else if (
      params.customRootURI ||
      params.customBitcoinNetwork ||
      params.customSigningAddress ||
      params.serverXpub ||
      process.env.BITGO_CUSTOM_ROOT_URI ||
      process.env.BITGO_CUSTOM_BITCOIN_NETWORK
    ) {
      // for branch deploys, we want to be able to specify custom endpoints while still
      // maintaining the name of specified the environment
      env = params.env === 'branch' ? 'branch' : 'custom';
      if (params.customRootURI) {
        common.Environments[env].uri = params.customRootURI;
      }
      if (params.customBitcoinNetwork) {
        common.Environments[env].network = params.customBitcoinNetwork;
      }
      if (params.customSigningAddress) {
        (common.Environments[env] as any).customSigningAddress = params.customSigningAddress;
      }
      if (params.serverXpub) {
        common.Environments[env].serverXpub = params.serverXpub;
      }
      if (params.stellarFederationServerUrl) {
        common.Environments[env].stellarFederationServerUrl = params.stellarFederationServerUrl;
      }
      if (params.cookiesPropagationEnabled) {
        this.cookiesPropagationEnabled = true;
      }
    } else {
      env = params.env || (process.env.BITGO_ENV as EnvironmentName);
    }

    // if this hasn't been set to true already some conditions are not met
    if (params.cookiesPropagationEnabled && !this.cookiesPropagationEnabled) {
      throw new Error('Cookies are only allowed when custom URIs are in use');
    }

    if (params.authVersion !== undefined) {
      this._authVersion = params.authVersion;
    }

    // if this env is an alias, swap it out with the equivalent supported environment
    if (env in AliasEnvironments) {
      env = AliasEnvironments[env];
    }

    if (env === 'custom' && _.isUndefined(common.Environments[env].uri)) {
      throw new Error(
        'must use --customrooturi or set the BITGO_CUSTOM_ROOT_URI environment variable when using the custom environment'
      );
    }

    if (env) {
      if (common.Environments[env]) {
        this._baseUrl = common.Environments[env].uri;
      } else {
        throw new Error('invalid environment ' + env + '. Supported environments: prod, test, dev, latest');
      }
    } else {
      env = 'test';
      if (!BitGoAPI._testnetWarningMessage) {
        BitGoAPI._testnetWarningMessage = true;
        console.log('BitGo SDK env not set - defaulting to test at test.bitgo.com.');
      }
      this._baseUrl = common.Environments[env].uri;
    }
    this._env = this.env = env;

    const supportedApiTokens = [
      'etherscanApiToken',
      'polygonscanApiToken',
      'arbiscanApiToken',
      'optimisticEtherscanApiToken',
      'zksyncExplorerApiToken',
      'bscscanApiToken',
      'coredaoExplorerApiToken',
      'oasExplorerApiToken',
      'baseethApiToken',
      'sgbExplorerApiToken',
      'flrExplorerApiToken',
      'xdcExplorerApiToken',
      'wemixExplorerApiToken',
    ];

    Object.keys(params).forEach((key) => {
      if (supportedApiTokens.includes(key)) {
        common.Environments[env][key] = params[key];
      }
    });

    common.setNetwork(common.Environments[env].network);

    this._baseApiUrl = this._baseUrl + '/api/v1';
    this._baseApiUrlV2 = this._baseUrl + '/api/v2';
    this._baseApiUrlV3 = this._baseUrl + '/api/v3';
    this._token = params.accessToken;
    this._userAgent = params.userAgent || 'BitGoJS-api/' + this.version();
    this._reqId = undefined;
    this._refreshToken = params.refreshToken;
    this._clientId = params.clientId;
    this._clientSecret = params.clientSecret;
    this._keychains = null;
    this._wallets = null;

    // whether to perform extra client-side validation for some things, such as
    // address validation or signature validation. defaults to true, but can be
    // turned off by setting to false. can also be overridden individually in the
    // functions that use it.
    this._validate = params.validate === undefined ? true : params.validate;

    if (!params.hmacVerification && params.hmacVerification !== undefined) {
      if ((env == 'prod' || env == 'adminProd') && common.Environments[env].hmacVerificationEnforced) {
        throw new Error(`Cannot disable request HMAC verification in environment ${this.getEnv()}`);
      }
      debug('HMAC verification explicitly disabled by constructor option');
      this._hmacVerification = params.hmacVerification;
    }

    if ((process as any).browser && params.customProxyAgent) {
      throw new Error('should not use https proxy while in browser');
    }

    this._customProxyAgent = params.customProxyAgent;

    // capture outer stack so we have useful debug information if fetch constants fails
    const e = new Error();

    // Kick off first load of constants
    this.fetchConstants().catch((err) => {
      if (err) {
        // make sure an error does not terminate the entire script
        console.error('failed to fetch initial client constants from BitGo');
        debug(e.stack);
      }
    });
  }

  /**
   * Get a superagent request for specified http method and URL configured to the SDK configuration
   * @param method - http method for the new request
   * @param url - URL for the new request
   */
  protected getAgentRequest(method: RequestMethods, url: string): superagent.SuperAgentRequest {
    let req: superagent.SuperAgentRequest = superagent[method](url);
    if (this.cookiesPropagationEnabled) {
      req = req.withCredentials();
    }
    return req;
  }
  /**
   * Create a basecoin object
   * @param name
   */
  public coin(name: string): BaseCoin {
    return GlobalCoinFactory.getInstance(this, name);
  }

  /**
   * Return the current BitGo environment
   */
  getEnv(): EnvironmentName {
    return this._env;
  }

  /**
   * Return the current auth version used for requests to the BitGo server
   */
  getAuthVersion(): number {
    return this._authVersion;
  }

  /**
   * This is a patching function which can apply our authorization
   * headers to any outbound request.
   * @param method
   */
  private requestPatch(method: RequestMethods, url: string) {
    const req = this.getAgentRequest(method, url);
    if (this._customProxyAgent) {
      debug('using custom proxy agent');
      if (this._customProxyAgent) {
        req.agent(this._customProxyAgent);
      }
    }

    const originalThen = req.then.bind(req);
    req.then = (onfulfilled, onrejected) => {
      // intercept a request before it's submitted to the server for v2 authentication (based on token)
      if (this._version) {
        // TODO - decide where to get version
        req.set('BitGo-SDK-Version', this._version);
      }

      if (!_.isUndefined(this._reqId)) {
        req.set('Request-ID', this._reqId.toString());

        // increment after setting the header so the sequence numbers start at 0
        this._reqId.inc();

        // request ids must be set before each request instead of being kept
        // inside the bitgo object. This is to prevent reentrancy issues where
        // multiple simultaneous requests could cause incorrect reqIds to be used
        delete this._reqId;
      }

      // prevent IE from caching requests
      req.set('If-Modified-Since', 'Mon, 26 Jul 1997 05:00:00 GMT');

      if (!(process as any).browser && this._userAgent) {
        // If not in the browser, set the User-Agent. Browsers don't allow
        // setting of User-Agent, so we must disable this when run in the
        // browser (browserify sets process.browser).
        req.set('User-Agent', this._userAgent);
      }

      // Set the request timeout to just above 5 minutes by default
      req.timeout((process.env.BITGO_TIMEOUT as any) * 1000 || 305 * 1000);

      // if there is no token, and we're not logged in, the request cannot be v2 authenticated
      req.isV2Authenticated = true;
      req.authenticationToken = this._token;
      // some of the older tokens appear to be only 40 characters long
      if ((this._token && this._token.length !== 67 && this._token.indexOf('v2x') !== 0) || req.forceV1Auth) {
        // use the old method
        req.isV2Authenticated = false;

        req.set('Authorization', 'Bearer ' + this._token);
        debug('sending v1 %s request to %s with token %s', method, url, this._token?.substr(0, 8));
        return originalThen(onfulfilled).catch(onrejected);
      }

      req.set('BitGo-Auth-Version', this._authVersion === 3 ? '3.0' : '2.0');

      const data = serializeRequestData(req);
      if (this._token) {
        setRequestQueryString(req);

        const requestProperties = this.calculateRequestHeaders({
          url: req.url,
          token: this._token,
          method,
          text: data || '',
          authVersion: this._authVersion,
        });
        req.set('Auth-Timestamp', requestProperties.timestamp.toString());

        // we're not sending the actual token, but only its hash
        req.set('Authorization', 'Bearer ' + requestProperties.tokenHash);
        debug('sending v2 %s request to %s with token %s', method, url, this._token?.substr(0, 8));

        // set the HMAC
        req.set('HMAC', requestProperties.hmac);
      }

      if (this.getAdditionalHeadersCb) {
        const additionalHeaders = this.getAdditionalHeadersCb(method, url, data);
        for (const { key, value } of additionalHeaders) {
          req.set(key, value);
        }
      }

      /**
       * Verify the response before calling the original onfulfilled handler,
       * and make sure onrejected is called if a verification error is encountered
       */
      const newOnFulfilled = onfulfilled
        ? (response: superagent.Response) => {
            // HMAC verification is only allowed to be skipped in certain environments.
            // This is checked in the constructor, but checking it again at request time
            // will help prevent against tampering of this property after the object is created
            if (!this._hmacVerification && !common.Environments[this.getEnv()].hmacVerificationEnforced) {
              return onfulfilled(response);
            }

            const verifiedResponse = verifyResponse(this, this._token, method, req, response, this._authVersion);
            return onfulfilled(verifiedResponse);
          }
        : null;
      return originalThen(newOnFulfilled).catch(onrejected);
    };
    return toBitgoRequest(req);
  }

  get(url: string): BitGoRequest {
    return this.requestPatch('get', url);
  }
  post(url: string): BitGoRequest {
    return this.requestPatch('post', url);
  }
  put(url: string): BitGoRequest {
    return this.requestPatch('put', url);
  }
  del(url: string): BitGoRequest {
    return this.requestPatch('del', url);
  }
  patch(url: string): BitGoRequest {
    return this.requestPatch('patch', url);
  }
  options(url: string): BitGoRequest {
    return this.requestPatch('options', url);
  }

  /**
   * Calculate the HMAC for the given key and message
   * @param key {String} - the key to use for the HMAC
   * @param message {String} - the actual message to HMAC
   * @returns {*} - the result of the HMAC operation
   */
  calculateHMAC(key: string, message: string): string {
    return sdkHmac.calculateHMAC(key, message);
  }

  /**
   * Calculate the subject string that is to be HMAC'ed for a HTTP request or response
   * @param urlPath request url, including query params
   * @param text request body text
   * @param timestamp request timestamp from `Date.now()`
   * @param statusCode Only set for HTTP responses, leave blank for requests
   * @param method request method
   * @returns {string}
   */
  calculateHMACSubject(params: CalculateHmacSubjectOptions): string {
    return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion });
  }

  /**
   * Calculate the HMAC for an HTTP request
   */
  calculateRequestHMAC(params: CalculateRequestHmacOptions): string {
    return sdkHmac.calculateRequestHMAC({ ...params, authVersion: this._authVersion });
  }

  /**
   * Calculate request headers with HMAC
   */
  calculateRequestHeaders(params: CalculateRequestHeadersOptions): RequestHeaders {
    return sdkHmac.calculateRequestHeaders({ ...params, authVersion: this._authVersion });
  }

  /**
   * Verify the HMAC for an HTTP response
   */
  verifyResponse(params: VerifyResponseOptions): VerifyResponseInfo {
    return sdkHmac.verifyResponse({ ...params, authVersion: this._authVersion });
  }

  /**
   * Fetch useful constant values from the BitGo server.
   * These values do change infrequently, so they need to be fetched,
   * but are unlikely to change during the lifetime of a BitGo object,
   * so they can safely cached.
   */
  async fetchConstants(): Promise<any> {
    const env = this.getEnv();

    if (!BitGoAPI._constants) {
      BitGoAPI._constants = {};
    }
    if (!BitGoAPI._constantsExpire) {
      BitGoAPI._constantsExpire = {};
    }

    if (BitGoAPI._constants[env] && BitGoAPI._constantsExpire[env] && new Date() < BitGoAPI._constantsExpire[env]) {
      return BitGoAPI._constants[env];
    }

    // client constants call cannot be authenticated using the normal HMAC validation
    // scheme, so we need to use a raw superagent instance to do this request.
    // Proxy settings must still be respected however
    const resultPromise = this.getAgentRequest('get', this.url('/client/constants'));
    resultPromise.set('BitGo-SDK-Version', this._version);
    if (this._customProxyAgent) {
      resultPromise.agent(this._customProxyAgent);
    }
    const result = await resultPromise;
    BitGoAPI._constants[env] = result.body.constants;

    if (result.body?.ttl && typeof result.body?.ttl === 'number') {
      BitGoAPI._constantsExpire[env] = new Date(new Date().getTime() + (result.body.ttl as number) * 1000);
    }

    return BitGoAPI._constants[env];
  }

  /**
   * Create a url for calling BitGo platform APIs
   * @param path
   * @param version
   */
  url(path: string, version = 1): string {
    const baseUrl = version === 3 ? this._baseApiUrlV3 : version === 2 ? this._baseApiUrlV2 : this._baseApiUrl;
    return baseUrl + path;
  }

  /**
   * Create a url for calling BitGo microservice APIs
   */
  microservicesUrl(path: string): string {
    return this._baseUrl + path;
  }

  /**
   * Gets the version of the BitGoJS package
   */
  version(): string {
    return this._version;
  }

  /**
   * Test connectivity to the server
   * @param params
   */
  ping({ reqId }: PingOptions = {}): Promise<any> {
    if (reqId) {
      this._reqId = reqId;
    }

    return this.get(this.url('/ping')).result();
  }

  /**
   * Set a request tracer to provide request IDs during multi-request workflows
   */
  setRequestTracer(reqTracer: IRequestTracer): void {
    if (reqTracer) {
      this._reqId = reqTracer;
    }
  }

  /**
   * Utility function to encrypt locally.
   */
  encrypt(params: EncryptOptions): string {
    common.validateParams(params, ['input', 'password'], ['adata']);
    if (!params.password) {
      throw new Error(`cannot encrypt without password`);
    }
    return encrypt(params.password, params.input, { adata: params.adata });
  }

  /**
   * Decrypt an encrypted string locally.
   */
  decrypt(params: DecryptOptions): string {
    params = params || {};
    common.validateParams(params, ['input', 'password'], []);
    if (!params.password) {
      throw new Error(`cannot decrypt without password`);
    }
    try {
      return decrypt(params.password, params.input);
    } catch (error) {
      if (error.message.includes("ccm: tag doesn't match")) {
        error.message = 'password error - ' + error.message;
      }
      throw error;
    }
  }

  /**
   * Attempt to decrypt multiple wallet keys with the provided passphrase
   * @param {DecryptKeysOptions} params - Parameters object containing wallet key pairs and password
   * @param {Array<{walletId: string, encryptedPrv: string}>} params.walletIdEncryptedKeyPairs - Array of wallet ID and encrypted private key pairs
   * @param {string} params.password - The passphrase to attempt decryption with
   * @returns {string[]} - Array of wallet IDs for which decryption failed
   */
  decryptKeys(params: DecryptKeysOptions): string[] {
    params = params || {};
    if (!params.walletIdEncryptedKeyPairs) {
      throw new Error('Missing parameter: walletIdEncryptedKeyPairs');
    }

    if (!params.password) {
      throw new Error('Missing parameter: password');
    }

    if (!Array.isArray(params.walletIdEncryptedKeyPairs)) {
      throw new Error('walletIdEncryptedKeyPairs must be an array');
    }

    if (params.walletIdEncryptedKeyPairs.length === 0) {
      return [];
    }

    const failedWalletIds: string[] = [];

    for (const keyPair of params.walletIdEncryptedKeyPairs) {
      if (!keyPair.walletId || typeof keyPair.walletId !== 'string') {
        throw new Error('each key pair must have a string walletId');
      }

      if (!keyPair.encryptedPrv || typeof keyPair.encryptedPrv !== 'string') {
        throw new Error('each key pair must have a string encryptedPrv');
      }

      try {
        this.decrypt({
          input: keyPair.encryptedPrv,
          password: params.password,
        });
        // If no error was thrown, decryption was successful
      } catch (error) {
        // If decryption fails, add the walletId to the failed list
        failedWalletIds.push(keyPair.walletId);
      }
    }

    return failedWalletIds;
  }

  /**
   * Serialize this BitGo object to a JSON object.
   *
   * Caution: contains sensitive data
   */
  toJSON(): BitGoJson {
    return {
      user: this._user,
      token: this._token,
      extensionKey: this._extensionKey ? this._extensionKey.toWIF() : undefined,
      ecdhXprv: this._ecdhXprv,
    };
  }

  /**
   * Get the current user
   */
  user(): User | undefined {
    return this._user;
  }

  /**
   * Deserialize a JSON serialized BitGo object.
   *
   * Overwrites the properties on the current BitGo object with
   * those of the deserialzed object.
   *
   * @param json
   */
  fromJSON(json: BitGoJson): void {
    this._user = json.user;
    this._token = json.token;
    this._ecdhXprv = json.ecdhXprv;
    if (json.extensionKey) {
      const network = common.Environments[this.getEnv()].network;
      this._extensionKey = utxolib.ECPair.fromWIF(
        json.extensionKey,
        utxolib.networks[network] as utxolib.BitcoinJSNetwork
      );
    }
  }

  /**
   * Process the username, password and otp into an object containing the username and hashed password, ready to
   * send to bitgo for authentication.
   */
  preprocessAuthenticationParams({
    username,
    password,
    otp,
    forceSMS,
    extensible,
    trust,
    forReset2FA,
  }: AuthenticateOptions): ProcessedAuthenticationOptions {
    if (!_.isString(username)) {
      throw new Error('expected string username');
    }

    if (!_.isString(password)) {
      throw new Error('expected string password');
    }

    const lowerName = username.toLowerCase();
    // Calculate the password HMAC so we don't send clear-text passwords
    const hmacPassword = this.calculateHMAC(lowerName, password);

    const authParams: ProcessedAuthenticationOptions = {
      email: lowerName,
      password: hmacPassword,
      forceSMS: !!forceSMS,
    };

    if (otp) {
      authParams.otp = otp;
      if (trust) {
        authParams.trust = 1;
      }
    }

    if (extensible) {
      this._extensionKey = makeRandomKey();
      authParams.extensible = true;
      authParams.extensionAddress = getAddressP2PKH(this._extensionKey);
    }

    if (forReset2FA) {
      authParams.forReset2FA = true;
    }

    return authParams;
  }

  /**
   * Validate the passkey response is in the expected format
   * Should be as is returned from navigator.credentials.get()
   */
  validatePasskeyResponse(passkeyResponse: string): void {
    const parsedPasskeyResponse = JSON.parse(passkeyResponse);
    if (!parsedPasskeyResponse && !parsedPasskeyResponse.response) {
      throw new Error('unexpected webauthnResponse');
    }
    if (!_.isString(parsedPasskeyResponse.id)) {
      throw new Error('id is missing');
    }
    if (!_.isString(parsedPasskeyResponse.response.authenticatorData)) {
      throw new Error('authenticatorData is missing');
    }
    if (!_.isString(parsedPasskeyResponse.response.clientDataJSON)) {
      throw new Error('clientDataJSON is missing');
    }
    if (!_.isString(parsedPasskeyResponse.response.signature)) {
      throw new Error('signature is missing');
    }
    if (!_.isString(parsedPasskeyResponse.response.userHandle)) {
      throw new Error('userHandle is missing');
    }
  }

  /**
   * Synchronous method for activating an access token.
   */
  authenticateWithAccessToken({ accessToken }: AccessTokenOptions): void {
    debug('now authenticating with access token %s', accessToken.substring(0, 8));
    this._token = accessToken;
  }

  /**
   * Creates a new ECDH keychain for the user.
   * @param {string} loginPassword - The user's login password.
   * @returns {Promise<any>} - A promise that resolves with the new ECDH keychain data.
   * @throws {Error} - Throws an error if there is an issue creating the keychain.
   */
  public async createUserEcdhKeychain(loginPassword: string): Promise<any> {
    const keyData = this.keychains().create();
    const hdNode = bitcoin.HDNode.fromBase58(keyData.xprv);

    /**
     * Add the new ECDH keychain to the user's account.
     * @type {Promise<any>} - A promise that resolves with the new ECDH keychain.
     */
    return await this.keychains().add({
      source: 'ecdh',
      xpub: hdNode.neutered().toBase58(),
      encryptedXprv: this.encrypt({
        password: loginPassword,
        input: hdNode.toBase58(),
      }),
    });
  }

  /**
   * Updates the user's settings with the provided parameters.
   * @param {Object} params - The parameters to update the user's settings with.
   * @returns {Promise<any>}
   * @throws {Error} - Throws an error if there is an issue updating the user's settings.
   */
  private async updateUserSettings(params: any): Promise<any> {
    return this.put(this.url('/user/settings', 2)).send(params).result();
  }

  /**
   * Ensures that the user's ECDH keychain is created for wallet sharing and TSS wallets.
   * If the keychain does not exist, it will be created and the user's settings will be updated.
   * @param {string} loginPassword - The user's login password.
   * @returns {Promise<any>} - A promise that resolves with the user's settings ensuring we have the ecdhKeychain in there.
   * @throws {Error} - Throws an error if there is an issue creating the keychain or updating the user's settings.
   */
  private async ensureUserEcdhKeychainIsCreated(loginPassword: string): Promise<any> {
    /**
     * Get the user's current settings.
     */
    const userSettings = await this.get(this.url('/user/settings')).result();
    /**
     * If the user's ECDH keychain does not exist, create a new keychain and update the user's settings.
     */
    if (!userSettings.settings.ecdhKeychain) {
      const newKeychain = await this.createUserEcdhKeychain(loginPassword);
      await this.updateUserSettings({
        settings: {
          ecdhKeychain: newKeychain.xpub,
        },
      });
      /**
       * Update the user's settings object with the new ECDH keychain.
       */
      userSettings.settings.ecdhKeychain = newKeychain.xpub;
    }
    /**
     * Return the user's ECDH keychain settings.
     */
    return userSettings.settings;
  }

  /**
   * Login to the bitgo platform.
   */
  async authenticate(params: AuthenticateOptions): Promise<LoginResponse | any> {
    try {
      if (!_.isObject(params)) {
        throw new Error('required object params');
      }

      if (!_.isString(params.password)) {
        throw new Error('expected string password');
      }

      const forceV1Auth = !!params.forceV1Auth;
      const authParams = this.preprocessAuthenticationParams(params);
      const password = params.password;

      if (this._token) {
        return new Error('already logged in');
      }

      const authUrl = this.microservicesUrl('/api/auth/v1/session');
      const request = this.post(authUrl);

      if (forceV1Auth) {
        request.forceV1Auth = true;
        // tell the server that the client was forced to downgrade the authentication protocol
        authParams.forceV1Auth = true;
        debug('forcing v1 auth for call to authenticate');
      }
      const response: superagent.Response = await request.send(authParams);
      // extract body and user information
      const body = response.body;
      this._user = body.user;

      if (body.access_token) {
        this._token = body.access_token;
        // if the downgrade was forced, adding a warning message might be prudent
      } else {
        // check the presence of an encrypted ECDH xprv
        // if not present, legacy account
        const encryptedXprv = body.encryptedECDHXprv;
        if (!encryptedXprv) {
          throw new Error('Keychain needs encryptedXprv property');
        }

        const responseDetails = this.handleTokenIssuance(response.body, password);
        this._token = responseDetails.token;
        this._ecdhXprv = responseDetails.ecdhXprv;

        // verify the response's authenticity
        verifyResponse(this, responseDetails.token, 'post', request, response, this._authVersion);

        // add the remaining component for easier access
        response.body.access_token = this._token;
      }

      const userSettings = params.ensureEcdhKeychain ? await this.ensureUserEcdhKeychainIsCreated(password) : undefined;
      if (userSettings?.ecdhKeychain) {
        response.body.user.ecdhKeychain = userSettings.ecdhKeychain;
      }

      return handleResponseResult<LoginResponse>()(response);
    } catch (e) {
      handleResponseError(e);
    }
  }

  /**
   * Login to the bitgo platform with passkey.
   */
  async authenticateWithPasskey(passkey: string): Promise<LoginResponse | any> {
    try {
      if (this._token) {
        return new Error('already logged in');
      }

      const authUrl = this.microservicesUrl('/api/auth/v1/session');
      const request = this.post(authUrl);

      this.validatePasskeyResponse(passkey);
      const userId = JSON.parse(passkey).response.userHandle;

      const response: superagent.Response = await request.send({
        passkey: passkey,
        userId: userId,
      });
      // extract body and user information
      const body = response.body;
      this._user = body.user;

      if (body.access_token) {
        this._token = body.access_token;
        response.body.access_token = body.access_token;
      } else {
        throw new Error('Failed to login. Please contact support@bitgo.com');
      }

      return handleResponseResult<LoginResponse>()(response);
    } catch (e) {
      handleResponseError(e);
    }
  }

  /**
   *
   * @param responseBody Response body object
   * @param password Password for the symmetric decryption
   */
  handleTokenIssuance(responseBody: TokenIssuanceResponse, password?: string): TokenIssuance {
    // make sure the response body contains the necessary properties
    common.validateParams(responseBody, ['derivationPath'], ['encryptedECDHXprv']);

    const environment = this._env;
    const environmentConfig = common.Environments[environment];
    const serverXpub = environmentConfig.serverXpub;
    let ecdhXprv = this._ecdhXprv;
    if (!ecdhXprv) {
      if (!password || !responseBody.encryptedECDHXprv) {
        throw new Error('ecdhXprv property must be set or password and encrypted encryptedECDHXprv must be provided');
      }
      try {
        ecdhXprv = this.decrypt({
          input: responseBody.encryptedECDHXprv,
          password: password,
        });
      } catch (e) {
        e.errorCode = 'ecdh_xprv_decryption_failure';
        console.error('Failed to decrypt encryptedECDHXprv.');
        throw e;
      }
    }

    // construct HDNode objects for client's xprv and server's xpub
    const clientHDNode = bip32.fromBase58(ecdhXprv);
    const serverHDNode = bip32.fromBase58(serverXpub);

    // BIP32 derivation path is applied to both client and server master keys
    const derivationPath = sanitizeLegacyPath(responseBody.derivationPath);
    const clientDerivedNode = clientHDNode.derivePath(derivationPath);
    const serverDerivedNode = serverHDNode.derivePath(derivationPath);

    const publicKey = serverDerivedNode.publicKey;
    const secretKey = clientDerivedNode.privateKey;
    if (!secretKey) {
      throw new Error('no client private Key');
    }
    const secret = Buffer.from(
      // FIXME(BG-34386): we should use `secp256k1.ecdh()` in the future
      //                  see discussion here https://github.com/bitcoin-core/secp256k1/issues/352
      secp256k1.publicKeyTweakMul(publicKey, secretKey)
    ).toString('hex');

    // decrypt token with symmetric ECDH key
    let response: TokenIssuance;
    try {
      response = {
        token: this.decrypt({
          input: responseBody.encryptedToken,
          password: secret,
        }),
      };
    } catch (e) {
      e.errorCode = 'token_decryption_failure';
      console.error('Failed to decrypt token.');
      throw e;
    }
    if (!this._ecdhXprv) {
      response.ecdhXprv = ecdhXprv;
    }
    return response;
  }

  /**
   */
  verifyPassword(params: VerifyPasswordOptions = {}): Promise<any> {
    if (!_.isString(params.password)) {
      throw new Error('missing required string password');
    }

    if (!this._user || !this._user.username) {
      throw new Error('no current user');
    }
    const hmacPassword = this.calculateHMAC(this._user.username, params.password);

    return this.post(this.url('/user/verifypassword')).send({ password: hmacPassword }).result('valid');
  }

  /**
   * Clear out all state from this BitGo object, effectively logging out the current user.
   */
  clear(): void {
    // TODO: are there any other fields which should be cleared?
    this._user = undefined;
    this._token = undefined;
    this._refreshToken = undefined;
    this._ecdhXprv = undefined;
  }

  /**
   * Use refresh token to get new access token.
   * If the refresh token is null/defined, then we use the stored token from auth
   */
  async refreshToken(params: { refreshToken?: string } = {}): Promise<any> {
    common.validateParams(params, [], ['refreshToken']);

    const refreshToken = params.refreshToken || this._refreshToken;

    if (!refreshToken) {
      throw new Error('Must provide refresh token or have authenticated with Oauth before');
    }

    if (!this._clientId || !this._clientSecret) {
      throw new Error('Need client id and secret set first to use this');
    }

    const body = await this.post(this._baseUrl + '/oauth/token')
      .send({
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: this._clientId,
        client_secret: this._clientSecret,
      })
      .result();
    this._token = body.access_token;
    this._refreshToken = body.refresh_token;
    return body;
  }

  /**
   *
   * listAccessTokens
   * Get information on all of the BitGo access tokens on the user
   * @return {
   *  id: <id of the token>
   *  label: <the user-provided label for this token>
   *  user: <id of the user on the token>
   *  enterprise <id of the enterprise this token is valid for>
   *  client: <the auth client that this token belongs to>
   *  scope: <list of allowed OAuth scope values>
   *  created: <date the token was created>
   *  expires: <date the token will expire>
   *  origin: <the origin for which this token is valid>
   *  isExtensible: <flag indicating if the token can be extended>
   *  extensionAddress: <address whose private key's signature is necessary for extensions>
   *  unlock: <info for actions that require an unlock before firing>
   * }
   */
  async listAccessTokens(): Promise<any> {
    return this.get(this.url('/user/accesstoken')).send().result('accessTokens');
  }

  /**
   * addAccessToken
   * Add a BitGo API Access Token to the current user account
   * @param params {
   *    otp: (required) <valid otp code>
   *    label: (required) <label for the token>
   *    duration: <length of time in seconds the token will be valid for>
   *    ipRestrict: <array of IP address strings to whitelist>
   *    txValueLimit: <number of outgoing satoshis allowed on this token>
   *    scope: (required) <authorization scope of the requested token>
   * }
   * @return {
   *    id: <id of the token>
   *    token: <access token hex string to be used for BitGo API request verification>
   *    label: <user-provided label for this token>
   *    user: <id of the user on the token>
   *    enterprise <id of the enterprise this token is valid for>
   *    client: <the auth client that this token belongs to>
   *    scope: <list of allowed OAuth scope values>
   *    created: <date the token was created>
   *    expires: <date the token will expire>
   *    origin: <the origin for which this token is valid>
   *    isExtensible: <flag indicating if the token can be extended>
   *    extensionAddress: <address whose private key's signature is necessary for extensions>
   *    unlock: <info for actions that require an unlock before firing>
   * }
   */
  async addAccessToken(params: AddAccessTokenOptions): Promise<AddAccessTokenResponse> {
    try {
      if (!_.isString(params.label)) {
        throw new Error('required string label');
      }

      // check non-string params
      if (params.duration) {
        if (!_.isNumber(params.duration) || params.duration < 0) {
          throw new Error('duration must be a non-negative number');
        }
      }
      if (params.ipRestrict) {
        if (!_.isArray(params.ipRestrict)) {
          throw new Error('ipRestrict must be an array');
        }
        _.forEach(params.ipRestrict, (ipAddr) => {
          if (!_.isString(ipAddr)) {
            throw new Error('ipRestrict must be an array of IP address strings');
          }
        });
      }
      if (params.txValueLimit) {
        if (!_.isNumber(params.txValueLimit)) {
          throw new Error('txValueLimit must be a number');
        }
        if (params.txValueLimit < 0) {
          throw new Error('txValueLimit must be a non-negative number');
        }
      }
      if (params.scope && params.scope.length > 0) {
        if (!_.isArray(params.scope)) {
          throw new Error('scope must be an array');
        }
      } else {
        throw new Error('must specify scope for token');
      }

      const authUrl = this.microservicesUrl('/api/auth/v1/accesstoken');
      const request = this.post(authUrl);

      if (!this._ecdhXprv) {
        // without a private key, the user cannot decrypt the new access token the server will send
        request.forceV1Auth = true;
        debug('forcing v1 auth for adding access token using token %s', this._token?.substr(0, 8));
      }

      const response = await request.send(params);
      if (request.forceV1Auth) {
        (response as any).body.warning = 'A protocol downgrade has occurred because this is a legacy account.';
        return handleResponseResult<AddAccessTokenResponse>()(response);
      }

      // verify the authenticity of the server's response before proceeding any further
      verifyResponse(this, this._token, 'post', request, response, this._authVersion);

      const responseDetails = this.handleTokenIssuance(response.body);
      response.body.token = responseDetails.token;

      return handleResponseResult<AddAccessTokenResponse>()(response);
    } catch (e) {
      handleResponseError(e);
    }
  }

  /**
   * Sets the expire time of an access token matching either the id or label to the current date, effectively deleting it
   *
   * Params:
   * id: <id of the access token to be deleted>
   * label: <label of the access token to be deleted>
   *
   * Returns:
   * id: <id of the token>
   * label: <user-provided label for this token>
   * user: <id of the user on the token>
   * enterprise <id of the enterprise this token is valid for>
   * client: <the auth client that this token belongs to>
   * scope: <list of allowed OAuth scope values>
   * created: <date the token was created>
   * expires: <date the token will expire>
   * origin: <the origin for which this token is valid>
   * isExtensible: <flag indicating if the token can be extended>
   * extensionAddress: <address whose private key's signature is ne*cessary for extensions>
   * unlock: <info for actions that require an unlock before firing>
   * @param params
   */
  async removeAccessToken({ id, label }: RemoveAccessTokenOptions): Promise<any> {
    if ((!id && !label) || (id && label)) {
      throw new Error('must provide exactly one of id or label');
    }
    if (id) {
      return this.del(this.url(`/user/accesstoken/${id}`))
        .send()
        .result();
    }

    const tokens = await this.listAccessTokens();

    if (!tokens) {
      throw new Error('token with this label does not exist');
    }

    const matchingTokens = _.filter(tokens, { label });
    if (matchingTokens.length > 1) {
      throw new Error('ambiguous call: multiple tokens matching this label');
    }
    if (matchingTokens.length === 0) {
      throw new Error('token with this label does not exist');
    }

    return this.del(this.url(`/user/accesstoken/${matchingTokens[0].id}`))
      .send()
      .result();
  }

  /**
   * Generate a random password
   * @param   {Number} numWords     Number of 32-bit words
   * @returns {String}          base58 random password
   */
  generateRandomPassword(numWords = 5): string {
    return generateRandomPassword(numWords);
  }

  /**
   * Logout of BitGo
   */
  async logout(): Promise<any> {
    const result = await this.get(this.url('/user/logout')).result();
    this.clear();
    return result;
  }

  /**
   * Get a user by ID (name/email only)
   * @param id
   *
   * @deprecated
   */
  async getUser({ id }: GetUserOptions): Promise<any> {
    if (!_.isString(id)) {
      throw new Error('expected string id');
    }
    return this.get(this.url(`/user/${id}`)).result('user');
  }
  /**
   * Get the current logged in user
   */
  async me(): Promise<any> {
    return this.getUser({ id: 'me' });
  }

  /**
   * Unlock the session by providing OTP
   * @param {string} otp Required OTP code for the account.
   * @param {number} duration Desired duration of the unlock in seconds (default=600, max=3600).
   */
  async unlock({ otp, duration }: UnlockOptions): Promise<any> {
    if (otp && !_.isString(otp)) {
      throw new Error('expected string or undefined otp');
    }
    return this.post(this.url('/user/unlock')).send({ otp, duration }).result();
  }

  /**
   * Lock the session
   */
  async lock(): Promise<any> {
    return this.post(this.url('/user/lock')).result();
  }

  /**
   * Get the current session
   */
  async session(): Promise<any> {
    return this.get(this.url('/user/session')).result('session');
  }

  /**
   * Trigger a push/sms for the OTP code
   * @param {boolean} params.forceSMS If set to true, will use SMS to send the OTP to the user even if they have other 2FA method set up.
   * @deprecated
   */
  async sendOTP(params: { forceSMS?: boolean } = {}): Promise<any> {
    return this.post(this.url('/user/sendotp')).send(params).result();
  }

  /**
   * Extend token, provided the current token is extendable
   * @param params
   * - duration: duration in seconds by which to extend the token, starting at the current time
   */
  async extendToken(params: ExtendTokenOptions = {}): Promise<any> {
    if (!this._extensionKey) {
      throw new Error('missing required property _extensionKey');
    }

    const timestamp = Date.now();
    const duration = params.duration;
    const message = timestamp + '|' + this._token + '|' + duration;
    const privateKey = this._extensionKey.privateKey;
    if (!privateKey) {
      throw new Error('no privateKey on extensionKey');
    }
    const isCompressed = this._extensionKey.compressed;
    const prefix = utxolib.networks.bitcoin.messagePrefix;
    const signature = bitcoinMessage.sign(message, privateKey, isCompressed, prefix).toString('hex');

    return this.post(this.url('/user/extendtoken'))
      .send(params)
      .set('timestamp', timestamp.toString())
      .set('signature', signature)
      .result();
  }

  /**
   * Get a key for sharing a wallet with a user
   * @param email email of user to share wallet with
   */
  async getSharingKey({ email }: GetSharingKeyOptions): Promise<any> {
    if (!_.isString(email)) {
      throw new Error('required string email');
    }

    return this.post(this.url('/user/sharingkey')).send({ email }).result();
  }

  /**
   * Users that want to sign with a key will use this api to fetch the keychain and the path.
   * Users that want to verify a signature will use this api to fetch another users ecdh pubkey.
   * Note: If the user id is not provided, it will default to getting the current user's keychain.
   * @param bitgo
   * @param enterpriseId
   * @param userId
   */
  async getSigningKeyForUser(enterpriseId: string, userId?: string): Promise<GetSigningKeyApi> {
    const user = userId ?? 'me';
    return this.get(this.url(`/enterprise/${enterpriseId}/user/${user}/signingkey`, 2))
      .query({})
      .result();
  }

  /**
   *
   */
  getValidate(): boolean {
    return this._validate;
  }

  /**
   *
   */
  setValidate(validate: boolean): void {
    if (!_.isBoolean(validate)) {
      throw new Error('invalid argument');
    }
    this._validate = validate;
  }

  /**
   * Register a new coin instance with its builder factory
   * @param {string} name coin name as it was registered in @bitgo/statics
   * @param {CoinConstructor} coin the builder factory class for that coin
   * @returns {void}
   */
  public register(name: string, coin: CoinConstructor): void {
    GlobalCoinFactory.register(name, coin);
  }

  /**
   * Get bitcoin market data
   *
   * @deprecated
   */
  markets(): any {
    if (!this._markets) {
      this._markets = new Markets(this);
    }
    return this._markets;
  }

  /**
   * Get the latest bitcoin prices
   * (Deprecated: Will be removed in the future) use `bitgo.markets().latest()`
   * @deprecated
   */
  // cb-compat
  async market(): Promise<any> {
    return this.get(this.url('/market/latest')).result();
  }

  /**
   * Get market data from yesterday
   * (Deprecated: Will be removed in the future) use bitgo.markets().yesterday()
   * @deprecated
   */
  async yesterday(): Promise<any> {
    return this.get(this.url('/market/yesterday')).result();
  }

  /**
   * Get the blockchain object.
   * @deprecated
   */
  blockchain(): any {
    if (!this._blockchain) {
      this._blockchain = new Blockchain(this);
    }
    return this._blockchain;
  }

  /**
   * Get the user's keychains object.
   * @deprecated
   */
  keychains(): any {
    if (!this._keychains) {
      this._keychains = new Keychains(this);
    }
    return this._keychains;
  }

  /**
   * Get the travel rule object
   * @deprecated
   */
  travelRule(): any {
    if (!this._travelRule) {
      this._travelRule = new TravelRule(this);
    }
    return this._travelRule;
  }

  /**
   * Get the user's wallets object.
   * @deprecated
   */
  wallets(): any {
    if (!this._wallets) {
      this._wallets = new Wallets(this);
    }
    return this._wallets;
  }

  /**
   * Get pending approvals that can be approved/ or rejected
   * @deprecated
   */
  pendingApprovals(): any {
    if (!this._pendingApprovals) {
      this._pendingApprovals = new PendingApprovals(this);
    }
    return this._pendingApprovals;
  }

  /**
   * A factory method to create a new Wallet object, initialized with the wallet params
   * Can be used to reconstitute a wallet from cached data
   * @param walletParams
   * @deprecated
   */
  newWalletObject(walletParams): any {
    return new Wallet(this, walletParams);
  }

  /**
   * V1 method for calculating miner fee amounts, given the number and
   * type of transaction inputs, along with a fee rate in satoshis per vkB.
   *
   * This method should not be used for new code.
   *
   * @deprecated
   * @param params
   * @return {any}
   */
  async calculateMinerFeeInfo(params: any): Promise<any> {
    return TransactionBuilder.calculateMinerFeeInfo(params);
  }

  /**
   * Verify a Bitcoin address is a valid base58 address
   * @deprecated
   */
  verifyAddress(params: DeprecatedVerifyAddressOptions = {}): boolean {
    common.validateParams(params, ['address'], []);

    if (!_.isString(params.address)) {
      throw new Error('missing required string address');
    }

    const networkName = common.Environments[this.getEnv()].network;
    const network = utxolib.networks[networkName];

    return verifyAddress(params.address, network);
  }

  /**
   * Split a secret into shards using Shamir Secret Sharing.
   * @param seed A hexadecimal secret to split
   * @param passwords An array of the passwords used to encrypt each share
   * @param m The threshold number of shards necessary to reconstitute the secret
   */
  splitSecret({ seed, passwords, m }: SplitSecretOptions): SplitSecret {
    if (!Array.isArray(passwords)) {
      throw new Error('passwords must be an array');
    }
    if (!_.isInteger(m) || m < 2) {
      throw new Error('m must be a positive integer greater than or equal to 2');
    }

    if (passwords.length < m) {
      throw new Error('passwords array length cannot be less than m');
    }

    const n = passwords.length;
    const secrets: string[] = shamir.share(seed, n, m);
    const shards = _.zipWith(secrets, passwords, (shard, password) => {
      return this.encrypt({ input: shard, password });
    });
    const node = bip32.fromSeed(Buffer.from(seed, 'hex'));
    return {
      xpub: node.neutered().toBase58(),
      m,
      n,
      seedShares: shards,
    };
  }

  /**
   * Reconstitute a secret which was sharded with `splitSecret`.
   * @param shards
   * @param passwords
   */
  reconstituteSecret({ shards, passwords }: ReconstituteSecretOptions): ReconstitutedSecret {
    if (!Array.isArray(shards)) {
      throw new Error('shards must be an array');
    }
    if (!Array.isArray(passwords)) {
      throw new Error('passwords must be an array');
    }

    if (shards.length !== passwords.length) {
      throw new Error('shards and passwords arrays must have same length');
    }

    const secrets = _.zipWith(shards, passwords, (shard, password) => {
      return this.decrypt({ input: shard, password });
    });
    const seed: string = shamir.combine(secrets);
    const node = bip32.fromSeed(Buffer.from(seed, 'hex'));
    return {
      xpub: node.neutered().toBase58() as string,
      xprv: node.toBase58() as string,
      seed,
    };
  }

  /**
   *
   * @param shards
   * @param passwords
   * @param m
   * @param xpub Optional xpub to verify the results against
   */
  verifyShards({ shards, passwords, m, xpub }: VerifyShardsOptions): boolean {
    /**
     * Generate all possible combinations of a given array's values given subset size m
     * @param array The array whose values are to be arranged in all combinations
     * @param m The size of each subset
     * @param entryIndices Recursively trailing set of currently chosen array indices for the combination subset under construction
     * @returns {Array}
     */
    const generateCombinations = (array: string[], m: number, entryIndices: number[] = []): string[][] => {
      let combinations: string[][] = [];

      if (entryIndices.length === m) {
        const currentCombination = _.at(array, entryIndices);
        return [currentCombination];
      }

      // The highest index
      let entryIndex = _.last(entryIndices);
      // If there are currently no indices, assume -1
      if (_.isUndefined(entryIndex)) {
        entryIndex = -1;
      }
      for (let i = entryIndex + 1; i < array.length; i++) {
        // append the current index to the trailing indices
        const currentEntryIndices = [...entryIndices, i];
        const newCombinations = generateCombinations(array, m, currentEntryIndices);
        combinations = [...combinations, ...newCombinations];
      }

      return combinations;
    };

    if (!Array.isArray(shards)) {
      throw new Error('shards must be an array');
    }
    if (!Array.isArray(passwords)) {
      throw new Error('passwords must be an array');
    }

    if (shards.length !== passwords.length) {
      throw new Error('shards and passwords arrays must have same length');
    }

    const secrets = _.zipWith(shards, passwords, (shard, password) => {
      return this.decrypt({ input: shard, password });
    });
    const secretCombinations = generateCombinations(secrets, m);
    const seeds = secretCombinations.map((currentCombination) => {
      return shamir.combine(currentCombination);
    });
    const uniqueSeeds = _.uniq(seeds);
    if (uniqueSeeds.length !== 1) {
      return false;
    }
    const seed = _.first(uniqueSeeds);
    const node = bip32.fromSeed(Buffer.from(seed, 'hex'));
    const restoredXpub = node.neutered().toBase58();

    if (!_.isUndefined(xpub)) {
      if (!_.isString(xpub)) {
        throw new Error('xpub must be a string');
      }
      if (restoredXpub !== xpub) {
        return false;
      }
    }

    return true;
  }

  /**
   * @deprecated - use `getSharedSecret()`
   */
  getECDHSecret({ otherPubKeyHex, eckey }: GetEcdhSecretOptions): string {
    if (!_.isString(otherPubKeyHex)) {
      throw new Error('otherPubKeyHex string required');
    }
    if (!_.isObject(eckey)) {
      throw new Error('eckey object required');
    }

    return getSharedSecret(eckey, Buffer.from(otherPubKeyHex, 'hex')).toString('hex');
  }

  /**
   * Gets the user's private ECDH keychain
   */
  async getECDHKeychain(ecdhKeychainPub?: string): Promise<any> {
    if (!ecdhKeychainPub) {
      const result = await this.get(this.url('/user/settings')).result();
      if (!result.settings.ecdhKeychain) {
        return new Error('ecdh keychain not found for user');
      }
      ecdhKeychainPub = result.settings.ecdhKeychain;
    }
    return this.keychains().get({ xpub: ecdhKeychainPub });
  }

  /**
   * Returns the user derived public and private ECDH keypair
   * @param password password to decrypt the user's ECDH encrypted private key
   * @param entId? optional enterprise id to check for permissions
   */
  async getEcdhKeypairPrivate(password: string, entId: string): Promise<EcdhDerivedKeypair> {
    const userSigningKey = await this.getSigningKeyForUser(entId);
    const pubkeyOfAdminEcdhKeyHex = userSigningKey.derivedPubkey;
    if (!userSigningKey.ecdhKeychain || !userSigningKey.derivationPath) {
      throw new Error('Something went wrong with the user keychain. Please contact support@bitgo.com.');
    }
    const userEcdhKeychain = await this.getECDHKeychain(userSigningKey.ecdhKeychain);
    let xprv;
    try {
      xprv = this.decrypt({
        password: password,
        input: userEcdhKeychain.encryptedXprv,
      });
    } catch (e) {
      throw new Error('Incorrect password. Please try again.');
    }
    return {
      derivedPubKey: pubkeyOfAdminEcdhKeyHex,
      derivationPath: userSigningKey.derivationPath,
      xprv,
    };
  }

  /**
   * @param params
   * - operatingSystem: one of ios, android
   * - pushToken: hex-formatted token for the respective native push notification service
   * @returns {*}
   * @deprecated
   */
  async registerPushToken(params: RegisterPushTokenOptions): Promise<any> {
    params = params || {};
    common.validateParams(params, ['pushToken', 'operatingSystem'], []);

    if (!this._token) {
      // this device has to be registered to an extensible session
      throw new Error('not logged in');
    }

    const postParams = _.pick(params, ['pushToken', 'operatingSystem']);

    return this.post(this.url('/devices')).send(postParams).result();
  }

  /**
   * @param params
   * - pushVerificationToken: the token received via push notification to confirm the device's mobility
   * @deprecated
   */
  verifyPushToken(params: VerifyPushTokenOptions): Promise<any> {
    if (!_.isObject(params)) {
      throw new Error('required object params');
    }

    if (!_.isString(params.pushVerificationToken)) {
      throw new Error('required string pushVerificationToken');
    }

    if (!this._token) {
      // this device has to be registered to an extensible session
      throw new Error('not logged in');
    }

    const postParams = _.pick(params, 'pushVerificationToken');

    return this.post(this.url('/devices/verify')).send(postParams).result();
  }

  /**
   * Login to the bitgo system using an authcode generated via Oauth
   */
  async authenticateWithAuthCode(params: AuthenticateWithAuthCodeOptions): Promise<any> {
    if (!_.isObject(params)) {
      throw new Error('required object params');
    }

    if (!_.isString(params.authCode)) {
      throw new Error('required string authCode');
    }

    if (!this._clientId || !this._clientSecret) {
      throw new Error('Need client id and secret set first to use this');
    }

    const authCode = params.authCode;

    if (this._token) {
      throw new Error('already logged in');
    }

    const request = this.post(this._baseUrl + '/oauth/token');
    request.forceV1Auth = true; // OAuth currently only supports v1 authentication
    const body = await request
      .send({
        grant_type: 'authorization_code',
        code: authCode,
        client_id: this._clientId,
        client_secret: this._clientSecret,
      })
      .result();

    this._token = body.access_token;
    this._refreshToken = body.refresh_token;
    this._user = await this.me();
    return body;
  }

  /**
   * Change the password of the currently logged in user.
   * Also change all v1 and v2 keychain passwords if they match the
   * given oldPassword. Returns nothing on success.
   * @param oldPassword {String} - the current password
   * @param newPassword {String} - the new password
   */
  async changePassword({ oldPassword, newPassword }: ChangePasswordOptions): Promise<any> {
    if (!_.isString(oldPassword)) {
      throw new Error('expected string oldPassword');
    }

    if (!_.isString(newPassword)) {
      throw new Error('expected string newPassword');
    }

    const user = this.user();
    if (typeof user !== 'object' || !user.username) {
      throw new Error('missing required object user');
    }

    const validation = await this.verifyPassword({ password: oldPassword });
    if (!validation) {
      throw new Error('the provided oldPassword is incorrect');
    }

    // it doesn't matter which coin we choose because the v2 updatePassword functions updates all v2 keychains
    // we just need to choose a coin that exists in the current environment
    const coin = common.Environments[this.getEnv()].network === 'bitcoin' ? 'btc' : 'tbtc';

    const updateKeychainPasswordParams = { oldPassword, newPassword };
    const v1KeychainUpdatePWResult = await this.keychains().updatePassword(updateKeychainPasswordParams);
    const v2Keychains = await this.coin(coin).keychains().updatePassword(updateKeychainPasswordParams);

    const updatePasswordParams = {
      keychains: v1KeychainUpdatePWResult.keychains,
      v2_keychains: v2Keychains,
      version: v1KeychainUpdatePWResult.version,
      oldPassword: this.calculateHMAC(user.username, oldPassword),
      password: this.calculateHMAC(user.username, newPassword),
    };

    return this.post(this.url('/user/changepassword')).send(updatePasswordParams).result();
  }

  /**
   * Get all the address labels on all of the user's wallets
   *
   * @deprecated
   */
  async labels(): Promise<any> {
    return this.get(this.url('/labels')).result('labels');
  }

  /**
   * Estimates approximate fee per kb needed for a tx to get into a block
   * @param {number} params.numBlocks target blocks for the transaction to be confirmed
   * @param {number} params.maxFee maximum fee willing to be paid (for safety)
   * @param {array[string]} params.inputs list of unconfirmed txIds from which this transaction uses inputs
   * @param {number} params.txSize estimated transaction size in bytes, optional parameter used for CPFP estimation.
   * @param {boolean} params.cpfpAware flag indicating fee should take into account CPFP
   * @deprecated
   */
  async estimateFee(params: EstimateFeeOptions = {}): Promise<any> {
    const queryParams: any = { version: 12 };
    if (params.numBlocks) {
      if (!_.isNumber(params.numBlocks)) {
        throw new Error('invalid argument');
      }
      queryParams.numBlocks = params.numBlocks;
    }
    if (params.maxFee) {
      if (!_.isNumber(params.maxFee)) {
        throw new Error('invalid argument');
      }
      queryParams.maxFee = params.maxFee;
    }
    if (params.inputs) {
      if (!Array.isArray(params.inputs)) {
        throw new Error('invalid argument');
      }
      queryParams.inputs = params.inputs;
    }
    if (params.txSize) {
      if (!_.isNumber(params.txSize)) {
        throw new Error('invalid argument');
      }
      queryParams.txSize = params.txSize;
    }
    if (params.cpfpAware) {
      if (!_.isBoolean(params.cpfpAware)) {
        throw new Error('invalid argument');
      }
      queryParams.cpfpAware = params.cpfpAware;
    }

    return this.get(this.url('/tx/fee')).query(queryParams).result();
  }

  /**
   * Get BitGo's guarantee using an instant id
   * @param params
   * @deprecated
   */
  async instantGuarantee(params: { id: string }): Promise<any> {
    if (!_.isString(params.id)) {
      throw new Error('required string id');
    }

    const body = await this.get(this.url('/instant/' + params.id)).result();
    if (!body.guarantee) {
      throw new Error('no guarantee found in response body');
    }
    if (!body.signature) {
      throw new Error('no signature found in guarantee response body');
    }
    const signingAddress = common.Environments[this.getEnv()].signingAddress;
    const signatureBuffer = Buffer.from(body.signature, 'hex');
    const prefix = utxolib.networks[common.Environments[this.getEnv()].network].messagePrefix;
    const isValidSignature = bitcoinMessage.verify(body.guarantee, signingAddress, signatureBuffer, prefix);
    if (!isValidSignature) {
      throw new Error('incorrect signature');
    }
    return body;
  }

  /**
   * Get a target address for payment of a BitGo fee
   * @deprecated
   */
  async getBitGoFeeAddress(): Promise<any> {
    return this.post(this.url('/billing/address')).send({}).result();
  }

  /**
   * Gets an address object (including the wallet id) for a given address.
   * @param {string} params.address The address to look up.
   * @deprecated
   */
  async getWalletAddress({ address }: { address: string }): Promise<any> {
    return this.get(this.url(`/walletaddress/${address}`)).result();
  }

  /**
   * Fetch list of user webhooks
   *
   * @returns {*}
   * @deprecated
   */
  async listWebhooks(): Promise<any> {
    return this.get(this.url('/webhooks')).result();
  }

  /**
   * Add new user webhook
   *
   * @param params
   * @returns {*}
   * @deprecated
   */
  async addWebhook(params: WebhookOptions): Promise<any> {
    if (!_.isString(params.url)) {
      throw new Error('required string url');
    }

    if (!_.isString(params.type)) {
      throw new Error('required string type');
    }

    return this.post(this.url('/webhooks')).send(params).result();
  }

  /**
   * Remove user webhook
   *
   * @param params
   * @returns {*}
   * @deprecated
   */
  async removeWebhook(params: WebhookOptions): Promise<any> {
    if (!_.isString(params.url)) {
      throw new Error('required string url');
    }

    if (!_.isString(params.type)) {
      throw new Error('required string type');
    }

    return this.del(this.url('/webhooks')).send(params).result();
  }

  /**
   * Fetch list of webhook notifications for the user
   *
   * @param params
   * @returns {*}
   */
  async listWebhookNotifications(params: ListWebhookNotificationsOptions = {}): Promise<any> {
    const query: any = {};
    if (params.prevId) {
      if (!_.isString(params.prevId)) {
        throw new Error('invalid prevId argument, expecting string');
      }
      query.prevId = params.prevId;
    }
    if (params.limit) {
      if (!_.isNumber(params.limit)) {
        throw new Error('invalid limit argument, expecting number');
      }
      query.limit = params.limit;
    }

    return this.get(this.url('/webhooks/notifications')).query(query).result();
  }

  /**
   * Simulate a user webhook
   *
   * @param params
   * @returns {*}
   */
  async simulateWebhook(params: BitGoSimulateWebhookOptions): Promise<any> {
    common.validateParams(params, ['webhookId', 'blockId'], []);
    if (!_.isString(params.webhookId)) {
      throw new Error('required string webhookId');
    }

    if (!_.isString(params.blockId)) {
      throw new Error('required string blockId');
    }

    return this.post(this.url(`/webhooks/${params.webhookId}/simulate`))
      .send(params)
      .result();
  }

  /**
   * Synchronously get constants which are relevant to the client.
   *
   * Note: This function has a known race condition. It may return different values over time,
   * especially if called shortly after creation of the BitGo object.
   *
   * New code should call fetchConstants() directly instead.
   *
   * @deprecated
   * @return {Object} The client constants object
   */
  getConstants(): any {
    // kick off a fresh request for the client constants
    this.fetchConstants().catch(function (err) {
      if (err) {
        // make sure an error does not terminate the entire script
        console.error('failed to fetch client constants from BitGo');
        console.trace(err);
      }
    });

    // use defaultConstants as the backup for keys that are not set in this._constants
    return _.merge({}, defaultConstants(this.getEnv()), BitGoAPI._constants[this.getEnv()]);
  }
}

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


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