PHP WebShell

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

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

/**
 * @prettier
 */

import BigNumber from 'bignumber.js';
import * as _ from 'lodash';
import * as base58 from 'bs58';
import { Networks, BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';

import {
  BaseCoin,
  BitGoBase,
  BaseTransaction,
  KeyPair,
  MethodNotImplementedError,
  ParsedTransaction,
  ParseTransactionOptions as BaseParseTransactionOptions,
  SignedTransaction,
  SignTransactionOptions as BaseSignTransactionOptions,
  TransactionExplanation,
  VerifyAddressOptions,
  VerifyTransactionOptions,
  Eddsa,
  PublicKey,
  Environments,
  MPCAlgorithm,
  EDDSAMethods,
  EDDSAMethodTypes,
  MPCTx,
  MPCUnsignedTx,
  RecoveryTxRequest,
  MPCSweepTxs,
  MPCSweepRecoveryOptions,
  MPCTxs,
  MultisigType,
  multisigTypes,
} from '@bitgo/sdk-core';
import * as nearAPI from 'near-api-js';
import * as request from 'superagent';

import { KeyPair as NearKeyPair, Transaction, TransactionBuilderFactory } from './lib';
import nearUtils from './lib/utils';

export interface SignTransactionOptions extends BaseSignTransactionOptions {
  txPrebuild: TransactionPrebuild;
  prv: string;
}

export interface TransactionPrebuild {
  txHex: string;
  key: string;
  blockHash: string;
  nonce: number;
}

export interface ExplainTransactionOptions {
  txPrebuild: TransactionPrebuild;
  publicKey: string;
  feeInfo: {
    fee: string;
  };
}

export interface VerifiedTransactionParameters {
  txHex: string;
  prv: string;
  signer: string;
}

export interface NearParseTransactionOptions extends BaseParseTransactionOptions {
  txPrebuild: TransactionPrebuild;
  publicKey: string;
  feeInfo: {
    fee: string;
  };
}

interface TransactionOutput {
  address: string;
  amount: string;
}

interface RecoveryOptions {
  userKey: string; // Box A
  backupKey: string; // Box B
  bitgoKey: string; // Box C
  recoveryDestination: string;
  krsProvider?: string;
  walletPassphrase: string;
  startingScanIndex?: number;
  scan?: number;
}

interface NearTxBuilderParamsFromNode {
  nonce: number;
  blockHash: string;
}

interface NearFeeConfig {
  sendSir: number;
  sendNotSir: number;
  execution: number;
}

interface ProtocolConfigOutput {
  storageAmountPerByte: number;
  transferCost: NearFeeConfig;
  receiptConfig: NearFeeConfig;
}

type TransactionInput = TransactionOutput;

export interface NearParsedTransaction extends ParsedTransaction {
  // total assets being moved, including fees
  inputs: TransactionInput[];

  // where assets are moved to
  outputs: TransactionOutput[];
}

export type NearTransactionExplanation = TransactionExplanation;

export class Near extends BaseCoin {
  protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;

  constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
    super(bitgo);
    if (!staticsCoin) {
      throw new Error('missing required constructor parameter staticsCoin');
    }

    this._staticsCoin = staticsCoin;
  }

  protected static initialized = false;
  protected static MPC: Eddsa;
  protected network = this.bitgo.getEnv() === 'prod' ? 'main' : 'test';

  static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
    return new Near(bitgo, staticsCoin);
  }

  allowsAccountConsolidations(): boolean {
    return true;
  }

  /**
   * Flag indicating if this coin supports TSS wallets.
   * @returns {boolean} True if TSS Wallets can be created for this coin
   */
  supportsTss(): boolean {
    return true;
  }

  /** inherited doc */
  getDefaultMultisigType(): MultisigType {
    return multisigTypes.tss;
  }

  getMPCAlgorithm(): MPCAlgorithm {
    return 'eddsa';
  }

  getChain(): string {
    return this._staticsCoin.name;
  }

  getBaseChain(): string {
    return this.getChain();
  }

  getFamily(): CoinFamily {
    return this._staticsCoin.family;
  }

  getFullName(): string {
    return this._staticsCoin.fullName;
  }

  getBaseFactor(): any {
    return Math.pow(10, this._staticsCoin.decimalPlaces);
  }

  /**
   * Flag for sending value of 0
   * @returns {boolean} True if okay to send 0 value, false otherwise
   */
  valuelessTransferAllowed(): boolean {
    return false;
  }

  /**
   * Generate ed25519 key pair
   *
   * @param seed
   * @returns {Object} object with generated pub, prv
   */
  generateKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new NearKeyPair({ seed }) : new NearKeyPair();
    const keys = keyPair.getKeys();
    if (!keys.prv) {
      throw new Error('Missing prv in key generation.');
    }
    return {
      pub: keys.pub,
      prv: keys.prv,
    };
  }

  /**
   * Return boolean indicating whether input is valid public key for the coin.
   *
   * @param {String} pub the pub to be checked
   * @returns {Boolean} is it valid?
   */
  isValidPub(pub: string): boolean {
    return nearUtils.isValidPublicKey(pub);
  }

  /**
   * Return boolean indicating whether the supplied private key is a valid near private key
   *
   * @param {String} prv the prv to be checked
   * @returns {Boolean} is it valid?
   */
  isValidPrv(prv: string): boolean {
    return nearUtils.isValidPrivateKey(prv);
  }

  /**
   * Return boolean indicating whether input is valid public key for the coin
   *
   * @param {String} address the pub to be checked
   * @returns {Boolean} is it valid?
   */
  isValidAddress(address: string): boolean {
    return nearUtils.isValidAddress(address);
  }

  /** @inheritDoc */
  async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {
    const nearKeypair = new NearKeyPair({ prv: key.prv });
    if (Buffer.isBuffer(message)) {
      message = base58.encode(message);
    }

    return Buffer.from(nearKeypair.signMessage(message));
  }

  /**
   * Explain/parse transaction
   * @param params
   */
  async explainTransaction(params: ExplainTransactionOptions): Promise<NearTransactionExplanation> {
    const factory = this.getBuilder();
    let rebuiltTransaction: BaseTransaction;
    const txRaw = params.txPrebuild.txHex;

    try {
      const transactionBuilder = factory.from(txRaw);
      rebuiltTransaction = await transactionBuilder.build();
    } catch {
      throw new Error('Invalid transaction');
    }

    return rebuiltTransaction.explainTransaction();
  }

  verifySignTransactionParams(params: SignTransactionOptions): VerifiedTransactionParameters {
    const prv = params.prv;

    const txHex = params.txPrebuild.txHex;

    if (_.isUndefined(txHex)) {
      throw new Error('missing txPrebuild parameter');
    }

    if (!_.isString(txHex)) {
      throw new Error(`txPrebuild must be an object, got type ${typeof txHex}`);
    }

    if (_.isUndefined(prv)) {
      throw new Error('missing prv parameter to sign transaction');
    }

    if (!_.isString(prv)) {
      throw new Error(`prv must be a string, got type ${typeof prv}`);
    }

    if (!_.has(params.txPrebuild, 'key')) {
      throw new Error('missing public key parameter to sign transaction');
    }

    // if we are receiving addresses do not try to convert them
    const signer = !nearUtils.isValidAddress(params.txPrebuild.key)
      ? new NearKeyPair({ pub: params.txPrebuild.key }).getAddress()
      : params.txPrebuild.key;
    return { txHex, prv, signer };
  }

  /**
   * Assemble keychain and half-sign prebuilt transaction
   *
   * @param params
   * @param params.txPrebuild {TransactionPrebuild} prebuild object returned by platform
   * @param params.prv {String} user prv
   * @param callback
   * @returns {Bluebird<SignedTransaction>}
   */
  async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
    const factory = this.getBuilder();
    const txBuilder = factory.from(params.txPrebuild.txHex);
    txBuilder.sign({ key: params.prv });
    const transaction: BaseTransaction = await txBuilder.build();

    if (!transaction) {
      throw new Error('Invalid transaction');
    }

    const serializedTx = (transaction as BaseTransaction).toBroadcastFormat();

    return {
      txHex: serializedTx,
    } as any;
  }

  /**
   * Builds a funds recovery transaction without BitGo
   * @param params
   */
  async recover(params: RecoveryOptions): Promise<MPCTx | MPCSweepTxs> {
    if (!params.bitgoKey) {
      throw new Error('missing bitgoKey');
    }
    if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
      throw new Error('invalid recoveryDestination');
    }
    let startIdx = params.startingScanIndex;
    if (startIdx === undefined) {
      startIdx = 0;
    } else if (!Number.isInteger(startIdx) || startIdx < 0) {
      throw new Error('Invalid starting index to scan for addresses');
    }
    let numIteration = params.scan;
    if (numIteration === undefined) {
      numIteration = 20;
    } else if (!Number.isInteger(numIteration) || numIteration <= 0) {
      throw new Error('Invalid scanning factor');
    }
    const bitgoKey = params.bitgoKey.replace(/\s/g, '');
    const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
    const MPC = await EDDSAMethods.getInitializedMpcInstance();
    const { storageAmountPerByte, transferCost, receiptConfig } = await this.getProtocolConfig();

    for (let i = startIdx; i < numIteration + startIdx; i++) {
      const currPath = `m/${i}`;
      const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64);
      let availableBalance = new BigNumber(0);
      try {
        availableBalance = new BigNumber(await this.getAccountBalance(accountId, storageAmountPerByte));
      } catch (e) {
        // UNKNOWN_ACCOUNT error indicates that the address has not partake in any transaction so far, so we will
        // treat it as a zero balance address
        if (e.message !== 'UNKNOWN_ACCOUNT') {
          throw e;
        }
      }
      if (availableBalance.toNumber() <= 0) {
        continue;
      }

      // first build the unsigned txn
      const bs58EncodedPublicKey = nearAPI.utils.serialize.base_encode(new Uint8Array(Buffer.from(accountId, 'hex')));
      const { nonce, blockHash } = await this.getAccessKey({ accountId, bs58EncodedPublicKey });
      const gasPrice = await this.getGasPrice(blockHash);
      const gasPriceFirstBlock = new BigNumber(gasPrice);
      const gasPriceSecondBlock = gasPriceFirstBlock.multipliedBy(1.05);
      const totalGasRequired = new BigNumber(transferCost.sendSir)
        .plus(receiptConfig.sendSir)
        .multipliedBy(gasPriceFirstBlock)
        .plus(new BigNumber(transferCost.execution).plus(receiptConfig.execution).multipliedBy(gasPriceSecondBlock));
      // adding some padding to make sure the gas doesn't go below required gas by network
      const totalGasWithPadding = totalGasRequired.multipliedBy(1.5);
      const feeReserve = BigNumber(Networks[this.network].near.feeReserve);
      const storageReserve = BigNumber(Networks[this.network].near.storageReserve);
      const netAmount = availableBalance.minus(totalGasWithPadding).minus(feeReserve).minus(storageReserve);
      if (netAmount.toNumber() <= 0) {
        throw new Error(
          `Found address ${i} with non-zero fund but fund is insufficient to support a recover ` +
            `transaction. Please start the next scan at address index ${i + 1}.`
        );
      }
      const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
      const txBuilder = factory
        .getTransferBuilder()
        .sender(accountId, accountId)
        .nonce(nonce)
        .receiverId(params.recoveryDestination)
        .recentBlockHash(blockHash)
        .amount(netAmount.toFixed());
      const unsignedTransaction = (await txBuilder.build()) as Transaction;
      let serializedTx = unsignedTransaction.toBroadcastFormat();
      if (!isUnsignedSweep) {
        // Sign the txn
        /* ***************** START **************************************/
        // TODO(BG-51092): This looks like a common part which can be extracted out too
        if (!params.userKey) {
          throw new Error('missing userKey');
        }
        if (!params.backupKey) {
          throw new Error('missing backupKey');
        }
        if (!params.walletPassphrase) {
          throw new Error('missing wallet passphrase');
        }

        // Clean up whitespace from entered values
        const userKey = params.userKey.replace(/\s/g, '');
        const backupKey = params.backupKey.replace(/\s/g, '');

        // Decrypt private keys from KeyCard values
        let userPrv;
        try {
          userPrv = this.bitgo.decrypt({
            input: userKey,
            password: params.walletPassphrase,
          });
        } catch (e) {
          throw new Error(`Error decrypting user keychain: ${e.message}`);
        }
        /** TODO BG-52419 Implement Codec for parsing */
        const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;

        let backupPrv;
        try {
          backupPrv = this.bitgo.decrypt({
            input: backupKey,
            password: params.walletPassphrase,
          });
        } catch (e) {
          throw new Error(`Error decrypting backup keychain: ${e.message}`);
        }
        const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
        /* ********************** END ***********************************/

        // add signature
        const signatureHex = await EDDSAMethods.getTSSSignature(
          userSigningMaterial,
          backupSigningMaterial,
          currPath,
          unsignedTransaction
        );
        const publicKeyObj = { pub: accountId };
        txBuilder.addSignature(publicKeyObj as PublicKey, signatureHex);

        const completedTransaction = await txBuilder.build();
        serializedTx = completedTransaction.toBroadcastFormat();
      } else {
        const value = new BigNumber(netAmount); // Use the calculated netAmount for the transaction
        const walletCoin = this.getChain();
        const inputs = [
          {
            address: accountId, // The sender's account ID
            valueString: value.toString(),
            value: value.toNumber(),
          },
        ];
        const outputs = [
          {
            address: params.recoveryDestination, // The recovery destination address
            valueString: value.toString(),
            coinName: walletCoin,
          },
        ];
        const spendAmount = value.toString();
        const parsedTx = { inputs: inputs, outputs: outputs, spendAmount: spendAmount, type: '' };
        const feeInfo = { fee: totalGasWithPadding.toNumber(), feeString: totalGasWithPadding.toFixed() }; // Include gas fees

        const transaction: MPCTx = {
          serializedTx: serializedTx, // Serialized unsigned transaction
          scanIndex: i, // Current index in the scan
          coin: walletCoin,
          signableHex: unsignedTransaction.signablePayload.toString('hex'), // Hex payload for signing
          derivationPath: currPath, // Derivation path for the account
          parsedTx: parsedTx,
          feeInfo: feeInfo,
          coinSpecific: { commonKeychain: bitgoKey }, // Include block hash for NEAR
        };

        const transactions: MPCUnsignedTx[] = [{ unsignedTx: transaction, signatureShares: [] }];
        const txRequest: RecoveryTxRequest = {
          transactions: transactions,
          walletCoin: walletCoin,
        };
        return { txRequests: [txRequest] };
      }
      return { serializedTx: serializedTx, scanIndex: i };
    }
    throw new Error('Did not find an address with funds to recover');
  }

  async createBroadcastableSweepTransaction(params: MPCSweepRecoveryOptions): Promise<MPCTxs> {
    const req = params.signatureShares;
    const broadcastableTransactions: MPCTx[] = [];
    let lastScanIndex = 0;

    for (let i = 0; i < req.length; i++) {
      const MPC = await EDDSAMethods.getInitializedMpcInstance();
      const transaction = req[i].txRequest.transactions[0].unsignedTx;

      // Validate signature shares
      if (!req[i].ovc || !req[i].ovc[0].eddsaSignature) {
        throw new Error('Missing signature(s)');
      }
      const signature = req[i].ovc[0].eddsaSignature;

      // Validate signable hex
      if (!transaction.signableHex) {
        throw new Error('Missing signable hex');
      }
      const messageBuffer = Buffer.from(transaction.signableHex!, 'hex');
      const result = MPC.verify(messageBuffer, signature);
      if (!result) {
        throw new Error('Invalid signature');
      }

      // Prepare the signature in hex format
      const signatureHex = Buffer.concat([Buffer.from(signature.R, 'hex'), Buffer.from(signature.sigma, 'hex')]);

      // Validate transaction-specific fields
      if (!transaction.coinSpecific?.commonKeychain) {
        throw new Error('Missing common keychain');
      }
      const commonKeychain = transaction.coinSpecific!.commonKeychain! as string;

      if (!transaction.derivationPath) {
        throw new Error('Missing derivation path');
      }
      const derivationPath = transaction.derivationPath as string;

      // Derive account ID and sender address
      const accountId = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
      const txnBuilder = this.getBuilder().from(transaction.serializedTx as string);

      // Add the signature
      const nearKeyPair = new NearKeyPair({ pub: accountId });
      txnBuilder.addSignature({ pub: nearKeyPair.getKeys().pub }, signatureHex);

      // Finalize and serialize the transaction
      const signedTransaction = await txnBuilder.build();
      const serializedTx = signedTransaction.toBroadcastFormat();

      // Add the signed transaction to the list
      broadcastableTransactions.push({
        serializedTx: serializedTx,
        scanIndex: transaction.scanIndex,
      });

      // Update the last scan index if applicable
      if (i === req.length - 1 && transaction.coinSpecific!.lastScanIndex) {
        lastScanIndex = transaction.coinSpecific!.lastScanIndex as number;
      }
    }

    // Return the broadcastable transactions and the last scan index
    return { transactions: broadcastableTransactions, lastScanIndex };
  }

  /**
   * Make a request to one of the public EOS nodes available
   * @param params.payload
   */
  protected async getDataFromNode(params: { payload?: Record<string, unknown> }): Promise<request.Response> {
    const nodeUrls = this.getPublicNodeUrls();
    for (const nodeUrl of nodeUrls) {
      try {
        return await request.post(nodeUrl).send(params.payload);
      } catch (e) {
        console.debug(e);
      }
    }
    throw new Error(`Unable to call endpoint: '/' from nodes: ${_.join(nodeUrls, ', ')}`);
  }

  protected async getAccessKey({
    accountId,
    bs58EncodedPublicKey,
  }: {
    accountId: string;
    bs58EncodedPublicKey: string;
  }): Promise<NearTxBuilderParamsFromNode> {
    const response = await this.getDataFromNode({
      payload: {
        jsonrpc: '2.0',
        id: 'dontcare',
        method: 'query',
        params: {
          request_type: 'view_access_key',
          finality: 'final',
          account_id: accountId,
          public_key: bs58EncodedPublicKey,
        },
      },
    });
    if (response.status !== 200) {
      throw new Error('Account not found');
    }
    const accessKey = response.body.result;
    return { nonce: accessKey.nonce + 1, blockHash: accessKey.block_hash };
  }

  protected async getAccountBalance(accountId: string, storageAmountPerByte: number): Promise<string> {
    const response = await this.getDataFromNode({
      payload: {
        jsonrpc: '2.0',
        id: 'dontcare',
        method: 'query',
        params: {
          request_type: 'view_account',
          finality: 'final',
          account_id: accountId,
        },
      },
    });
    if (response.status !== 200) {
      throw new Error('Failed to query account information');
    }
    const errorCause = response.body.error?.cause.name;
    if (errorCause !== undefined) {
      throw new Error(errorCause);
    }

    const account = response.body.result;
    const costPerByte = new BigNumber(storageAmountPerByte);
    const stateStaked = new BigNumber(account.storage_usage).multipliedBy(costPerByte);
    const staked = new BigNumber(account.locked);
    const totalBalance = new BigNumber(account.amount).plus(staked);
    const availableBalance = totalBalance.minus(BigNumber.max(staked, stateStaked));
    return availableBalance.toString();
  }

  protected async getProtocolConfig(): Promise<ProtocolConfigOutput> {
    const response = await this.getDataFromNode({
      payload: {
        jsonrpc: '2.0',
        id: 'dontcare',
        method: 'EXPERIMENTAL_protocol_config',
        params: {
          finality: 'final',
        },
      },
    });
    if (response.status !== 200) {
      throw new Error('Account not found');
    }

    const config = response.body.result;
    const storageAmountPerByte = config.runtime_config.storage_amount_per_byte;
    const transferCostFromNetwork = config.runtime_config.transaction_costs.action_creation_config.transfer_cost;
    const transferCost: NearFeeConfig = {
      sendSir: transferCostFromNetwork.send_sir,
      sendNotSir: transferCostFromNetwork.send_not_sir,
      execution: transferCostFromNetwork.execution,
    };

    const receiptConfigFromNetwork = config.runtime_config.transaction_costs.action_receipt_creation_config;
    const receiptConfig: NearFeeConfig = {
      sendSir: receiptConfigFromNetwork.send_sir,
      sendNotSir: receiptConfigFromNetwork.send_not_sir,
      execution: receiptConfigFromNetwork.execution,
    };
    return { storageAmountPerByte, transferCost, receiptConfig };
  }

  protected async getGasPrice(blockHash: string): Promise<string> {
    const response = await this.getDataFromNode({
      payload: {
        jsonrpc: '2.0',
        id: 'dontcare',
        method: 'gas_price',
        params: [blockHash],
      },
    });
    if (response.status !== 200) {
      throw new Error('Account not found');
    }
    return response.body.result.gas_price;
  }

  protected getPublicNodeUrls(): string[] {
    return Environments[this.bitgo.getEnv()].nearNodeUrls;
  }

  async parseTransaction(params: NearParseTransactionOptions): Promise<NearParsedTransaction> {
    const transactionExplanation = await this.explainTransaction({
      txPrebuild: params.txPrebuild,
      publicKey: params.publicKey,
      feeInfo: params.feeInfo,
    });

    if (!transactionExplanation) {
      throw new Error('Invalid transaction');
    }

    const nearTransaction = transactionExplanation as NearTransactionExplanation;
    if (nearTransaction.outputs.length <= 0) {
      return {
        inputs: [],
        outputs: [],
      };
    }

    const senderAddress = nearTransaction.outputs[0].address;
    const feeAmount = new BigNumber(nearTransaction.fee.fee === '' ? '0' : nearTransaction.fee.fee);

    // assume 1 sender, who is also the fee payer
    const inputs = [
      {
        address: senderAddress,
        amount: new BigNumber(nearTransaction.outputAmount).plus(feeAmount).toFixed(),
      },
    ];

    const outputs: TransactionOutput[] = nearTransaction.outputs.map((output) => {
      return {
        address: output.address,
        amount: new BigNumber(output.amount).toFixed(),
      };
    });

    return {
      inputs,
      outputs,
    };
  }

  async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
    throw new MethodNotImplementedError();
  }

  async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
    let totalAmount = new BigNumber(0);
    const coinConfig = coins.get(this.getChain());
    const { txPrebuild: txPrebuild, txParams: txParams } = params;
    const transaction = new Transaction(coinConfig);
    const rawTx = txPrebuild.txHex;
    if (!rawTx) {
      throw new Error('missing required tx prebuild property txHex');
    }

    transaction.fromRawTransaction(rawTx);
    const explainedTx = transaction.explainTransaction();

    // users do not input recipients for consolidation requests as they are generated by the server
    if (txParams.recipients !== undefined) {
      const filteredRecipients = txParams.recipients?.map((recipient) => _.pick(recipient, ['address', 'amount']));
      const filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount']));

      if (!_.isEqual(filteredOutputs, filteredRecipients)) {
        throw new Error('Tx outputs does not match with expected txParams recipients');
      }
      for (const recipients of txParams.recipients) {
        totalAmount = totalAmount.plus(recipients.amount);
      }
      if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
        throw new Error('Tx total amount does not match with expected total amount field');
      }
    }
    return true;
  }

  private getBuilder(): TransactionBuilderFactory {
    return new TransactionBuilderFactory(coins.get(this.getBaseChain()));
  }
}

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


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