Integración con un Exchange

Este documento está destinado a guiar a los desarrolladores a través de la integración del token BXR en una plataforma de Exchange. Contiene recomendaciones sobre cómo configurar cuentas, escuchar depósitos y crear retiros, así como ejemplos de códigos listos para ser adoptados.

Los ejemplos de código compartidos utilizan Bitxor SDK para TypeScript, pero se pueden trasladar a otros SDK disponibles ya que todos ellos comparten los mismos principios de diseño. Si no hay un SDK compatible con un lenguaje de programación requerido, aún puede integrarse conectándose directamente a través de Bitxor’s REST API.

Descripción general de la integración

Hay muchas maneras de diseñar un intercambio. Esta guía se basa en cómo respaldar los depósitos y retiros BXR en un intercambio que sigue un enfoque de monedero central.

Tenga en cuenta que este diseño no se recomienda especialmente sobre otros. Sin embargo, su arquitectura simplificada es un buen escaparate para el conjunto de características de Bitxor involucradas en la integración con un Exchange. Un enfoque diferente, por ejemplo, sería usar una billetera diferente para cada usuario.

../../_images/exchange-integration-overview.png

Fig. 1: Diagrama de diseño general del enfoque de billetera central.

Los principales componentes de esta arquitectura se describen a continuación.

Componentes

Monedero central

El intercambio posee una cuenta Bitxor donde se realizan todos los depósitos y retiros del usuario. Las claves de esta cuenta deben estar en una máquina en línea, por lo que esto también se conoce comúnmente como la billetera Hot. Esta cuenta solo tiene la cantidad necesaria de BXR para uso diario (retiros y depósitos), ya que es la cuenta más expuesta a ataques.

billetera fría

Las billeteras frías tienen un cierto umbral para el grupo de BXR. Estas cuentas deben crearse y permanecer en una configuración sin conexión a Internet. Las transacciones emitidas desde monederos fríos deben firmarse fuera de línea y anunciarse a la red usando otro dispositivo. También es recomendable que las billeteras frías estén configuradas con cuentas multisig.

ID de usuario único

En la arquitectura propuesta, cada usuario se identifica mediante un identificador de usuario único (UUID) en la base de datos del intercambio. Un usuario depositará en la billetera central con su UUID adjunto como el mensaje de la transacción (a veces llamado memo). El UUID solo se muestra en el panel del usuario durante la confirmación del depósito.

Uno de los inconvenientes de este diseño es que muchos usuarios no están acostumbrados a tener un mensaje adjunto a sus transacciones. Si se olvidan de adjuntar el UUID o adjuntan un UUID incorrecto, recibirán muchos tickets de soporte relacionados con “fondos perdidos”.

Caution

Transacciones de transferencia de Bitxor puede contener un mensaje arbitrario de hasta 1023 bytes de largo, pero el primer byte es tratado especialmente por el SDK de Bitxor.

Esto puede ser una fuente de confusión porque el receptor de una transacción no sabe si el SDK de Bitxor generó el mensaje o no (por ejemplo, accediendo a la puerta de enlace REST), por lo que no sabe si el primer byte debe ser tratado de forma especial o no.

Para evitar cualquier problema, las siguientes medidas siempre deben aplicarse:

  • Siempre comience los mensajes con un byte en el rango de 32 a 128 (este es el rango imprimible ASCII estándar).

  • Siempre ignorar cualquier byte inicial recibido fuera del rango de 32 a 128.

Siga estas reglas, independientemente de si usa Bitxor SDK o no para generar y analizar transacciones de transferencia.

Servidor de exchange

Esta máquina escucha constantemente las solicitudes de retiro de los usuarios y monitorea la cadena de bloques para detectar los depósitos de los usuarios en Exchange Central Wallet. Como se explica en el resto de este documento, mantiene la base de datos actualizada y anuncia cualquier transacción requerida.

Base de datos de intercambio

Todos los fondos del usuario se fusionan en las billeteras de Exchange. Esta base de datos realiza un seguimiento de la cantidad de tokens que posee cada usuario individual. También registra todas las transacciones procesadas, para el mantenimiento de registros y para evitar procesar la misma transacción más de una vez.

Ejecutar un nodo

Aunque no es absolutamente necesario, se recomienda que los intercambios implementen su propio nodo Bitxor para comunicarse con el resto de la red. Dado que cada nodo se conecta automáticamente a varios otros nodos en la red, este enfoque es más sólido que acceder a la red siempre a través del mismo nodo público, que podría dejar de estar disponible.

Si no puede ejecutar su propio nodo, puede elegir uno de la lista proporcionada por el Servicio de Estadísticas.

Consulte las diferentes guías sobre la implementación de nodos Bitxor y asegúrese de crear un API nodo.

Configuración de cuentas

Cada billetera tiene asignada una cuenta (una caja de depósito que contiene tokens, que solo se pueden transferir con la clave privada adecuada).

Caution

La clave privada debe mantenerse en secreto en todo momento y no debe compartirse. Perder la clave privada significa perder el acceso a los fondos de una cuenta, así que asegúrese de tener una copia de seguridad segura.

Es recomendable convertir las billeteras frías y centrales en cuentas multisig para agregar autenticación de dos factores. Los cosignatarios de la cuenta multisig se convierten en los administradores de la cuenta, por lo que no se puede anunciar ninguna transacción desde la cuenta multisig sin la aprobación de los cosignatarios. La implementación actual de multisig de Bitxor es “M-de-N”, lo que significa que M del total de N cosignatarios de una cuenta deben aprobar una transacción para que se anuncie.

Caution

Las cuentas multigrado son una herramienta poderosa pero peligrosa. Si se pierde el acceso a alguna cuenta de cosignatario y no se alcanza la aprobación mínima (la M anterior), el acceso a la cuenta multigrado se puede perder de forma permanente. Configure siempre las cuentas multigrado con precaución.

Para fortalecer la seguridad, se pueden agregar restricciones adicionales de cuenta a las cuentas de Exchange, como bloquear anuncios o recibir transacciones según una serie de reglas.

El token BXR

La moneda nativa de la red Bitxor se llama BXR. El token se utiliza para pagar transacciones y servicios fees, que también se utilizan para proporcionar un incentivo para aquellos participantes que protegen la red y ejecutan la infraestructura.

Las fichas se pueden dividir hasta decimales de divisibilidad. Las cantidades dadas sin decimales se llaman absolutas, mientras que cuando se usan decimales, las cantidades se llaman relativas. Por ejemplo, cuando la divisibilidad es 6, 1 ficha relativa corresponde a 1 000 000 fichas absolutas, y la ficha más pequeña es 0,000001 unidades relativas. La unidad absoluta más pequeña es siempre 1, independientemente de la divisibilidad.

Estas son las propiedades de BXR:

Property

Value

Description

IDENTIFICACIÓN

0x3D1FE6EDC7F9611E

Identificador único de token

Alias

bitxor

Nombre descriptivo para el token

Suministro inicial

160.000.000 (relativa)

Cantidad inicial de unidades de token en circulación

suministro máximo

800.000.000 (relativa)

Cantidad máxima de unidades de token en circulación después de aplicar inflation

Divisibilidad

8

Esto significa que la fracción más pequeña del token es 0.000001 (relativa).

Duración

0

El token no caduca

Suministro mutable

FALSO

El suministro de tokens no se puede modificar

Transferible

Verdadero

El token se puede transferir entre cuentas arbitrarias

Restringible

FALSO

El creador del token no puede restringir qué cuentas pueden realizar transacciones con el token

Caution

Se puede hacer referencia al token BXR a través de su ID de token nativo o su alias más amigable bitxor, que tiene una ID en sí mismo.

En MAINNET, estos ID son 0x3D1FE6EDC7F9611E (ID de token) y 0xEE905A59E4F6DB7D (ID de alias).

Always treat these two IDs as equivalent.

agregados

Bitxor tiene una característica novedosa llamada transacción agregada que permite agrupar múltiples transacciones internas en una sola.

Por lo tanto, al monitorear las transacciones de Transferencia entrantes, debe recordar mirar también dentro de todas las transacciones Agregadas (tanto las transacciones Agregadas completas como las Agregadas vinculadas). El código de ejemplo dado debajo de muestra una forma de lograr esto.

Caution

Cuando las transacciones agregadas no se supervisan, las transacciones de transferencias internas no se detectan, lo que genera muchos informes de fondos perdidos.

Esto es especialmente relevante para cuentas de múltiples firmas, donde todas las transacciones se envuelven en un Agregado.

Evitar reversiones

Este es un conflicto clásico en la tecnología blockchain: por un lado, si las transacciones se aceptan demasiado rápido, es posible que deban ser revertidas más adelante en caso de una :ref:`red fork <rollbacks> `. Por otro lado, esperar demasiado es inconveniente para los usuarios.

Hay dos formas de lidiar con esto en Bitxor:

Uso de la finalización

Bitxor implementa Finalización, un proceso que garantiza que los bloques son inmutables y por lo tanto las transacciones son seguras.

Para saber si un bloque ha sido finalizado, verifique la propiedad latestFinalizedBlock en /chain/info punto final Todos los bloques con una altura menor que (o igual a) latestFinalizedBlock.height están finalizados y, por lo tanto, son inmutables.

En promedio, los bloques se finalizan después de 5 minutos, en ausencia de problemas de red.

Usar una espera fija

Para tener tiempos de respuesta más rápidos, uno debe ignorar la finalización y aceptar el riesgo que conlleva: Los bloques no finalizados tienen una probabilidad de revertirse, que disminuye con el tiempo pero nunca es cero hasta que se finaliza el bloque.

El procedimiento, que es común en las cadenas de bloques que no admiten la finalización, es esperar a que se validen algunos bloques (agregados a la cadena de bloques) antes de aceptar una transacción.

La cantidad de bloques a esperar depende del riesgo que uno quiera aceptar. La recomendación para Bitxor es 20 bloques (alrededor de 10 minutos, independientemente de las condiciones de la red, porque la finalización casi siempre sucederá durante este tiempo).

Plazos

Un problema adicional causado por las reversiones es que las transacciones pueden caducar en el proceso de resolución de una bifurcación de la red.

Se requiere un poco de contexto aquí. No se permite que las transacciones permanezcan sin confirmar en la red para siempre, ya que esto supondría una carga significativa para los recursos de la red. En cambio, todas las transacciones tienen una fecha límite y se eliminan automáticamente cuando llega la fecha límite.

Los usuarios son libres de usar cualquier fecha límite que deseen para sus transacciones, entre ahora y 6 horas en el futuro (48 horas para transacciones agregadas-vinculadas).

Las transacciones que están a punto de caducar son delicadas porque, incluso si se confirman y se agregan a la cadena de bloques, una reversión podría devolverlas al estado no confirmado y su fecha límite podría caducar antes de que se vuelvan a confirmar.

El código de ejemplo

Esta guía muestra fragmentos de código para ejemplificar los diferentes procesos. Todos los fragmentos se basan en el mismo programa que se puede encontrar aquí <https://github.com/bitxorcorp/bitxor-docs/tree/main/source/resources/examples/typescript/exchanges>`__. Algunas notas sobre este programa de ejemplo:

  • Utiliza un objeto DBService falso que simula la base de datos de Exchange. Obviamente, las llamadas a este objeto deben reemplazarse por la infraestructura de Exchange real en el código de producción. Para simplificar, estas llamadas son síncronas, pero también pueden realizarse de forma asíncrona.

  • No se realiza ningún manejo de errores. Usa mecanismos como try {} catch cuando corresponda en el código de producción.

  • Finalmente, además de los fragmentos que se muestran en la guía, el programa completo también contiene código auxiliar (como bucles de sondeo) para que sea ejecutable y autosuficiente. Este código auxiliar no está destinado a ser utilizado como inspiración en absoluto, solo está ahí por conveniencia.

Depósitos

../../_images/exchange-integration-deposit.png

Fig. 2: Deposit process.

Los usuarios realizan depósitos anunciando una transacción de transferencia regular usando su billetera, moviendo los fondos de su cuenta directamente a Exchange Central Wallet. Dado que la transferencia es manejada completamente por blockchain, los fondos se agregarán a Exchange Central Wallet sin la mediación de Exchange, y esto plantea algunos problemas:

  • Se debe determinar el destinatario de la transacción. Esto se hace adjuntando el UUID del usuario como mensaje de la transacción.

  • El hecho de que haya ocurrido una transacción debe ser detectado oportunamente para actualizar la cuenta del usuario en el Exchange.

  • Las transacciones deben finalizarse para estar 100 % seguro de que no serán revertidas.

El código propuesto a continuación aborda todos estos problemas al monitorear la cadena de bloques.

Supervisión

La cadena de bloques se sondea periódicamente y todas las transacciones entrantes desde la última encuesta se procesan en un lote:

1.Se examinan todas las transacciones de Transferencia añadidas a la cadena de bloques desde el último cheque y hasta el último bloque finalizado, buscando las destinadas al Monedero Central Exchange. Esto se puede hacer de manera eficiente con una sola llamada a la API de Bitxor.

  • Las transacciones de transferencia integradas en las transacciones Agregado completo y Agregado en condiciones de servidumbre también deben examinarse (consulte la sección intercambio-agregado-transacciones anterior). Esto se maneja en el código de ejemplo mediante el parámetro embedded: true en la llamada ``searchConfirmedTransactions <https://bitxor.github.io/bitxor-openapi/v1.0.4/#operation/searchConfirmedTransactions>`__.

  • Si no desea la finalización (consulte la sección exchange-evite-rollbacks anterior), puede buscar hasta 20 bloques antes de la altura de la cadena actual, por ejemplo.

  1. Filtre las transacciones que:

    1. No tiene mensaje o el mensaje no corresponde a un UUID existente.

    2. No contienen tokens, o el token no es bitxor.

    1. Ya han sido procesados ​​(como medida de seguridad).

  2. A continuación, se procesan las transacciones restantes:

    1. Los tokens se agregan a la cuenta del usuario en la base de datos.

    2. La transacción se marca como procesada agregando su hash a la base de datos

  3. Guarde la última altura que se haya procesado y espere al siguiente período de sondeo.

El fragmento de código, usando el SDK de TypeScript de Bitxor <https://bitxor.github.io/bitxor-sdk-typescript-javascript/1.0.0/>`__ es este:

  // Mocks a database service
  const dbService = new DBServiceImpl();

  const config: ExchangeBitxorConfig = {
    // Replace with your node URL
    apiUrl: 'NODE_URL',
    // Use MAIN_NET or TEST_NET
    networkType: NetworkType.TEST_NET,
    // Replace with value from http://<API-NODE-URL>:3000/network/properties
    networkGenerationHashSeed:
      '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
    // Replace with the account central address
    centralAccountAddress: Address.createFromRawAddress(
      'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
    ),
    // Use 3D1FE6EDC7F9611E for MAIN_NET or 091F837E059AE13C for TEST_NET
    tokenId: new TokenId('091F837E059AE13C'),
    tokenAlias: new NamespaceId('bitxor'),
    tokenDivisibility: 6,
    requiredConfirmations: 20,
    useFinalization: true,
  };

  const repositoryFactory = new RepositoryFactoryHttp(config.apiUrl);
  const chainHttp = repositoryFactory.createChainRepository();
  const transactionHttp = repositoryFactory.createTransactionRepository();
  const transactionPaginationStreamer = new TransactionPaginationStreamer(
    transactionHttp,
  );

  // Get current chain height and latest finalized block
  const {
    height: currentHeight,
    latestFinalizedBlock: finalizationBlock,
  } = await chainHttp.getChainInfo().toPromise();
  const maxHeight = config.useFinalization
    ? finalizationBlock.height
    : currentHeight.subtract(UInt64.fromUint(config.requiredConfirmations));

  // 1. Look for confirmed transactions destined to the central account address,
  // in the desired block height range.
  const searchCriteria = {
    group: TransactionGroup.Confirmed,
    recipientAddress: config.centralAccountAddress,
    embedded: true,
    order: Order.Asc,
    type: [TransactionType.TRANSFER],
    fromHeight: dbService.getLastProcessedHeight(),
    toHeight: maxHeight,
  } as TransactionSearchCriteria;

  const data = await transactionPaginationStreamer
    .search(searchCriteria)
    .pipe(toArray() as any)
    .toPromise();

  // 2. Exclude invalid transactions
  const results = (data as TransferTransaction[]).filter((transaction) => {
    const transactionInfo = transaction.transactionInfo;
    if (!transactionInfo) return false;
    const transactionIndex = transactionInfo.index;
    const transactionHash =
      transactionInfo instanceof AggregateTransactionInfo
        ? transactionInfo.aggregateHash
        : transactionInfo.hash ?? '';

    return (
      // 2.a
      dbService.existsUser(transaction.message.payload) &&
      // 2.b
      transaction.tokens.length === 1 &&
      (transaction.tokens[0].id.toHex() === config.tokenId.toHex() ||
        transaction.tokens[0].id.toHex() === config.tokenAlias.toHex()) &&
      // 2.c
      !dbService.existsTransaction(transactionHash, transactionIndex)
    );
  });

  // 3. Record the valid deposits in the exchange database
  results.forEach((transaction) => {
    const transactionInfo = transaction.transactionInfo;
    if (!transactionInfo) return;
    const transactionHash =
      transactionInfo instanceof AggregateTransactionInfo
        ? transactionInfo.aggregateHash
        : transactionInfo.hash ?? '';
    const transactionIndex = transactionInfo.index;
    const amount =
      transaction.tokens[0].amount.compact() /
      Math.pow(10, config.tokenDivisibility);
    dbService.recordDeposit(
      transaction.message.payload,
      amount,
      transactionHash,
      transactionIndex,
    );
  });

  // 4. Store the last height that has been processed
  dbService.setLastProcessedHeight(maxHeight);
    // Mocks a database service
    const dbService = new DBServiceImpl_1.DBServiceImpl();
    const config = {
        // Replace with your node URL
        apiUrl: 'NODE_URL',
        // Use MAIN_NET or TEST_NET
        networkType: bitxor_sdk_1.NetworkType.TEST_NET,
        // Replace with value from http://<API-NODE-URL>:3000/network/properties
        networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
        // Replace with the account central address
        centralAccountAddress: bitxor_sdk_1.Address.createFromRawAddress(
            'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
        ),
        // Use 3D1FE6EDC7F9611E for MAIN_NET or 091F837E059AE13C for TEST_NET
        tokenId: new bitxor_sdk_1.TokenId('091F837E059AE13C'),
        tokenAlias: new bitxor_sdk_1.NamespaceId('bitxor'),
        tokenDivisibility: 6,
        requiredConfirmations: 20,
        useFinalization: true,
    };
    const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(
        config.apiUrl,
    );
    const chainHttp = repositoryFactory.createChainRepository();
    const transactionHttp = repositoryFactory.createTransactionRepository();
    const transactionPaginationStreamer = new bitxor_sdk_1.TransactionPaginationStreamer(
        transactionHttp,
    );
    // Get current chain height and latest finalized block
    const {
        height: currentHeight,
        latestFinalizedBlock: finalizationBlock,
    } = await chainHttp.getChainInfo().toPromise();
    const maxHeight = config.useFinalization ?
        finalizationBlock.height :
        currentHeight.subtract(
            bitxor_sdk_1.UInt64.fromUint(config.requiredConfirmations),
        );
    // 1. Look for confirmed transactions destined to the central account address,
    // in the desired block height range.
    const searchCriteria = {
        group: bitxor_sdk_1.TransactionGroup.Confirmed,
        recipientAddress: config.centralAccountAddress,
        embedded: true,
        order: bitxor_sdk_1.Order.Asc,
        type: [bitxor_sdk_1.TransactionType.TRANSFER],
        fromHeight: dbService.getLastProcessedHeight(),
        toHeight: maxHeight,
    };
    const data = await transactionPaginationStreamer
        .search(searchCriteria)
        .pipe(operators_1.toArray())
        .toPromise();
    // 2. Exclude invalid transactions
    const results = data.filter((transaction) => {
        var _a;
        const transactionInfo = transaction.transactionInfo;
        if (!transactionInfo) return false;
        const transactionIndex = transactionInfo.index;
        const transactionHash =
            transactionInfo instanceof bitxor_sdk_1.AggregateTransactionInfo ?
            transactionInfo.aggregateHash :
            (_a = transactionInfo.hash) !== null && _a !== void 0 ?
            _a :
            '';
        return (
            // 2.a
            dbService.existsUser(transaction.message.payload) &&
            // 2.b
            transaction.tokens.length === 1 &&
            (transaction.tokens[0].id.toHex() === config.tokenId.toHex() ||
                transaction.tokens[0].id.toHex() === config.tokenAlias.toHex()) &&
            // 2.c
            !dbService.existsTransaction(transactionHash, transactionIndex)
        );
    });
    // 3. Record the valid deposits in the exchange database
    results.forEach((transaction) => {
        var _a;
        const transactionInfo = transaction.transactionInfo;
        if (!transactionInfo) return;
        const transactionHash =
            transactionInfo instanceof bitxor_sdk_1.AggregateTransactionInfo ?
            transactionInfo.aggregateHash :
            (_a = transactionInfo.hash) !== null && _a !== void 0 ?
            _a :
            '';
        const transactionIndex = transactionInfo.index;
        const amount =
            transaction.tokens[0].amount.compact() /
            Math.pow(10, config.tokenDivisibility);
        dbService.recordDeposit(
            transaction.message.payload,
            amount,
            transactionHash,
            transactionIndex,
        );
    });
    // 4. Store the last height that has been processed
    dbService.setLastProcessedHeight(maxHeight);

Todos los datos de configuración se guardan en el objeto ExchangeBitxorConfig, incluida la selección del mecanismo de finalización.

El fragmento de código anterior debe llamarse en un bucle cada minuto, por ejemplo, y procesará todas las nuevas transacciones válidas que ya se han finalizado (o que han esperado suficientes bloques, según el método elegido ).

Sin embargo, las transacciones no se informarán de inmediato y esto puede resultar molesto para los usuarios. Usando WebSockets las transacciones se pueden monitorear en tiempo real y se puede mostrar una notificación al usuario tan pronto como se confirme una transacción en la red (o incluso tan pronto como * anunciado* en la red).

Estas transacciones, sin embargo, deben marcarse claramente como pendientes y sin acción hasta que se verifiquen con el código anterior, para evitar reversiones.

Retiros

../../_images/exchange-integration-withdrawal.png

Fig. 3: Withdrawal process.

Los usuarios envían solicitudes de retiro al Exchange Server, a través de una página web o una aplicación móvil, por ejemplo. Si la base de datos indica que el usuario tiene fondos suficientes para realizar el retiro, se anuncia una transacción de transferencia desde Exchange Central Wallet a la dirección de Bitxor indicada en el pedido.

Anunciar la transacción tiene un fee, que es pagado por Exchange Central Wallet pero puede deducirse de la cuenta del usuario. Independientemente del token que se transfiera, las tarifas siempre se pagan en tokens BXR.

