Llevo unos meses siguiendo temas relacionados con las criptomonedas como Bitcoin y estoy muy fascinado con todo lo que está pasando.

Como desarrollador de aplicaciones web, uno de los temas que más me interesan es el de los intercambios de criptomonedas y cómo crearlos. De entrada, estas aplicaciones parecen ser herramientas para gestionar cuentas, convertir Bitcoin a una moneda fiduciaria como el USD y viceversa, y transferir Bitcoin a otras personas, pero ¿son más?

Vamos a ver algunos ejemplos con Node.js y la base de datos NoSQL, Couchbaseque trata temas relacionados con las bolsas de criptomonedas.

Actualizaciones sobre temas relacionados:

Descargo de responsabilidad: No soy un experto en criptodivisas, ni he participado en ningún desarrollo en torno a servicios financieros o intercambios. Soy un entusiasta de la materia y cualquier cosa obtenida de este artículo debe ser debidamente probado y utilizado bajo su propio riesgo.

Para llevar

Hay algunas cosas que obtendrás y que no obtendrás de este artículo en particular. Por ejemplo, vamos a empezar con las cosas que no obtendrá de este artículo:

  • No configuraremos ningún servicio bancario o de tarjeta de crédito para transferir monedas fiduciarias como el USD.
  • No emitiremos ninguna transacción firmada a la red Bitcoin, finalizando una transferencia.

Dicho esto, he aquí algunas cosas que puede esperar aprender en este artículo:

  • Crearemos un monedero determinista jerárquico (HD) que puede generar una cantidad ilimitada de claves para una semilla dada, cada una representando un monedero de usuario.
  • Crearemos cuentas de usuario cada una con monederos basados en la semilla maestra.
  • Crearemos transacciones que representen depósitos, retiradas y transferencias de fondos de la bolsa, sin trabajar realmente con una moneda fiduciaria.
  • Buscaremos saldos en la red Bitcoin.
  • Crearemos transacciones firmadas que se difundirán en la red Bitcoin.

Hay muchas cosas que veremos en este artículo que se pueden hacer mucho mejor. Si has encontrado algo que se pueda mejorar, compártelo en los comentarios. Como ya he dicho, no soy un experto en el tema, solo un aficionado.

Requisitos del proyecto

Hay algunos requisitos que deben cumplirse para tener éxito con este proyecto:

  • Debes tener Node.js 6+ instalado y configurado.
  • Debes tener Couchbase 5.1+ instalado y configurado con un Bucket y un perfil RBAC listo para funcionar.

El punto principal es que no voy a ir a través de cómo obtener Couchbase en marcha y funcionando. No es un proceso difícil, pero necesitarás un Bucket configurado con una cuenta de aplicación y un índice para realizar consultas con N1QL.

Creación de una aplicación Node.js con dependencias

Vamos a crear una nueva aplicación Node.js y descargar las dependencias antes de empezar a añadir cualquier lógica. Crea un directorio de proyecto en algún lugar de tu ordenador y ejecuta los siguientes comandos desde un CLI dentro de ese directorio:

Sé que podría haber hecho todas las instalaciones de dependencias en una sola línea, pero quería que fueran claras de leer. Entonces, ¿qué estamos haciendo en los comandos anteriores?

En primer lugar, vamos a inicializar un nuevo proyecto Node.js creando un archivo paquete.json archivo. A continuación, vamos a descargar nuestras dependencias y añadirlos a la paquete.json a través de --guardar bandera.

Para este ejemplo utilizaremos Express Framework. El sitio express, body-parsery joi son todos relevantes para aceptar y validar datos de petición. Dado que nos comunicaremos con nodos Bitcoin públicos, utilizaremos el paquete solicitar y solicitud-promesa paquete para envolver promesas. El muy popular bitcore-lib nos permitirá crear monederos y firmar transacciones mientras que el paquete bitcore-mnemónico nos permitirá generar una semilla que podremos utilizar para nuestras claves de monedero HD. Finalmente, couchbase y uuid se utilizará para trabajar con nuestra base de datos.

Ahora probablemente queramos estructurar mejor nuestro proyecto. Añade los siguientes directorios y archivos dentro del directorio de tu proyecto si no existen ya:

Todos nuestros puntos finales de API se dividirán en categorías y se colocarán en cada archivo de enrutamiento apropiado. No tenemos que hacer esto, pero hace que nuestro proyecto sea un poco más limpio. Para eliminar una tonelada de Bitcoin y la lógica de base de datos fuera de nuestras rutas, vamos a añadir todo lo que no es la validación de datos en nuestra clases/helper.js expediente. La dirección config.json tendrá toda la información de nuestra base de datos, así como nuestra semilla mnemotécnica. En un escenario realista, este archivo debería ser tratado como oro y recibir toda la protección posible. La dirección app.js tendrá toda nuestra configuración y lógica de arranque para conectar nuestras rutas, conectarse a la base de datos, etc.

Por comodidad, vamos a añadir una dependencia más a nuestro proyecto y configurarlo:

En nodemon nos permitirá recargar en caliente nuestro proyecto cada vez que cambiemos un archivo. No es un requisito, pero nos puede ahorrar algo de tiempo mientras estamos construyendo.

Abra el paquete.json y añada la siguiente secuencia de comandos:

En este punto podemos iniciar el proceso de desarrollo de nuestra aplicación.

Desarrollo de la base de datos y la lógica Bitcoin

A la hora de desarrollar nuestra aplicación, antes de empezar a preocuparnos por los puntos finales de la API, queremos crear nuestra base de datos y la lógica relacionada con Bitcoin.

Vamos a pasar nuestro tiempo en el proyecto de clases/helper.js . Ábralo e incluya lo siguiente:

Vamos a pasar esta clase como un singleton para nuestra aplicación. En el constructor establecemos una conexión con nuestro clúster de base de datos, abrimos un Bucket y nos autenticamos. El Bucket abierto se utilizará a lo largo de esta clase de ayuda.

Acabemos con la lógica de Bitcoin antes que la de la base de datos.

Si no estás familiarizado con los monederos HD, son esencialmente un monedero derivado de una única semilla. Usando la semilla puedes derivar hijos y esos hijos pueden tener hijos, y así sucesivamente.

En maestro variable en el createKeyPair representa la clave semilla de nivel superior. Cada cuenta de usuario será un hijo directo de esa clave, de ahí que estemos derivando un hijo basado en una función cuenta valor. En cuenta es un número de persona y cada cuenta creada obtendrá un número incremental. Sin embargo, no vamos a generar claves de cuenta y darlo por terminado. En su lugar, cada clave de cuenta tendrá 10.000 posibles claves privadas y públicas en caso de que no quieran usar la misma clave más de una vez. Una vez que hemos generado una clave al azar, la devolvemos.

Del mismo modo, tenemos un getMasterChangeAddress como la siguiente:

Cuando empecemos a crear cuentas, empezarán en uno, dejando cero para el intercambio o la aplicación web, o como quieras llamarlo. También estamos asignando 10 posibles direcciones a esta cuenta. Estas direcciones harán dos cosas posibles. La primera es que guardarán Bitcoin para transferir a otras cuentas y la segunda es que recibirán pagos restantes, también conocido como el cambio. Recuerde, en una transacción Bitcoin, todo el producto de la transacción no gastado (UTXO) debe ser gastado, incluso si es menor que la cantidad deseada. Esto significa que la cantidad deseada se envía al destino y el resto se devuelve a una de estas 10 direcciones.

¿Hay otras formas o formas mejores de hacerlo? Por supuesto, pero ésta servirá para el ejemplo.

Para obtener el saldo de cualquier dirección que utilicemos o generemos utilizando la semilla HD, podemos utilizar un explorador público de Bitcoin:

La función anterior tomará una dirección y obtendrá el saldo tanto en formato decimal como en satoshis. En adelante, el valor satoshi es el único valor relevante para nosotros. Si tenemos X número de direcciones para una cuenta dada, podemos obtener el saldo total utilizando una función como esta:

En el getWalletBalance función estamos haciendo una solicitud para cada dirección y cuando todos han completado, podemos añadir los saldos y devolverlos.

Se necesita algo más que el saldo de una dirección para poder transferir criptomoneda. En su lugar, necesitamos conocer la salida de transacción no gastada (UTXO) para una dirección dada. Esto se puede encontrar utilizando la misma API de BitPay:

Si no hay salida de transacción no gastada, significa que no hay nada que podamos transferir y deberíamos lanzar un error en su lugar. Tener suficiente para transferir es otra historia.

Por ejemplo, podríamos hacer algo así:

En la función anterior, estamos tomando una lista de direcciones y comprobando cuál de ellas tiene una cantidad superior al umbral que proporcionamos. Si ninguna de ellas tiene saldo suficiente, probablemente deberíamos transmitir ese mensaje.

La última función relacionada con la utilidad es algo que ya hemos visto:

La función anterior nos obtendrá todas las claves maestras, que serán útiles para firmar y comprobar el valor.

Sólo para reiterar, estoy usando un valor finito para el número de claves que se generan. Usted puede o no puede querer hacer lo mismo, depende de usted.

Ahora vamos a sumergirnos en algo de lógica NoSQL para almacenar los datos de nuestra aplicación.

En este momento no hay datos en nuestra base de datos. El primer paso lógico podría ser crear algunos datos. Aunque no es particularmente difícil de forma independiente, podemos crear una función como esta:

Esencialmente, estamos aceptando un objeto y un id para ser utilizado como una clave de documento. Si no se proporciona una clave de documento, la generaremos automáticamente. Cuando todo esté dicho y hecho, devolveremos lo que fue creado incluyendo el id en la respuesta.

Digamos que queremos crear una cuenta de usuario. Podemos hacer lo siguiente:

Recuerde, las cuentas son manejadas por un valor numérico auto incremental para este ejemplo. Podemos crear valores incrementales utilizando un contador en Couchbase. Si el contador no existe, lo inicializaremos a 1 y lo incrementaremos en cada siguiente llamada. Recuerda, 0 está reservado para claves de aplicación.

Después de obtener el valor de nuestro contador, lo añadimos al objeto pasado y llamamos a nuestra función de inserción, que en este caso genera un identificador único para nosotros.

Todavía no lo hemos visto porque no tenemos ningún endpoint, pero vamos a suponer que cuando creamos una cuenta, no tiene información de dirección, sólo un identificador de cuenta. Podríamos querer añadir una dirección para el usuario:

Al añadir una dirección, primero obtenemos el usuario por el id del documento. Cuando se recupera el documento, obtenemos el valor numérico de la cuenta y creamos un nuevo par de claves de nuestras 10.000 opciones. Utilizando un subdocumento podemos añadir el par de claves al documento de usuario sin tener que descargar el documento ni manipularlo.

Hay que tener en cuenta algo muy serio sobre lo que acabamos de hacer.

Estoy almacenando la clave privada sin cifrar y la dirección pública en el documento de usuario. Esto es un gran no-no para la producción. ¿Recuerdas todas esas historias que has leído sobre gente a la que le han robado sus claves? En realidad, querríamos cifrar los datos antes de insertarlos. Podemos hacerlo usando la librería crypto de Node.js, o si estamos usando Couchbase Server 5.5, el SDK de Node.js para Couchbase ofrece encriptación. Sin embargo, no lo exploraremos aquí.

Bien, ya tenemos los datos de las cuentas y las direcciones en la base de datos. Vamos a consultar esos datos:

Lo anterior getAddresses puede hacer una de dos cosas. Si se proporcionó una cuenta, utilizaremos una consulta N1QL para obtener todas las direcciones de esa cuenta en particular. Si no se proporcionó ninguna cuenta, obtendremos todas las direcciones de todas las cuentas de la base de datos. En ambos casos, sólo obtendremos las direcciones públicas, nada confidencial. Usando una consulta N1QL parametrizada, podemos devolver los resultados de la base de datos al cliente.

Algo a tener en cuenta en nuestra consulta.

Estamos almacenando nuestras direcciones en un array en los documentos de usuario. Utilizando un UNNEST podemos aplanar esas direcciones y hacer que la respuesta sea más atractiva.

Supongamos ahora que tenemos una dirección y queremos obtener la clave privada correspondiente. Podríamos hacer lo siguiente:

Dada una cuenta concreta, creamos una consulta similar a la que vimos anteriormente. Esta vez, después de UNNESThacemos un DONDE para obtener resultados sólo para la dirección coincidente. Si quisiéramos podríamos haber hecho una operación de array en su lugar. Con Couchbase y N1QL, hay muchas maneras de resolver un problema.

Vamos a cambiar un poco de marcha aquí. Hasta ahora hemos hecho operaciones orientadas a cuentas en nuestra base de datos NoSQL. Otro aspecto importante son las transacciones. Por ejemplo, tal vez el usuario X deposita algunos USD moneda para BTC y el usuario Y hace una retirada. Necesitamos almacenar y consultar la información de esa transacción.

Las funciones del punto final de la API guardarán los datos de la transacción, pero aún podremos consultarlos.

Dada una cuenta, queremos obtener el saldo de la cuenta de un usuario concreto.

Espera un segundo, demos un paso atrás porque ¿no habíamos creado ya algunas funciones de saldo de cuenta? Técnicamente sí, pero esas funciones eran para comprobar el saldo del monedero, no el saldo de la cuenta.

Aquí es donde parte de mi experiencia se convierte en zona gris. Cada vez que transfieres Bitcoin, hay una comisión implicada, y a veces es bastante cara. Cuando haces un depósito, no es rentable transferir dinero a tu monedero porque te cobrarían una tasa de minero. Luego se le cobraría por retirar e incluso transferir de nuevo. En ese momento ya ha perdido la mayor parte de su Bitcoin.

En su lugar, creo que las bolsas tienen una cuenta de haberes similar a una cuenta del mercado monetario bursátil. Hay un registro del dinero que deberías tener en tu cuenta, pero técnicamente no está en un monedero. Cuando quieres transferir, estás transfiriendo desde la dirección de la aplicación, no desde tu dirección de usuario. Cuando usted retira, sólo se está restando.

De nuevo, no sé si realmente funciona así, pero es como yo lo haría para evitar comisiones por doquier.

Volviendo a nuestro getAccountBalance función. Tomamos una suma de cada transacción. Los depósitos tienen un valor positivo, mientras que las transferencias y las retiradas tienen un valor negativo. La suma de esta información debería darte un número exacto, excluyendo el saldo de tu monedero. Más adelante obtendremos una cuenta con el saldo del monedero.

Dado lo poco que sabemos sobre los saldos de las cuentas, podemos intentar crear una transacción desde nuestro monedero:

Si se nos proporciona una dirección de origen, una dirección de destino y una cantidad, podemos crear y firmar una transacción que posteriormente se emitirá en la red Bitcoin.

Primero obtenemos el saldo de la dirección de origen en cuestión. Necesitamos asegurarnos de que tiene suficiente UTXO para cumplir con la expectativa de cantidad enviada. Tenga en cuenta que en este ejemplo, estamos haciendo transacciones de una sola dirección. Si quisieras complicarte, podrías enviar desde múltiples direcciones en una sola transacción. No vamos a hacer eso aquí. Si nuestra única dirección tiene fondos suficientes, obtenemos la clave privada para ella y los datos UTXO. Con los datos UTXO podemos crear una transacción Bitcoin, aplicar la dirección de destino y una dirección de cambio, luego firmar la transacción usando nuestra clave privada. La respuesta puede ser difundida.

Del mismo modo, supongamos que queremos transferir Bitcoin desde nuestra cuenta de haberes:

Suponemos que nuestras direcciones de intercambio se han cargado con una cantidad insana de Bitcoin para satisfacer la demanda.

El primer paso es asegurarnos de que tenemos fondos en nuestra cuenta de haberes. Podemos ejecutar esa consulta que suma cada una de nuestras transacciones para obtener un número válido. Si tenemos suficiente, podemos obtener nuestros 10 pares de claves maestras y las direcciones. Tenemos que comprobar qué dirección tiene fondos suficientes para enviar. Recuerde, las transacciones de una sola dirección aquí, cuando podría haber más.

Si una dirección tiene fondos suficientes, obtenemos los datos UTXO y comenzamos a realizar una transacción. Esta vez, en lugar de nuestra cartera como dirección de origen, utilizamos la cartera del intercambio. Después de obtener una transacción firmada, queremos crear una transacción en nuestra base de datos para restar el valor que estamos transfiriendo.

Antes de pasar a los puntos finales de la API, quiero reiterar algunas cosas:

  • Supongo que los intercambios populares tienen una cuenta de retención para evitar las tasas impuestas a las direcciones de los monederos.
  • En este ejemplo estamos utilizando transacciones de una sola dirección, en lugar de agregar las que tenemos.
  • No estoy cifrando los datos clave de los documentos de la cuenta, cuando debería hacerlo.
  • No estoy emitiendo ninguna transacción, sólo creándolas.

Ahora vamos a centrarnos en nuestros puntos finales de la API, la parte sencilla.

Diseño de puntos finales de API RESTful con Express Framework

Recuerde, como configuramos al principio, nuestros puntos finales se dividirán en tres archivos que actúan como agrupaciones. Empezaremos con el grupo más pequeño y simple de endpoints, que son más para utilidad que otra cosa.

Abra el archivo rutas/utilidad.js e incluya lo siguiente:

Aquí tenemos dos puntos finales, uno para generar semillas mnemónicas y el otro para obtener el valor fiat de un saldo Bitcoin. Ninguno de los dos son realmente necesarios, pero en el primer lanzamiento, podría ser bueno generar un valor semilla para guardarlo más tarde en nuestro archivo de configuración.

Ahora abra el proyecto rutas/cuenta.js para que podamos manejar la información de la cuenta:

Tenga en cuenta que estamos tirando de la clase de ayuda de la app.js que aún no hemos empezado. Sólo tienes que ir con él por ahora y tendrá sentido más tarde, aunque no es nada especial.

A la hora de crear cuentas, disponemos de lo siguiente:

Usando Joi podemos validar el cuerpo de la petición y lanzar errores si no es correcto. Asumiendo que el cuerpo de la petición es correcto, podemos llamar a nuestro crearCuenta para guardar una nueva cuenta en la base de datos.

Con una cuenta creada, podemos añadir algunas direcciones:

Utilizando el identificador de cuenta que se pasó, podemos llamar a nuestro addAddress para utilizar una operación de subdocumento en nuestro documento.

No está tan mal, ¿verdad?

Para obtener todas las direcciones de una cuenta en particular, podríamos tener algo como lo siguiente:

Alternativamente, si no proporcionamos un id, podemos obtener todas las direcciones de todas las cuentas utilizando la siguiente función endpoint:

Ahora probablemente la función más complicada. Digamos que queremos obtener el saldo de nuestra cuenta, que incluye la cuenta de explotación, así como cada una de nuestras direcciones de cartera. Podemos hacer lo siguiente:

Lo anterior llamará a nuestras dos funciones para obtener el balance, y sumará los resultados para obtener un balance masivo.

Los puntos finales de las cuentas no eran especialmente interesantes. La creación de transacciones es un poco más emocionante.

Abra el archivo rutas/transacción.js e incluya lo siguiente:

Tenemos tres tipos diferentes de transacción. Podemos depositar moneda fiduciaria por Bitcoin, retirar Bitcoin por moneda fiduciaria y transferir Bitcoin a nuevas direcciones de monedero.

Echemos un vistazo al punto final de depósito:

Después de validar la entrada, comprobamos el valor actual de Bitcoin en USD con CoinMarketCap. Utilizando los datos de la respuesta, podemos calcular cuántos Bitcoin deberíamos obtener en función de la cantidad depositada en USD.

Después de crear una transacción en la base de datos, podemos guardarla y, como es un número positivo, volverá como saldo positivo al consultarla.

Ahora digamos que queremos retirar dinero de nuestro Bitcoin:

Aquí ocurren eventos similares. Después de validar el cuerpo de la solicitud, obtenemos el saldo de nuestra cuenta y nos aseguramos de que la cantidad que estamos retirando es menor o igual a nuestro saldo. Si lo es, podemos hacer otra conversión basada en el precio actual de CoinMarketCap. Crearemos una transacción usando un valor negativo y la guardaremos en la base de datos.

En ambos casos estamos confiando en CoinMarketCap que ha tenido controversia negativa en el pasado. Es posible que desee elegir un recurso diferente para las conversiones.

Por último, tenemos las transferencias:

Si la solicitud contiene una dirección de origen, vamos a transferir desde nuestro propio monedero, de lo contrario vamos a transferir desde el monedero que gestiona la bolsa.

Todo esto se basa en funciones que habíamos creado previamente.

Con los puntos finales fuera del camino, podemos centrarnos en bootstrapping nuestra aplicación y llegar a una conclusión.

Arranque de la aplicación Express Framework

En este momento tenemos dos archivos que permanecen intactos por el ejemplo. No hemos añadido una configuración o lógica de manejo para arrancar nuestros endpoints.

Abra el archivo config.json e incluya algo como lo siguiente:

Recuerde que este archivo es increíblemente sensible. Considera bloquearlo o incluso utilizar un enfoque diferente. Si la semilla está expuesta, cada clave privada para todas las cuentas de usuario y la cuenta de intercambio se puede obtener con cero esfuerzo.

Ahora abra el proyecto app.js e incluya lo siguiente:

Lo que estamos haciendo es inicializar Express, cargar la información de configuración y enlazar nuestras rutas. La página módulo.exports.helper es nuestro singleton que se utilizará en todos los demás archivos JavaScript.

Conclusión

Acaba de ver cómo empezar a crear su propia bolsa de criptomonedas usando Node.js y Couchbase como base de datos NoSQL. Cubrimos mucho, desde la generación de carteras HD hasta la creación de endpoints con lógica de base de datos compleja.

Sin embargo, no me cansaré de repetirlo. Soy un entusiasta de la criptomoneda y no tengo experiencia real en el espacio financiero. Las cosas que he compartido deberían funcionar, pero se pueden hacer mucho mejor. No se olvide de cifrar sus claves y mantener su semilla segura. Pruebe su trabajo y sepa en lo que se está metiendo.

Si desea descargar este proyecto, consúltelo en GitHub. Si quieres compartir tu visión, experiencia, etc., sobre el tema, por favor, hazlo en los comentarios. La comunidad puede trabajar para crear algo grande.

Si eres un fan de Golang, he creado un proyecto similar en un tutorial anterior.

Autor

Publicado por Nic Raboy, Defensor del Desarrollador, Couchbase

Nic Raboy es un defensor de las tecnologías modernas de desarrollo web y móvil. Tiene experiencia en Java, JavaScript, Golang y una variedad de frameworks como Angular, NativeScript y Apache Cordova. Nic escribe sobre sus experiencias de desarrollo relacionadas con hacer el desarrollo web y móvil más fácil de entender.

3 Comentarios

  1. IMHO bitcoin exchange es una elección bastante sub-óptima para un artículo sobre Couchbase. Cualquier cosa que toque dinero necesita una "D" fiable en ACID (es decir, con 4 o más nueves como mínimo). No creo que Couchbase lo tenga (piensa en nodos muriendo y auto-failover ocurriendo y gente perdiendo dinero). Otro problema es la consistencia. El código de arriba tiene una carrera entre la comprobación del saldo y el ahorro de la transacción de retirada, lo que lo pone de manifiesto con bastante claridad.

    Otro problema es que comprobar el saldo es O(N) en número de transacciones de la cuenta. Las vistas materializadas de CouchDB pueden hacer la búsqueda del saldo de la cuenta en O(log N), pero por supuesto esto sigue siendo muy lento log N. AFAIK ningún índice niql es capaz de eso.

    IMHO este artículo termina destacando las debilidades de couchbase. Es decir, porque couchbase no es adecuado como backend de intercambio de bitcoin. Un artículo como este podría ser ideal para algo como CockroachDB o cloud spanner.

    1. No lo he demostrado en este artículo, pero puedes definir tus propios requisitos de durabilidad a través del SDK. Si te preocupa la consistencia y perder dinero, puedes cambiar la configuración para responder sólo después de que haya persistido en disco o sólo después de que se haya replicado X número de veces.

      https://developer.couchbase.com/documentation/server/current/sdk/durability.html

      A la hora de consultar, puedes establecer la coherencia de la consulta. Si quieres, puedes esperar a que se actualice el índice.

      https://developer.couchbase.com/documentation/server/current/indexes/performance-consistency.html

      Entiendo tu punto de vista, pero no creo que sea un problema tan grave como crees.

      Gracias por compartir tu opinión :-)

Dejar una respuesta