Há alguns meses, venho acompanhando assuntos relacionados a criptomoedas, como o Bitcoin, e estou muito fascinado com tudo o que está acontecendo.
Como desenvolvedor de aplicativos da Web, um tópico sobre o qual estou particularmente interessado em aprender mais é sobre trocas de criptomoedas e como criá-las. De início, esses aplicativos parecem ser ferramentas para gerenciar contas, converter Bitcoin em moeda fiduciária, como dólar americano, e vice-versa, e transferir Bitcoin para outras pessoas, mas será que são mais do que isso?
Veremos alguns exemplos com o Node.js e o banco de dados NoSQL, Couchbaseque abrange tópicos modelados em torno de trocas de criptomoedas.
Atualizações sobre tópicos relacionados:
- A sinergia entre blockchain e bancos de dados NoSQL
- Transações ACID multidocumento distribuídas do Couchbase
Isenção de responsabilidade: Não sou especialista em criptomoedas nem participei de nenhum desenvolvimento relacionado a serviços financeiros ou bolsas de valores. Sou um entusiasta do assunto e tudo o que for obtido neste artigo deve ser devidamente testado e usado por sua própria conta e risco.
O que levar
Há algumas coisas que você obterá e não obterá com este artigo específico. Por exemplo, vamos começar com as coisas que você não obterá com este artigo:
- Não configuraremos nenhum serviço bancário ou de cartão de crédito para transferir moedas fiduciárias, como o dólar americano.
- Não estaremos transmitindo nenhuma transação assinada para a rede Bitcoin, finalizando uma transferência.
Dito isso, aqui estão algumas coisas que você pode esperar para aprender neste artigo:
- Criaremos uma carteira hierárquica determinística (HD) que pode gerar uma quantidade ilimitada de chaves para uma determinada semente, cada uma representando uma carteira de usuário.
- Criaremos contas de usuário, cada uma com carteiras baseadas na semente principal.
- Criaremos transações que representam depósitos, saques e transferências de fundos da bolsa, sem trabalhar de fato com uma moeda fiduciária.
- Vamos consultar os saldos da rede Bitcoin.
- Criaremos transações assinadas para serem transmitidas na rede Bitcoin.
Há muitas coisas que veremos neste artigo que podem ser feitas de forma muito melhor. Se você encontrou algo que poderia ser melhorado, compartilhe nos comentários. Como eu disse, não sou um especialista no assunto, apenas um fã.
Os requisitos do projeto
Existem alguns requisitos que devem ser atendidos para que esse projeto seja bem-sucedido:
- Você deve ter o Node.js 6+ instalado e configurado.
- Você deve ter o Couchbase 5.1+ instalado e configurado com um perfil de Bucket e RBAC pronto para uso.
O ponto principal é que não vou explicar como colocar o Couchbase em funcionamento. Não é um processo difícil, mas você precisará de um Bucket configurado com uma conta de aplicativo e um índice para fazer consultas com o N1QL.
Criação de um aplicativo Node.js com dependências
Vamos criar um novo aplicativo Node.js e fazer o download das dependências antes de começarmos a adicionar qualquer lógica. Crie um diretório de projeto em algum lugar do seu computador e execute os seguintes comandos de uma CLI dentro desse diretório:
|
1 2 3 4 5 6 7 8 9 |
npm inicial -y npm instalar couchbase --salvar npm instalar expresso --salvar npm instalar corpo-analisador --salvar npm instalar joi --salvar npm instalar solicitação solicitação-promessa --salvar npm instalar uuid --salvar npm instalar núcleo de bits-lib --salvar npm instalar núcleo de bits-mnemônico --salvar |
Sei que poderia ter feito todas as instalações de dependências em uma única linha, mas queria deixá-las claras para a leitura. Então, o que estamos fazendo nos comandos acima?
Primeiro, estamos inicializando um novo projeto Node.js criando um arquivo package.json arquivo. Em seguida, estamos baixando nossas dependências e adicionando-as ao arquivo package.json por meio do arquivo -salvar bandeira.
Para este exemplo, usaremos o Express Framework. O expresso, analisador de corpoe joi são todos relevantes para aceitar e validar os dados da solicitação. Como estaremos nos comunicando com nós públicos de Bitcoin, usaremos o pacote solicitação e promessa de solicitação pacote de embalagem de promessa. O muito popular bitcore-lib nos permitirá criar carteiras e assinar transações, enquanto o pacote bitcore-mnemônico nos permitirá gerar uma semente que pode ser usada para nossas chaves de carteira HD. Por fim, couchbase e uuid será usado para trabalhar com nosso banco de dados.
Agora, provavelmente queremos estruturar melhor nosso projeto. Adicione os seguintes diretórios e arquivos ao diretório do projeto, caso eles ainda não existam:
|
1 2 3 4 5 6 7 8 9 |
pacote.json configuração.json aplicativo.js rotas conta.js transação.js utilitário.js aulas ajudante.js |
Todos os nossos endpoints de API serão divididos em categorias e colocados em cada arquivo de roteamento apropriado. Não é necessário fazer isso, mas torna nosso projeto um pouco mais limpo. Para remover uma tonelada de Bitcoin e lógica de banco de dados de nossas rotas, adicionaremos tudo o que não for validação de dados em nosso arquivo classes/helper.js arquivo. O arquivo config.json terá todas as informações do nosso banco de dados, bem como nossa semente mnemônica. Em um cenário realista, esse arquivo deve ser tratado como ouro e receber o máximo de proteção possível. O arquivo app.js terá toda a nossa configuração e lógica de inicialização para conectar nossas rotas, conectar-se ao banco de dados, etc.
Por conveniência, vamos adicionar mais uma dependência ao nosso projeto e configurá-la:
|
1 |
npm instalar nodemônio --salvar-dev |
O nodemônio nos permitirá fazer o hot-reload do nosso projeto sempre que alterarmos um arquivo. Isso não é um requisito, mas pode nos poupar algum tempo durante a construção.
Abra o package.json e adicione o seguinte script para que isso aconteça:
|
1 2 3 4 5 6 |
... "scripts": { "teste": "echo \"Error: no test specified\" && exit 1", "start": "./node_modules/nodemon/bin/nodemon.js app.js" }, ... |
Neste ponto, podemos iniciar o processo de desenvolvimento do nosso aplicativo.
Desenvolvimento do banco de dados e da lógica do Bitcoin
Quando se trata de desenvolver nosso aplicativo, antes de começarmos a nos preocupar com os pontos de extremidade da API, queremos criar nosso banco de dados e a lógica relacionada ao Bitcoin.
Passaremos nosso tempo na seção classes/helper.js arquivo. Abra-o e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
const Couchbase = exigir("couchbase"); const Solicitação = exigir("request-promise"); const UUID = exigir("uuid"); const Bitcore = exigir("bitcore-lib"); classe Ajudante { construtor(hospedeiro, balde, nome de usuário, senha, semente) { este.agrupamento = novo Couchbase.Aglomerado("couchbase://" + hospedeiro); este.agrupamento.autenticar(nome de usuário, senha); este.balde = este.agrupamento.openBucket(balde); este.mestre = semente; } createKeyPair(conta) { } getWalletBalance(endereços) { } getAddressBalance(endereço) { } getAddressUtxo(endereço) { } inserir(dados, id = UUID.v4()) { } createAccount(dados) { } addAddress(conta) { } getAccountBalance(conta) { } getMasterAddresses() { } getMasterKeyPairs() { } getMasterAddressWithMinimum(endereços, quantidade) { } getMasterChangeAddress() { } getAddresses(conta) { } getPrivateKeyFromAddress(conta, endereço) { } createTransactionFromAccount(conta, fonte, destino, quantidade) { } createTransactionFromMaster(conta, destino, quantidade) { } } módulo.exportações = Ajudante; |
Vamos passar essa classe como um singleton para o nosso aplicativo. Na classe construtor estamos estabelecendo uma conexão com nosso cluster de banco de dados, abrindo um Bucket e autenticando. O Bucket aberto será usado em toda esta classe auxiliar.
Vamos eliminar a lógica do Bitcoin antes da lógica do banco de dados.
Se você não estiver familiarizado com as carteiras HD, elas são basicamente uma carteira derivada de uma única semente. Usando a semente, você pode derivar filhos e esses filhos podem ter filhos, e assim por diante.
|
1 2 3 4 5 |
createKeyPair(conta) { var conta = este.mestre.deriveChild(conta); var chave = conta.deriveChild(Matemática.aleatório() * 10000 + 1); retorno { "secret" (secreto): chave.privateKey.paraWIF().toString(), "endereço": chave.privateKey.toAddress().toString() } } |
O mestre na variável createKeyPair representa a chave semente de nível superior. Cada conta de usuário será um filho direto dessa chave, portanto, estamos derivando um filho com base em uma conta valor. O conta é um número pessoal e cada conta criada receberá um número incremental. No entanto, não vamos gerar chaves de conta e encerrar o assunto. Em vez disso, cada chave de conta terá 10.000 chaves públicas e privadas possíveis, caso não queiram usar a mesma chave mais de uma vez. Depois de gerarmos uma chave aleatoriamente, nós a retornamos.
Da mesma forma, temos um getMasterChangeAddress como a seguinte:
|
1 2 3 4 5 |
getMasterChangeAddress() { var conta = este.mestre.deriveChild(0); var chave = conta.deriveChild(Matemática.aleatório() * 10 + 1); retorno { "secret" (secreto): chave.privateKey.paraWIF().toString(), "endereço": chave.privateKey.toAddress().toString() } } |
Quando começarmos a criar contas, elas começarão com um, deixando zero para a troca ou o aplicativo da Web, ou como você quiser chamá-lo. Também estamos alocando 10 endereços possíveis para essa conta. Esses endereços terão duas funções possíveis. A primeira é que eles manterão Bitcoin para transferir para outras contas e a segunda é que eles receberão pagamentos restantes, também conhecidos como troco. Lembre-se, em uma transação de Bitcoin, toda a saída de transação não gasta (UTXO) deve ser gasta, mesmo que seja menor que o valor desejado. Isso significa que o valor desejado é enviado para o destino e o restante é enviado de volta para um desses 10 endereços.
Existem outras maneiras ou maneiras melhores de fazer isso? Com certeza, mas esta funcionará para o exemplo.
Para obter um saldo para qualquer endereço que usamos ou geramos usando a semente HD, podemos usar um Bitcoin explorer público:
|
1 2 3 |
getAddressBalance(endereço) { retorno Solicitação("https://insight.bitpay.com/api/addr/" + endereço); } |
A função acima pegará um endereço e obterá o saldo em formato decimal e também em satoshis. Daqui para frente, o valor satoshi é o único valor relevante para nós. Se tivermos um número X de endereços para uma determinada conta, poderemos obter o saldo total usando uma função como esta:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
getWalletBalance(endereços) { var promessas = []; para(var i = 0; i < endereços.comprimento; i++) { promessas.empurrar(Solicitação("https://insight.bitpay.com/api/addr/" + endereços[i])); } retorno Promessa.todos(promessas).então(resultado => { var equilíbrio = resultado.reduzir((a, b) => a + JSON.analisar(b).balanceSat, 0); retorno novo Promessa((resolver, rejeitar) => { resolver({ "saldo": equilíbrio }); }); }); } |
No exemplo acima getWalletBalance função, estamos fazendo uma solicitação para cada endereço e, quando todos tiverem sido concluídos, poderemos adicionar os saldos e devolvê-los.
É preciso um pouco mais do que apenas um saldo de endereço para poder transferir criptomoedas. Em vez disso, precisamos saber a saída de transação não gasta (UTXO) para um determinado endereço. Isso pode ser encontrado usando a mesma API do BitPay:
|
1 2 3 4 5 6 7 8 9 10 |
getAddressUtxo(endereço) { retorno Solicitação("https://insight.bitpay.com/api/addr/" + endereço + "/utxo").então(utxo => { retorno novo Promessa((resolver, rejeitar) => { se(JSON.analisar(utxo).comprimento == 0) { rejeitar({ "mensagem": "Não há transações não gastas disponíveis." }); } resolver(JSON.analisar(utxo)); }); }); } |
Se não houver saída de transação não gasta, isso significa que não há nada que possa ser transferido e, em vez disso, devemos lançar um erro. Ter o suficiente para transferir é uma história diferente.
Por exemplo, poderíamos fazer algo assim:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
getMasterAddressWithMinimum(endereços, quantidade) { var promessas = []; para(var i = 0; i < endereços.comprimento; i++) { promessas.empurrar(Solicitação("https://insight.bitpay.com/api/addr/" + endereços[i])); } retorno Promessa.todos(promessas).então(resultado => { para(var i = 0; i < resultado.comprimento; i++) { se(resultado[i].balanceSat >= quantidade) { retorno resolver({ "endereço": resultado[i].addrStr }); } } rejeitar({ "mensagem": "Não há fundos suficientes em troca" }); }); } |
Na função acima, estamos pegando uma lista de endereços e verificando qual deles possui um valor maior do que o limite que fornecemos. Se nenhum deles tiver saldo suficiente, provavelmente deveremos retransmitir essa mensagem.
A função final relacionada à utilidade é algo que já vimos:
|
1 2 3 4 5 6 7 8 9 10 |
getMasterKeyPairs() { var Pares de chaves = []; var chave; var conta = este.mestre.deriveChild(0); para(var i = 1; i <= 10; i++) { chave = conta.deriveChild(i); Pares de chaves.empurrar({ "secret" (secreto): chave.privateKey.paraWIF().toString(), "endereço": chave.privateKey.toAddress().toString() }); } retorno Pares de chaves; } |
A função acima nos fornecerá todas as chaves mestras, que serão úteis para assinar e verificar o valor.
Apenas para reiterar, estou usando um valor finito para quantas chaves são geradas. Você pode ou não querer fazer o mesmo, fica a seu critério.
Agora vamos nos aprofundar em uma lógica NoSQL para armazenar os dados do nosso aplicativo.
No momento, não há dados em nosso banco de dados. A primeira etapa lógica pode ser a criação de alguns dados. Embora isso não seja particularmente difícil, podemos criar uma função como esta:
|
1 2 3 4 5 6 7 8 9 10 11 |
inserir(dados, id = UUID.v4()) { retorno novo Promessa((resolver, rejeitar) => { este.balde.inserir(id, dados, (erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } dados.id = id; resolver(dados); }); }); } |
Basicamente, estamos aceitando um objeto e um ID para ser usado como chave de documento. Se uma chave de documento não for fornecida, nós a geraremos automaticamente. Quando tudo estiver pronto, retornaremos o que foi criado, incluindo o ID na resposta.
Então, digamos que queremos criar uma conta de usuário. Podemos fazer o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
createAccount(dados) { retorno novo Promessa((resolver, rejeitar) => { este.balde.contador("accounts::total", 1, { "inicial": 1 }, (erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } dados.conta = resultado.valor; este.inserir(dados).então(resultado => { resolver(resultado); }, erro => { rejeitar(erro); }); }); }); } |
Lembre-se de que, neste exemplo, as contas são acionadas por um valor numérico de incremento automático. Podemos criar valores incrementais usando um contador no Couchbase. Se o contador não existir, nós o inicializaremos em 1 e o incrementaremos a cada chamada seguinte. Lembre-se de que 0 é reservado para as chaves do aplicativo.
Depois de obtermos nosso valor de contador, nós o adicionamos ao objeto que foi passado e chamamos nossa função de inserção, que, nesse caso, gera um ID exclusivo para nós.
Ainda não vimos isso porque não temos nenhum ponto de extremidade, mas vamos supor que, quando criamos uma conta, ela não tenha informações de endereço, apenas um identificador de conta. Talvez queiramos adicionar um endereço para o usuário:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
addAddress(conta) { retorno novo Promessa((resolver, rejeitar) => { este.balde.obter(conta, (erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } var par de chaves = este.createKeyPair(resultado.valor.conta); este.balde.mutateIn(conta).arrayAppend("endereços", par de chaves, verdadeiro).executar((erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } resolver({ "endereço": par de chaves.endereço }); }); }); }); } |
Ao adicionar um endereço, primeiro obtemos o usuário pelo ID do documento. Quando o documento é recuperado, obtemos o valor numérico da conta e criamos um novo par de chaves de nossas 10.000 opções. Usando um subdocumento podemos adicionar o par de chaves ao documento do usuário sem precisar fazer download do documento ou manipulá-lo.
Há algo muito sério a ser observado sobre o que acabamos de fazer.
Estou armazenando a chave privada não criptografada e o endereço público no documento do usuário. Isso é muito ruim para a produção. Lembra-se de todas aquelas histórias que você leu sobre pessoas que tiveram suas chaves roubadas? Na realidade, gostaríamos de criptografar os dados antes de inseri-los. Podemos fazer isso usando a biblioteca de criptografia do Node.js ou, se estivermos usando o Couchbase Server 5.5, o SDK do Node.js para o Couchbase oferece criptografia. Mas não vamos explorar isso aqui.
Pronto, agora temos dados de conta e endereços no banco de dados. Vamos consultar esses dados:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
getAddresses(conta) { var declaração, parâmetros; se(conta) { declaração = "SELECT VALUE addresses.address FROM " + este.balde._nome + " AS account USE KEYS $id UNNEST account.addresses as addresses"; parâmetros = { "id": conta }; } mais { declaração = "SELECT VALUE addresses.address FROM " + este.balde._nome + " AS account UNNEST account.addresses as addresses WHERE account.type = 'account'"; } var consulta = Couchbase.N1qlQuery.fromString(declaração); retorno novo Promessa((resolver, rejeitar) => { este.balde.consulta(consulta, parâmetros, (erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } resolver(resultado); }); }); } |
O acima getAddresses pode fazer uma de duas coisas. Se uma conta foi fornecida, usaremos uma consulta N1QL para obter todos os endereços dessa conta específica. Se nenhuma conta foi fornecida, obteremos todos os endereços de todas as contas no banco de dados. Em ambos os cenários, estamos obtendo apenas endereços públicos, nada sensível. Usando uma consulta N1QL parametrizada, podemos retornar os resultados do banco de dados para o cliente.
Algo a ser observado em nossa consulta.
Estamos armazenando nossos endereços em uma matriz nos documentos do usuário. Usando um INÚTIL podemos achatar esses endereços e tornar a resposta mais atraente.
Agora, digamos que temos um endereço e queremos obter a chave privada correspondente. Podemos fazer o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 |
getPrivateKeyFromAddress(conta, endereço) { var declaração = "SELECT VALUE keypairs.secret FROM " + este.balde._nome + " AS account USE KEYS $account UNNEST account.addresses AS keypairs WHERE keypairs.address = $address"; var consulta = Couchbase.N1qlQuery.fromString(declaração); retorno novo Promessa((resolver, rejeitar) => { este.balde.consulta(consulta, { "account" (conta): conta, "endereço": endereço }, (erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } resolver({ "secret" (secreto): resultado[0] }); }); }); } |
Dada uma determinada conta, criamos uma consulta semelhante à que vimos anteriormente. Desta vez, depois de INÚTIL, fazemos um ONDE para nos dar resultados apenas para o endereço correspondente. Se quiséssemos, poderíamos ter feito uma operação de matriz. Com o Couchbase e o N1QL, há várias maneiras de resolver um problema.
Vamos mudar um pouco de marcha aqui. Até agora, fizemos operações orientadas a contas em nosso banco de dados NoSQL. Outro aspecto importante são as transações. Por exemplo, talvez o usuário X deposite alguns dólares americanos em BTC e o usuário Y faça um saque. Precisamos armazenar e consultar essas informações de transação.
As funções do ponto de extremidade da API salvarão os dados da transação, mas ainda podemos consultá-los.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
getAccountBalance(conta) { var declaração = "SELECT SUM(tx.satoshis) AS balance FROM " + este.balde._nome + " AS tx WHERE tx.type = 'transaction' AND tx.account = $account"; var consulta = Couchbase.N1qlQuery.fromString(declaração); retorno novo Promessa((resolver, rejeitar) => { este.balde.consulta(consulta, { "account" (conta): conta }, (erro, resultado) => { se(erro) { rejeitar({ "código": erro.código, "mensagem": erro.mensagem }); } resolver({ "saldo": resultado[0].equilíbrio }); }); }); } |
Dada uma conta, queremos obter o saldo da conta de um determinado usuário.
Espere um pouco, vamos dar um passo atrás, pois já não criamos algumas funções de saldo de conta? Tecnicamente sim, mas essas funções eram para verificar o saldo da carteira, não o saldo da conta.
É aqui que parte de minha experiência se transforma em uma área cinzenta. Toda vez que você transfere Bitcoin, há uma taxa envolvida e, às vezes, ela é bastante cara. Quando você faz um depósito, não é econômico transferir dinheiro para sua carteira porque seria cobrada uma taxa de mineração. Em seguida, você seria cobrado para sacar e até mesmo para transferir novamente. Nesse ponto, você já perdeu a maior parte do seu Bitcoin.
Em vez disso, acredito que as bolsas têm uma conta de retenção semelhante a uma conta do mercado monetário da bolsa de valores. Há um registro do dinheiro que você deveria ter em sua conta, mas ele não está tecnicamente em uma carteira. Quando você quer transferir, está transferindo do endereço do aplicativo, não do seu endereço de usuário. Quando você retira, o dinheiro está apenas sendo subtraído.
Novamente, não sei se é realmente assim que funciona, mas é assim que eu faria para evitar taxas em todos os lugares.
Voltando ao nosso getAccountBalance função. Estamos obtendo uma soma de todas as transações. Os depósitos têm um valor positivo, enquanto as transferências e as retiradas têm um valor negativo. A agregação dessas informações deve lhe dar um número preciso, excluindo o saldo da carteira. Mais tarde, obteremos uma conta com o saldo da carteira.
Considerando o pouco que sabemos sobre os saldos das contas, podemos tentar criar uma transação a partir de nossa carteira:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
createTransactionFromAccount(conta, fonte, destino, quantidade) { retorno novo Promessa((resolver, rejeitar) => { este.getAddressBalance(fonte).então(sourceAddress => { se(sourceAddress.balanceSat < quantidade) { retorno rejeitar({ "mensagem": "Não há fundos suficientes na conta." }); } este.getPrivateKeyFromAddress(conta, fonte).então(par de chaves => { este.getAddressUtxo(fonte).então(utxo => { var transação = novo Bitcore.Transação(); para(var i = 0; i < utxo.comprimento; i++) { transação.de(utxo[i]); } transação.para(destino, quantidade); este.addAddress(conta).então(mudança => { transação.mudança(mudança.endereço); transação.sinal(par de chaves.segredo); resolver(transação); }, erro => rejeitar(erro)); }, erro => rejeitar(erro)); }, erro => rejeitar(erro)); }, erro => rejeitar(erro)); }); } |
Se fornecermos um endereço de origem, um endereço de destino e um valor, poderemos criar e assinar uma transação a ser transmitida posteriormente na rede Bitcoin.
Primeiro, obtemos o saldo do endereço de origem em questão. Precisamos ter certeza de que ele tem UTXO suficiente para atender à expectativa do valor de envio. Observe que, neste exemplo, estamos fazendo transações de endereço único. Se você quisesse complicar, poderia enviar de vários endereços em uma única transação. Não faremos isso aqui. Se nosso único endereço tiver fundos suficientes, obteremos a chave privada para ele e os dados UTXO. Com os dados do UTXO, podemos criar uma transação de Bitcoin, aplicar o endereço de destino e um endereço de alteração e, em seguida, assinar a transação usando nossa chave privada. A resposta pode ser transmitida.
Da mesma forma, digamos que queremos transferir Bitcoin de nossa conta de depósito:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
createTransactionFromMaster(conta, destino, quantidade) { retorno novo Promessa((resolver, rejeitar) => { este.getAccountBalance(conta).então(accountBalance => { se(accountBalance.equilíbrio < quantidade) { rejeitar({ "mensagem": "Não há fundos suficientes na conta." }); } var mKeyPairs = este.getMasterKeyPairs(); var masterAddresses = mKeyPairs.mapa(a => a.endereço); este.getMasterAddressWithMinimum(masterAddresses, quantidade).então(fundos => { este.getAddressUtxo(fundos.endereço).então(utxo => { var transação = novo Bitcore.Transação(); para(var i = 0; i < utxo.comprimento; i++) { transação.de(utxo[i]); } transação.para(destino, quantidade); var mudança = ajudante.getMasterChangeAddress(); transação.mudança(mudança.endereço); para(var j = 0; j < mKeyPairs.comprimento; j ++) { se(mKeyPairs[j].endereço == fundos.endereço) { transação.sinal(mKeyPairs[j].segredo); } } var tx = { conta: conta, satoshis: (valor * -1), carimbo de data/hora: (novo Data()).getTime(), status: "transferência", tipo: "transação" }; este.inserir(tx).então(resultado => { resolver(transação); }, erro => rejeitar(erro)); }, erro => rejeitar(erro)); }, erro => rejeitar(erro)); }, erro => rejeitar(erro)); }); } |
Estamos supondo que nossos endereços de troca foram carregados com uma quantidade insana de Bitcoin para atender à demanda.
A primeira etapa é garantir que tenhamos fundos em nossa conta de depósito. Podemos executar a consulta que soma cada uma de nossas transações para obter um número válido. Se tivermos o suficiente, poderemos obter todos os 10 pares de chaves mestras e os endereços. Precisamos verificar qual endereço tem fundos suficientes para enviar. Lembre-se de que, aqui, há transações de um único endereço, quando poderia haver mais.
Se um endereço tiver fundos suficientes, obteremos os dados UTXO e começaremos a fazer uma transação. Desta vez, em vez de nossa carteira como endereço de origem, usamos a carteira da bolsa. Depois de obtermos uma transação assinada, queremos criar uma transação em nosso banco de dados para subtrair o valor que estamos transferindo.
Antes de passarmos para os pontos de extremidade da API, quero reiterar alguns pontos:
- Presumo que as bolsas populares tenham uma conta de retenção para evitar as taxas impostas aos endereços de carteira.
- Estamos usando transações de endereço único neste exemplo, em vez de agregar o que temos.
- Não estou criptografando os dados-chave nos documentos da conta, quando deveria estar.
- Não estou transmitindo nenhuma transação, apenas criando-as.
Agora vamos nos concentrar em nossos endpoints de API, a parte simples.
Projetando pontos de extremidade de API RESTful com a estrutura Express
Lembre-se de que, conforme configuramos no início, nossos pontos de extremidade serão divididos em três arquivos que funcionam como agrupamentos. Começaremos com o menor e mais simples grupo de pontos de extremidade, que são mais utilitários do que qualquer outra coisa.
Abra o arquivo routes/utility.js e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const Bitcore = exigir("bitcore-lib"); const Mnemônico = exigir("bitcore-mnemônico"); módulo.exportações = (aplicativo) => { aplicativo.obter("/mnemônico", (solicitação, resposta) => { resposta.enviar({ "mnemônico": (novo Mnemônico(Mnemônico.Palavras.INGLÊS)).toString() }); }); aplicativo.obter("/balance/value", (solicitação, resposta) => { Solicitação("https://api.coinmarketcap.com/v1/ticker/bitcoin/").então(mercado => { resposta.enviar({ "valor": "$" + (JSON.analisar(mercado)[0].price_usd * solicitação.consulta.equilíbrio).toFixed(2) }); }, erro => { resposta.status(500).enviar(erro); }); }); } |
Aqui temos dois pontos de extremidade, um para gerar sementes mnemônicas e o outro para obter o valor fiduciário de um saldo de Bitcoin. Nenhum deles é realmente necessário, mas no primeiro lançamento, pode ser bom gerar um valor de semente para salvar posteriormente em nosso arquivo de configuração.
Agora, abra o arquivo routes/account.js para que possamos lidar com as informações da conta:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const Solicitação = exigir("request-promise"); const Joi = exigir("joi"); const ajudante = exigir("../app").ajudante; módulo.exportações = (aplicativo) => { aplicativo.postagem("/account", (solicitação, resposta) => { }); aplicativo.colocar("/account/address/:id", (solicitação, resposta) => { }); aplicativo.obter("/account/addresses/:id", (solicitação, resposta) => { }); aplicativo.obter("/addresses", (solicitação, resposta) => { }); aplicativo.obter("/account/balance/:id", (solicitação, resposta) => { }); aplicativo.obter("/address/balance/:id", (solicitação, resposta) => { }); } |
Observe que estamos extraindo a classe auxiliar da classe app.js que ainda não começamos. Por enquanto, basta usá-lo e ele fará sentido mais tarde, embora não seja nada de especial.
Quando se trata de criar contas, temos as seguintes opções:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
aplicativo.postagem("/account", (solicitação, resposta) => { var modelo = Joi.objeto().chaves({ primeiro nome: Joi.string().necessário(), sobrenome: Joi.string().necessário(), tipo: Joi.string().proibido().padrão("account" (conta)) }); Joi.validar(solicitação.corpo, modelo, { stripUnknown: verdadeiro }, (erro, valor) => { se(erro) { retorno resposta.status(500).enviar(erro); } ajudante.createAccount(valor).então(resultado => { resposta.enviar(valor); }, erro => { resposta.status(500).enviar(erro); }); }); }); |
Usando o Joi, podemos validar o corpo da solicitação e gerar erros se ele não estiver correto. Supondo que o corpo da solicitação esteja correto, podemos chamar nosso createAccount para salvar uma nova conta no banco de dados.
Com uma conta criada, podemos adicionar alguns endereços:
|
1 2 3 4 5 6 7 |
aplicativo.colocar("/account/address/:id", (solicitação, resposta) => { ajudante.addAddress(solicitação.parâmetros.id).então(resultado => { resposta.enviar(resultado); }, erro => { retorno resposta.status(500).enviar(erro); }); }); |
Usando o ID da conta que foi passado, podemos chamar nosso addAddress para usar uma operação de subdocumento em nosso documento.
Não é tão ruim, certo?
Para obter todos os endereços de uma determinada conta, podemos ter algo como o seguinte:
|
1 2 3 4 5 6 7 |
aplicativo.obter("/account/addresses/:id", (solicitação, resposta) => { ajudante.getAddresses(solicitação.parâmetros.id).então(resultado => { resposta.enviar(resultado); }, erro => { resposta.status(500).enviar(erro); }); }); |
Como alternativa, se não fornecermos um ID, poderemos obter todos os endereços de todas as contas usando a seguinte função de ponto de extremidade:
|
1 2 3 4 5 6 7 |
aplicativo.obter("/addresses", (solicitação, resposta) => { ajudante.getAddresses().então(resultado => { resposta.enviar(resultado); }, erro => { resposta.status(500).enviar(erro); }); }); |
Agora, provavelmente a função de endpoint mais complicada. Digamos que queiramos obter o saldo de nossa conta, que inclui a conta de depósito, bem como cada um dos endereços de nossa carteira. Podemos fazer o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 |
aplicativo.obter("/account/balance/:id", (solicitação, resposta) => { ajudante.getAddresses(solicitação.parâmetros.id).então(endereços => ajudante.getWalletBalance(endereços)).então(equilíbrio => { ajudante.getAccountBalance(solicitação.parâmetros.id).então(resultado => { resposta.enviar({ "saldo": equilíbrio.equilíbrio + resultado.equilíbrio }); }, erro => { resposta.status(500).enviar({ "código": erro.código, "mensagem": erro.mensagem }); }); }, erro => { resposta.status(500).enviar({ "código": erro.código, "mensagem": erro.mensagem }); }); }); |
O procedimento acima chamará ambas as funções para obter o saldo e adicionará os resultados para obter um saldo enorme.
Os pontos de extremidade da conta não eram particularmente interessantes. A criação de transações é um pouco mais interessante.
Abra o arquivo routes/transaction.js e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const Solicitação = exigir("request-promise"); const Joi = exigir("joi"); const Bitcore = exigir("bitcore-lib"); const ajudante = exigir("../app").ajudante; módulo.exportações = (aplicativo) => { aplicativo.postagem("/withdraw", (solicitação, resposta) => { }); aplicativo.postagem("/deposit", (solicitação, resposta) => { }); aplicativo.postagem("/transferência", (solicitação, resposta) => { }); } |
Temos três tipos diferentes de transação. Podemos depositar moeda fiduciária para Bitcoin, sacar Bitcoin para moeda fiduciária e transferir Bitcoin para novos endereços de carteira.
Vamos dar uma olhada no endpoint de depósito:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
aplicativo.postagem("/deposit", (solicitação, resposta) => { var modelo = Joi.objeto().chaves({ usd: Joi.número().necessário(), id: Joi.string().necessário() }); Joi.validar(solicitação.corpo, modelo, { stripUnknown: verdadeiro }, (erro, valor) => { se(erro) { retorno resposta.status(500).enviar(erro); } Solicitação("https://api.coinmarketcap.com/v1/ticker/bitcoin/").então(mercado => { var btc = valor.usd / JSON.analisar(mercado)[0].preço_usd; var transação = { conta: valor.id, usd: valor.usd, satoshis: Bitcore.Unidade.doBTC(btc).paraSatoshis(), carimbo de data/hora: (novo Data()).getTime(), status: "depósito", tipo: "transação" }; ajudante.inserir(transação).então(resultado => { resposta.enviar(resultado); }, erro => { resposta.status(500).enviar(erro); }); }, erro => { resposta.status(500).enviar(erro); }); }); }); |
Depois de validarmos a entrada, verificamos o valor atual do Bitcoin em USD com o CoinMarketCap. Usando os dados na resposta, podemos calcular quantos Bitcoins devemos obter com base no valor em USD depositado.
Depois de criar uma transação de banco de dados, podemos salvá-la e, como se trata de um número positivo, ela retornará como saldo positivo ao ser consultada.
Agora, digamos que queiramos sacar dinheiro de nosso Bitcoin:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
aplicativo.postagem("/withdraw", (solicitação, resposta) => { var modelo = Joi.objeto().chaves({ satoshis: Joi.número().necessário(), id: Joi.string().necessário() }); Joi.validar(solicitação.corpo, modelo, { stripUnknown: verdadeiro }, (erro, valor) => { se(erro) { retorno resposta.status(500).enviar(erro); } ajudante.getAccountBalance(valor.id).então(resultado => { se(resultado.equilíbrio == nulo || (resultado.equilíbrio - valor.satoshis) < 0) { retorno resposta.status(500).enviar({ "mensagem": "Não há `". + valor.satoshis + "` satoshis disponíveis para saque" }); } Solicitação("https://api.coinmarketcap.com/v1/ticker/bitcoin/").então(mercado => { var usd = (Bitcore.Unidade.deSatoshis(valor.satoshis).paraBTC() * JSON.analisar(mercado)[0].preço_usd).toFixed(2); var transação = { conta: valor.id, satoshis: (valor.satoshis * -1), usd: parseFloat(usd), carimbo de data/hora: (novo Data()).getTime(), status: "retirada", tipo: "transação" }; ajudante.inserir(transação).então(resultado => { resposta.enviar(resultado); }, erro => { resposta.status(500).enviar(erro); }); }, erro => { resposta.status(500).enviar(erro); }); }, erro => { retorno resposta.status(500).enviar(erro); }); }); }); |
Eventos semelhantes estão acontecendo aqui. Depois de validar o corpo da solicitação, obtemos o saldo da nossa conta e nos certificamos de que o valor que estamos retirando é menor ou igual ao nosso saldo. Se for, podemos fazer outra conversão com base no preço atual do CoinMarketCap. Criaremos uma transação usando um valor negativo e a salvaremos no banco de dados.
Em ambos os casos, estamos confiando no CoinMarketCap, que teve uma controvérsia negativa no passado. Talvez você queira escolher um recurso diferente para as conversões.
Por fim, temos as transferências:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
aplicativo.postagem("/transferência", (solicitação, resposta) => { var modelo = Joi.objeto().chaves({ quantidade: Joi.número().necessário(), endereço de origem: Joi.string().opcional(), endereço de destino: Joi.string().necessário(), id: Joi.string().necessário() }); Joi.validar(solicitação.corpo, modelo, { stripUnknown: verdadeiro }, (erro, valor) => { se(erro) { retorno resposta.status(500).enviar(erro); } se(valor.endereço de origem) { ajudante.createTransactionFromAccount(valor.id, valor.endereço de origem, valor.endereço de destino, valor.quantidade).então(resultado => { resposta.enviar(resultado); }, erro => { resposta.status(500).enviar(erro); }); } mais { ajudante.createTransactionFromMaster(valor.id, valor.endereço de destino, valor.quantidade).então(resultado => { resposta.enviar(resultado); }, erro => { resposta.status(500).enviar(erro); }); } }); }); |
Se a solicitação contiver um endereço de origem, faremos a transferência de nossa própria carteira; caso contrário, faremos a transferência da carteira gerenciada pela bolsa.
Tudo isso é baseado em funções que criamos anteriormente.
Com os pontos de extremidade fora do caminho, podemos nos concentrar na inicialização do nosso aplicativo e chegar a uma conclusão.
Inicialização do aplicativo Express Framework
No momento, temos dois arquivos que permanecem intocados pelo exemplo. Não adicionamos uma configuração ou lógica de acionamento para inicializar nossos endpoints.
Abra o arquivo config.json e inclua algo como o seguinte:
|
1 2 3 4 5 6 7 |
{ "mnemônico": "manage inspire agent october potato thought hospital trim shoulder round tired kangaroo", "host": "localhost", "bucket" (balde): "bitbase", "nome de usuário": "bitbase", "senha": "123456" } |
Lembre-se de que esse arquivo é extremamente sensível. Considere bloqueá-lo ou até mesmo usar uma abordagem diferente. Se a semente for exposta, todas as chaves privadas de todas as contas de usuário e da conta de troca poderão ser obtidas sem nenhum esforço.
Agora, abra o arquivo app.js e inclua o seguinte:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const Expresso = exigir("expresso"); const Analisador de corpo = exigir("body-parser"); const Bitcore = exigir("bitcore-lib"); const Mnemônico = exigir("bitcore-mnemônico"); const Configuração = exigir("./config"); const Ajudante = exigir("./classes/helper"); var aplicativo = Expresso(); aplicativo.uso(Analisador de corpo.json()); aplicativo.uso(Analisador de corpo.codificado por url({ estendido: verdadeiro })); var mnemônico = novo Mnemônico(Configuração.mnemônico); var mestre = novo Bitcore.HDPrivateKey(mnemônico.toHDPrivateKey()); módulo.exportações.ajudante = novo Ajudante(Configuração.hospedeiro, Configuração.balde, Configuração.nome de usuário, Configuração.senha, mestre); exigir("./routes/account.js")(aplicativo); exigir("./routes/transaction.js")(aplicativo); exigir("./routes/utility.js")(aplicativo); var servidor = aplicativo.ouvir(3000, () => { console.registro("Ouvindo em :" + servidor.endereço().porto + "..."); }); |
O que estamos fazendo é inicializar o Express, carregar as informações de configuração e vincular nossas rotas. As module.exports.helper é o nosso singleton que será usado em todos os outros arquivos JavaScript.
Conclusão
Você acabou de ver como começar a criar sua própria bolsa de criptomoedas usando Node.js e Couchbase como o banco de dados NoSQL. Cobrimos muitos assuntos, desde a geração de carteiras HD até a criação de pontos de extremidade com lógica de banco de dados complexa.
Mas não posso deixar de enfatizar isso. Sou um entusiasta de criptomoedas e não tenho experiência real no setor financeiro. As coisas que compartilhei devem funcionar, mas podem ser feitas de forma muito melhor. Não se esqueça de criptografar suas chaves e manter suas sementes seguras. Teste seu trabalho e saiba no que está se metendo.
Se você quiser fazer o download desse projeto, confira-o em GitHub. Se quiser compartilhar percepções, experiências, etc., sobre o tópico, compartilhe-as nos comentários. A comunidade pode trabalhar para criar algo excelente!
Se você é fã do Golang, criei um projeto semelhante em um tutorial anterior.
Na minha opinião, a troca de bitcoin é uma escolha bastante inferior à ideal para o artigo sobre o Couchbase. Qualquer coisa que mexa com dinheiro precisa de um "D" confiável em ACID (ou seja, com 4 ou mais noves, no mínimo). Não acho que o Couchbase tenha isso (pense em um nó morrendo, em um auto-failover acontecendo e em pessoas perdendo dinheiro). Outro problema é a consistência. O código acima tem uma corrida entre verificar o saldo e salvar a transação de saque, o que destaca isso claramente.
Outro problema é que a verificação do saldo é O(N) no número de transações da conta. As visualizações materializadas do CouchDB podem fazer a busca do saldo da conta em O(log N), mas é claro que isso ainda é muito lento log N. AFAIK nenhum índice niql é capaz disso.
Na minha opinião, esse artigo acaba destacando os pontos fracos do couchbase. Ou seja, porque o couchbase não é adequado como backend de uma bolsa de bitcoin. Um artigo como esse poderia ser muito adequado para algo como o CockroachDB ou o cloud spanner.
Não demonstrei isso neste artigo, mas você pode definir seus próprios requisitos de durabilidade por meio do SDK. Se estiver preocupado com a consistência e com a perda de dinheiro, você poderá alterar as configurações para responder somente após a persistência no disco ou somente após a replicação de um número X de vezes.
https://developer.couchbase.com/documentation/server/current/sdk/durability.html
Quando se trata de consulta, você pode definir a consistência da consulta. Se desejar, você pode esperar até que o índice seja atualizado.
https://developer.couchbase.com/documentation/server/current/indexes/performance-consistency.html
Entendo o seu ponto de vista, mas não acho que seja um problema tão grande quanto você imagina.
Mas obrigado por compartilhar seus dois centavos :-)
Desculpe-me por isso, eu o tinha privado. Agora é público.