El proceso de retiro requiere dos pasos: primero, la transacción que transfiere los fondos se anuncia y se confirma (se agrega a la cadena de bloques). Luego, el intercambio debe esperar a que la transacción se finalice, como se explica en la sección anterior exchange-evitar-rollbacks.

anunciando

La transacción de retiro es solo una transacción de transferencia normal de Bitxor <../../concepts/transfer-transaction>. El código parece largo porque contiene muchos repeticiones repetitivas, para que sea autónomo:

  • La configuración se almacena en el objeto ExchangeBitxorConfig.

  • Se crean instancias de varios repositorios a través de la clase RepositoryFactoryHttp.

  • Los detalles de retiro se recuperan de las variables de entorno en este ejemplo.

Entonces:

  1. La transacción real se crea usando TransferTransaction.create.

  2. Se firma la transacción.

  3. La transacción firmada se anuncia mediante TransactionService para simplificar la espera de su confirmación.

  // Mocks a database service
  const dbService = new DBServiceImpl();

  // Exchange configuration
  const config: ExchangeBitxorConfig = {
    // Replace with your node URL
    apiUrl: 'NODE_URL',
    // Use MAIN_NET or TEST_NET
    networkType: NetworkType.TEST_NET,
    // Replace with value from http://<API-NODE-URL>:3000/network/properties
    networkGenerationHashSeed:
      '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
    // Replace with the account central address
    centralAccountAddress: Address.createFromRawAddress(
      'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
    ),
    // Use 3D1FE6EDC7F9611E for MAIN_NET or 091F837E059AE13C for TEST_NET
    tokenId: new TokenId('091F837E059AE13C'),
    tokenAlias: new NamespaceId('bitxor'),
    tokenDivisibility: 6,
    requiredConfirmations: 20,
    useFinalization: true,
  };

  // Repositories
  const repositoryFactory = new RepositoryFactoryHttp(config.apiUrl);
  const listener = repositoryFactory.createListener();
  const transactionHttp = repositoryFactory.createTransactionRepository();
  const chainHttp = repositoryFactory.createChainRepository();
  const transactionService = new TransactionService(
    transactionHttp,
    repositoryFactory.createReceiptRepository(),
  );
  const transactionPaginationStreamer = new TransactionPaginationStreamer(
    transactionHttp,
  );

  // Replace with exchange account private key
  const exchangeAccountPrivateKey = process.env.PRIVATE_KEY as string;
  const exchangeAccount = Account.createFromPrivateKey(
    exchangeAccountPrivateKey,
    config.networkType,
  );
  // Replace with destination address from user's request
  const userRawAddress = process.env.RECIPIENT_ADDRESS as string;
  const userAddress = Address.createFromRawAddress(userRawAddress);
  // Replace with the source UUID from user's request
  const uuid = process.env.UUID as string;
  // Replace with amount of tokens to transfer from user's request
  const relativeAmount = parseFloat(process.env.AMOUNT as string);

  // Check that the user has enough funds
  if (dbService.getUserAmount(uuid, config.tokenId) < relativeAmount) {
    throw Error('User ' + uuid + ' does not have enough funds.');
  }

  // 1. Create withdrawal transaction
  const absoluteAmount =
    relativeAmount * Math.pow(10, config.tokenDivisibility);
  const token = new Token(config.tokenId, UInt64.fromUint(absoluteAmount));

  const epochAdjustment = await repositoryFactory
    .getEpochAdjustment()
    .toPromise();

  const withdrawalTransaction = TransferTransaction.create(
    Deadline.create(epochAdjustment),
    userAddress,
    [token],
    EmptyMessage,
    config.networkType,
    UInt64.fromUint(200000), // Fixed max fee of 0.2 BXR
  );

  // 2. Sign transaction
  const signedTransaction = exchangeAccount.sign(
    withdrawalTransaction,
    config.networkGenerationHashSeed,
  );

  // 3. Announce transaction and wait for confirmation
  console.log('Announcing transaction', signedTransaction.hash);
  await listener.open();
  const transaction = await transactionService
    .announce(signedTransaction, listener)
    .toPromise();
  console.log(
    'Transaction confirmed at height',
    transaction.transactionInfo?.height.compact() ?? 0,
  );
  listener.close();
    // Mocks a database service
    const dbService = new DBServiceImpl_1.DBServiceImpl();
    // Exchange configuration
    const config = {
        // Replace with your node URL
        apiUrl: 'NODE_URL',
        // Use MAIN_NET or TEST_NET
        networkType: bitxor_sdk_1.NetworkType.TEST_NET,
        // Replace with value from http://<API-NODE-URL>:3000/network/properties
        networkGenerationHashSeed: '3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
        // Replace with the account central address
        centralAccountAddress: bitxor_sdk_1.Address.createFromRawAddress(
            'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
        ),
        // Use 3D1FE6EDC7F9611E for MAIN_NET or 091F837E059AE13C for TEST_NET
        tokenId: new bitxor_sdk_1.TokenId('091F837E059AE13C'),
        tokenAlias: new bitxor_sdk_1.NamespaceId('bitxor'),
        tokenDivisibility: 6,
        requiredConfirmations: 20,
        useFinalization: true,
    };
    // Repositories
    const repositoryFactory = new bitxor_sdk_1.RepositoryFactoryHttp(
        config.apiUrl,
    );
    const listener = repositoryFactory.createListener();
    const transactionHttp = repositoryFactory.createTransactionRepository();
    const chainHttp = repositoryFactory.createChainRepository();
    const transactionService = new bitxor_sdk_1.TransactionService(
        transactionHttp,
        repositoryFactory.createReceiptRepository(),
    );
    const transactionPaginationStreamer = new bitxor_sdk_1.TransactionPaginationStreamer(
        transactionHttp,
    );
    // Replace with exchange account private key
    const exchangeAccountPrivateKey = process.env.PRIVATE_KEY;
    const exchangeAccount = bitxor_sdk_1.Account.createFromPrivateKey(
        exchangeAccountPrivateKey,
        config.networkType,
    );
    // Replace with destination address from user's request
    const userRawAddress = process.env.RECIPIENT_ADDRESS;
    const userAddress = bitxor_sdk_1.Address.createFromRawAddress(userRawAddress);
    // Replace with the source UUID from user's request
    const uuid = process.env.UUID;
    // Replace with amount of tokens to transfer from user's request
    const relativeAmount = parseFloat(process.env.AMOUNT);
    // Check that the user has enough funds
    if (dbService.getUserAmount(uuid, config.tokenId) < relativeAmount) {
        throw Error('User ' + uuid + ' does not have enough funds.');
    }
    // 1. Create withdrawal transaction
    const absoluteAmount =
        relativeAmount * Math.pow(10, config.tokenDivisibility);
    const token = new bitxor_sdk_1.Token(
        config.tokenId,
        bitxor_sdk_1.UInt64.fromUint(absoluteAmount),
    );
    const epochAdjustment = await repositoryFactory
        .getEpochAdjustment()
        .toPromise();
    const withdrawalTransaction = bitxor_sdk_1.TransferTransaction.create(
        bitxor_sdk_1.Deadline.create(epochAdjustment),
        userAddress, [token],
        bitxor_sdk_1.EmptyMessage,
        config.networkType,
        bitxor_sdk_1.UInt64.fromUint(200000),
    );
    // 2. Sign transaction
    const signedTransaction = exchangeAccount.sign(
        withdrawalTransaction,
        config.networkGenerationHashSeed,
    );
    // 3. Announce transaction and wait for confirmation
    console.log('Announcing transaction', signedTransaction.hash);
    await listener.open();
    const transaction = await transactionService
        .announce(signedTransaction, listener)
        .toPromise();
    console.log(
        'Transaction confirmed at height',
        (_b =
            (_a = transaction.transactionInfo) === null || _a === void 0 ?
            void 0 :
            _a.height.compact()) !== null && _b !== void 0 ?
        _b :
        0,
    );
    listener.close();

