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.
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.
Los principales componentes de esta arquitectura se describen a continuación.
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.
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.
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.
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.
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.
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.
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.
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 |
|
Identificador único de token |
Alias |
|
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.
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.
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:
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.
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).
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.
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.
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.
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.
Filtre las transacciones que:
No tiene mensaje o el mensaje no corresponde a un UUID existente.
No contienen tokens, o el token no es bitxor
.
Ya han sido procesados (como medida de seguridad).
A continuación, se procesan las transacciones restantes:
Los tokens se agregan a la cuenta del usuario en la base de datos.
La transacción se marca como procesada agregando su hash a la base de datos
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.
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.
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:
La transacción real se crea usando TransferTransaction.create
.
Se firma la transacción.
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.
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);
Lea las siguientes páginas para obtener más información: