/* * This file is part of the Flowee project * Copyright (C) 2019 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ // #undef NDEBUG // make assert do something #include "Flowee.h" #include "ContextData.h" #include #include #include #include namespace { /* Javascripts ArrayBuffer is almost the same as Flowee's ConstBuffer * and technically it is posible to create one ArrayBuffer for one * of Flowee's shared buffers. Which may be shared between many ConstBuffer * instances and subsequently, many Javascript UInt8Array instances. * * To do this, however, I need to somehow figure out to reuse a BufferWrap * when I see the same share pointer used in a different ConstBuffer. * The correct solution for this is to have a map on the context object * to remember the Napi::ArrayBuffer instances and make the BufferWrap * update that map. * * This is left for the future, as we already have zero copy, all the way * from the network layer to the Javascript layer. * * Which is pretty decent. * * Refactoring stuff to pass in the context everywhere is a lot of work * and then profiling it to see if the map overhead is keeping the whole * actually beneficial is a lot more work... */ struct BufferWrap { BufferWrap(const std::shared_ptr &buffer) : buf(buffer) {} std::shared_ptr buf; }; void destructor(Napi::Env, void *bufPtr, void *bufWrap) { (void)bufPtr; BufferWrap *w = reinterpret_cast(bufWrap); assert(w); assert(bufPtr == w->buf.get()); delete w; } } Napi::Uint8Array Flowee::wrap(Napi::Env env, const Streaming::ConstBuffer &buffer) { if (buffer.isEmpty()) return Napi::Uint8Array::New(env, 0); BufferWrap *wrap = new BufferWrap(buffer.internal_buffer()); auto buf = Napi::ArrayBuffer::New(env, const_cast(reinterpret_cast(buffer.begin())), static_cast(buffer.size()), destructor, wrap); return Napi::Uint8Array::New(env, static_cast(buffer.size()), buf, 0); } char nibbleToChar(uint8_t k) { k = k & 0xF; if (k < 10) return char('0' + k); return char('a' + k - 10); } // custom made since Bitcoin Cash likes the order to be reversed on printing. Napi::String Flowee::hashToString(Napi::Env env, const Streaming::ConstBuffer &buffer) { if (buffer.isEmpty()) return Napi::String::New(env, ""); assert(buffer.size() == 32); if (buffer.size() != 32) throw std::runtime_error("Internal error (hash invalid size)"); char buf[65]; char *ptr = buf; for (int i = 31; i >= 0; --i) { uint8_t byte = static_cast(buffer.begin()[i]); *ptr = nibbleToChar(byte >> 4); ++ptr; *ptr = nibbleToChar(byte); ++ptr; } buf[64] = 0; return Napi::String::New(env, buf); } Napi::Object Flowee::populateTransaction(Napi::Env env, const Blockchain::Transaction *transaction, bool binaryHash) { using boost::algorithm::hex; Napi::Object tx = Napi::Object::New(env); tx.Set("blockHeight", Napi::Number::From(env, transaction->blockHeight)); tx.Set("offsetInBlock", Napi::Number::From(env, transaction->offsetInBlock)); tx.Set("jobId", Napi::Number::From(env, transaction->jobId)); tx.Set("isCoinbase", Napi::Boolean::From(env, transaction->isCoinbase())); tx.Set("fullTxData", wrap(env, transaction->fullTxData)); tx.Set("txid", binaryHash ? wrap(env, transaction->txid) : hashToString(env, transaction->txid)); Napi::Array inputs = Napi::Array::New(env, transaction->inputs.size()); for (size_t i = 0 ; i < transaction->inputs.size(); ++i) { const Blockchain::Input &input = transaction->inputs.at(i); Napi::Object in = Napi::Object::New(env); in.Set("outputIndex", Napi::Number::From(env, input.outIndex)); in.Set("previousTxid", binaryHash ? wrap(env, input.prevTxId) : hashToString(env, input.prevTxId)); std::string is; hex(input.inputScript.begin(), input.inputScript.end(), back_inserter(is)); in.Set("script", Napi::String::New(env, is)); in.Set("script-array", wrap(env, input.inputScript)); inputs[uint32_t(i)] = in; } tx.Set("inputs", inputs); Napi::Array outputs = Napi::Array::New(env, transaction->outputs.size()); for (size_t i = 0 ; i < transaction->outputs.size(); ++i) { const Blockchain::Output &output = transaction->outputs.at(i); Napi::Object out = Napi::Object::New(env); out.Set("index", Napi::Number::From(env, output.index)); out.Set("amount", Napi::Number::From(env, output.amount)); switch (output.type) { case Blockchain::Output::Nothing: break; case Blockchain::Output::FullScript: { out.Set("script-array", wrap(env, output.outScript)); std::string s; hex(output.outScript.begin(), output.outScript.end(), back_inserter(s)); out.Set("script", Napi::String::New(env, s)); break; } case Blockchain::Output::OnlyAddress: { // TODO we assume we use main-chain addresses here. // maybe useful to ask the hub which chain it is on? const std::string cashAddress = CashAddress::encodeCashAddr("bitcoincash", { CashAddress::PUBKEY_TYPE, std::vector(output.outScript.begin(), output.outScript.end()) }); out.Set("address", cashAddress); out.Set("address-array", wrap(env, output.outScript)); break; } default: assert(false); } outputs[uint32_t(i)] = out; } tx.Set("outputs", outputs); return tx; } Flowee::PromiseCallback::PromiseCallback(Napi::Env env) : p(Napi::Promise::Deferred::New(env)) { } void Flowee::PromiseCallback::resolve(Napi::Value value) { if (!present) return; assert(!value.IsNull()); // null values make Resolve below throw present = false; p.Resolve(value); } Napi::Promise Flowee::PromiseCallback::promise(Napi::Env env) { p = Napi::Promise::Deferred::New(env); present = true; return p.Promise(); } void Flowee::PromiseCallback::reject(Napi::Value value) { if (!present) return; assert(!value.IsNull()); // null values make Reject below throw present = false; p.Reject(value); } Streaming::ConstBuffer Flowee::hexStringToBuffer(const std::string &hash) { size_t i = 0; if (hash.size() == 66) { if (hash[0] == '0' && (hash[1] == 'x' || hash[1] == 'X')) { i = 2; } else { return Streaming::ConstBuffer(); // invalid input } } else if (hash.size() != 64) { // invalid input return Streaming::ConstBuffer(); // invalid input } Streaming::BufferPool pool(32); int i2 = 31; // back to front while (i < hash.size()) { const int8_t k = hash[i]; const uint8_t v = static_cast(HexDigit(k)); if (k > 'f' || v == 0xFF) return Streaming::ConstBuffer(); // invalid input if ((i % 2) == 0) { pool.begin()[i2] = static_cast(v << 4); } else { pool.begin()[i2--] += v; } ++i; } return pool.commit(32); } Streaming::ConstBuffer Flowee::parseAddress(const Napi::String &input, bool *isHash) { // this can be either a hex string, or an address. Either way, utf8 should suffice. std::string str = input.ToString().Utf8Value(); str.erase(remove_if(str.begin(), str.end(), isspace), str.end()); // trim auto rc = hexStringToBuffer(str); if (!rc.isEmpty()) { if (isHash) *isHash = true; return rc; } CashAddress::Content c = CashAddress::decodeCashAddrContent(str, "bitcoincash"); bool ok = !c.hash.empty(); if (!ok) { // try to fall back to legacy address encoding (btc compatible) CBase58Data old; if (old.SetString(str)) { c.hash = old.data(); ok = true; if (old.isMainnetPkh()) c.type = CashAddress::PUBKEY_TYPE; else if (old.isMainnetSh()) c.type = CashAddress::SCRIPT_TYPE; else ok = false; } } if (ok) { if (isHash) *isHash = false; return CashAddress::createHashedOutputScript(c); } return Streaming::ConstBuffer(); } Napi::Object setupBindings(Napi::Env env, Napi::Object exports) { ContextData *data = new ContextData(env); data->setupBindings(env, exports); /* // uncomment this to have full debug-level logging. auto *logger = Log::Manager::instance(); logger->clearChannels(); Log::Verbosity v = Log::WarningLevel; v = Log::DebugLevel; logger->clearLogLevels(v); logger->addConsoleChannel(); */ return exports; } NODE_API_MODULE(flowee, setupBindings)