Una vez que se confirma la transacción, el siguiente paso es esperar a que se finalice para asegurarse de que no se pueda revertir. Hasta entonces, debe marcarse como pendiente y sin actuar.

Finalización

La espera para la finalización se realiza de una manera muy similar a cómo se monitorean los depósitos entrantes (consulte Depósitos arriba): la cadena de bloques se sondea periódicamente y todas las transacciones desde el último cheque se procesan en un lote, en busca de salientes. transferencias que ya han sido finalizadas.

El siguiente fragmento de código debe ejecutarse en un bucle cada minuto, por ejemplo, y buscará operaciones de retiro finalizadas de Exchange y las registrará en la base de datos de Exchange.

Este fragmento se puede ejecutar en el mismo ciclo que el monitor de depósitos descrito arriba.

    // Get current chain height and latest finalized block
    const {
      height: currentHeight,
      latestFinalizedBlock: finalizationBlock,
    } = await chainHttp.getChainInfo().toPromise();
    const maxHeight = config.useFinalization
      ? finalizationBlock.height
      : currentHeight.subtract(UInt64.fromUint(config.requiredConfirmations));

    // Bail out if there have been no new (final) transactions since last check
    const lastProcessedHeight = dbService.getLastProcessedHeight();
    if (lastProcessedHeight.equals(maxHeight)) return;

    // Look for confirmed transactions signed by the central account address,
    // in the desired block height range.
    const searchCriteria = {
      group: TransactionGroup.Confirmed,
      signerPublicKey: exchangeAccount.publicKey,
      embedded: true,
      order: Order.Asc,
      type: [TransactionType.TRANSFER],
      fromHeight: lastProcessedHeight.add(UInt64.fromUint(1)),
      toHeight: maxHeight,
    } as TransactionSearchCriteria;

    const data = await transactionPaginationStreamer
      .search(searchCriteria)
      .pipe(toArray() as any)
      .toPromise();

    console.log(
      'Processing',
      (data as TransferTransaction[]).length,
      'entries from height',
      searchCriteria.fromHeight?.compact(),
      'to',
      searchCriteria.toHeight?.compact(),
    );

    // Record the valid withdrawals in the exchange database
    (data as TransferTransaction[]).forEach((transaction) => {
      const transactionInfo = transaction.transactionInfo;
      if (!transactionInfo) return;
      const transactionHash =
        transactionInfo instanceof AggregateTransactionInfo
          ? transactionInfo.aggregateHash
          : transactionInfo.hash ?? '';
      const amount =
        transaction.tokens[0].amount.compact() /
        Math.pow(10, config.tokenDivisibility);
      dbService.recordWithdrawal(uuid, amount, transactionHash);
    });

    // Store the last height that has been processed
    dbService.setLastProcessedHeight(maxHeight);
        // Get current chain height and latest finalized block
        const {
            height: currentHeight,
            latestFinalizedBlock: finalizationBlock,
        } = await chainHttp.getChainInfo().toPromise();
        const maxHeight = config.useFinalization ?
            finalizationBlock.height :
            currentHeight.subtract(
                bitxor_sdk_1.UInt64.fromUint(config.requiredConfirmations),
            );
        // Bail out if there have been no new (final) transactions since last check
        const lastProcessedHeight = dbService.getLastProcessedHeight();
        if (lastProcessedHeight.equals(maxHeight)) return;
        // Look for confirmed transactions signed by the central account address,
        // in the desired block height range.
        const searchCriteria = {
            group: bitxor_sdk_1.TransactionGroup.Confirmed,
            signerPublicKey: exchangeAccount.publicKey,
            embedded: true,
            order: bitxor_sdk_1.Order.Asc,
            type: [bitxor_sdk_1.TransactionType.TRANSFER],
            fromHeight: lastProcessedHeight.add(bitxor_sdk_1.UInt64.fromUint(1)),
            toHeight: maxHeight,
        };
        const data = await transactionPaginationStreamer
            .search(searchCriteria)
            .pipe(operators_1.toArray())
            .toPromise();
        console.log(
            'Processing',
            data.length,
            'entries from height',
            (_a = searchCriteria.fromHeight) === null || _a === void 0 ?
            void 0 :
            _a.compact(),
            'to',
            (_b = searchCriteria.toHeight) === null || _b === void 0 ?
            void 0 :
            _b.compact(),
        );
        // Record the valid withdrawals in the exchange database
        data.forEach((transaction) => {
            var _a;
            const transactionInfo = transaction.transactionInfo;
            if (!transactionInfo) return;
            const transactionHash =
                transactionInfo instanceof bitxor_sdk_1.AggregateTransactionInfo ?
                transactionInfo.aggregateHash :
                (_a = transactionInfo.hash) !== null && _a !== void 0 ?
                _a :
                '';
            const amount =
                transaction.tokens[0].amount.compact() /
                Math.pow(10, config.tokenDivisibility);
            dbService.recordWithdrawal(uuid, amount, transactionHash);
        });
        // Store the last height that has been processed
        dbService.setLastProcessedHeight(maxHeight);

Más información

Lea las siguientes páginas para obtener más información: