2019-11-09 19:08:38 +01:00
|
|
|
/*
|
|
|
|
|
* This file is part of the Flowee project
|
2021-03-18 12:32:59 +01:00
|
|
|
* Copyright (C) 2019-2021 Tom Zander <tom@flowee.org>
|
2019-11-09 19:08:38 +01:00
|
|
|
*
|
|
|
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
2019-11-29 22:12:08 +01:00
|
|
|
|
2019-11-30 19:46:30 +01:00
|
|
|
// #undef NDEBUG // make assert do something
|
2019-11-29 22:12:08 +01:00
|
|
|
#include "Flowee.h"
|
2019-11-09 18:18:56 +01:00
|
|
|
|
|
|
|
|
#include "ContextData.h"
|
2019-12-08 23:20:41 +01:00
|
|
|
#include <boost/algorithm/hex.hpp>
|
2019-12-30 17:13:29 +01:00
|
|
|
#include <base58.h>
|
2019-12-08 23:20:41 +01:00
|
|
|
#include <cashaddr.h>
|
2019-12-30 17:13:29 +01:00
|
|
|
#include <utilstrencodings.h>
|
2019-11-09 18:18:56 +01:00
|
|
|
|
2019-11-30 19:46:30 +01:00
|
|
|
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<char> &buffer) : buf(buffer) {}
|
|
|
|
|
std::shared_ptr<char> buf;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
void destructor(Napi::Env, void *bufPtr, void *bufWrap)
|
|
|
|
|
{
|
|
|
|
|
(void)bufPtr;
|
|
|
|
|
BufferWrap *w = reinterpret_cast<BufferWrap*>(bufWrap);
|
|
|
|
|
assert(w);
|
|
|
|
|
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<void*>(reinterpret_cast<const void*>(buffer.begin())),
|
|
|
|
|
static_cast<size_t>(buffer.size()), destructor, wrap);
|
|
|
|
|
return Napi::Uint8Array::New(env, static_cast<size_t>(buffer.size()), buf, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-08 23:20:41 +01:00
|
|
|
char nibbleToChar(uint8_t k) {
|
|
|
|
|
k = k & 0xF;
|
|
|
|
|
if (k < 10)
|
|
|
|
|
return char('0' + k);
|
|
|
|
|
return char('a' + k - 10);
|
|
|
|
|
}
|
2019-11-30 19:46:30 +01:00
|
|
|
|
2019-12-08 23:20:41 +01:00
|
|
|
// custom made since Bitcoin Cash likes the order to be reversed on printing.
|
|
|
|
|
Napi::String Flowee::hashToString(Napi::Env env, const Streaming::ConstBuffer &buffer)
|
2019-11-29 22:12:08 +01:00
|
|
|
{
|
2019-12-08 23:20:41 +01:00
|
|
|
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<uint8_t>(buffer.begin()[i]);
|
|
|
|
|
*ptr = nibbleToChar(byte >> 4);
|
|
|
|
|
++ptr;
|
|
|
|
|
*ptr = nibbleToChar(byte);
|
|
|
|
|
++ptr;
|
|
|
|
|
}
|
|
|
|
|
buf[64] = 0;
|
|
|
|
|
|
|
|
|
|
return Napi::String::New(env, buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2021-02-12 14:35:56 +01:00
|
|
|
Napi::Object Flowee::populateTransaction(Napi::Env env, const Blockchain::Transaction &transaction, bool binaryHash)
|
2019-12-08 23:20:41 +01:00
|
|
|
{
|
|
|
|
|
using boost::algorithm::hex;
|
2019-11-29 22:12:08 +01:00
|
|
|
Napi::Object tx = Napi::Object::New(env);
|
|
|
|
|
|
2021-02-12 14:35:56 +01:00
|
|
|
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));
|
2019-12-08 23:20:41 +01:00
|
|
|
tx.Set("txid",
|
2021-02-12 14:35:56 +01:00
|
|
|
binaryHash ? wrap(env, transaction.txid) : hashToString(env, transaction.txid));
|
2021-03-18 12:32:59 +01:00
|
|
|
if (transaction.fees != -1)
|
|
|
|
|
tx.Set("fees", Napi::Number::From(env, transaction.fees));
|
2019-11-29 22:12:08 +01:00
|
|
|
|
2021-02-12 14:35:56 +01:00
|
|
|
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);
|
2019-11-30 19:46:30 +01:00
|
|
|
Napi::Object in = Napi::Object::New(env);
|
2019-12-08 23:20:41 +01:00
|
|
|
in.Set("outputIndex", Napi::Number::From(env, input.outIndex));
|
2019-11-30 19:46:30 +01:00
|
|
|
|
2019-12-08 23:20:41 +01:00
|
|
|
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));
|
2019-12-18 23:22:48 +01:00
|
|
|
in.Set("script", Napi::String::New(env, is));
|
|
|
|
|
in.Set("script-array", wrap(env, input.inputScript));
|
2019-11-30 19:46:30 +01:00
|
|
|
inputs[uint32_t(i)] = in;
|
2019-11-29 22:12:08 +01:00
|
|
|
}
|
2019-11-30 19:46:30 +01:00
|
|
|
tx.Set("inputs", inputs);
|
2019-11-29 22:12:08 +01:00
|
|
|
|
2021-02-12 14:35:56 +01:00
|
|
|
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);
|
2019-11-30 19:46:30 +01:00
|
|
|
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;
|
2019-12-08 23:20:41 +01:00
|
|
|
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));
|
2019-11-30 19:46:30 +01:00
|
|
|
break;
|
2019-12-08 23:20:41 +01:00
|
|
|
}
|
|
|
|
|
case Blockchain::Output::OnlyAddress: {
|
2019-12-23 15:02:57 +01:00
|
|
|
// 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<uint8_t>(output.outScript.begin(), output.outScript.end()) });
|
2019-12-08 23:20:41 +01:00
|
|
|
out.Set("address", cashAddress);
|
|
|
|
|
out.Set("address-array", wrap(env, output.outScript));
|
2019-11-30 19:46:30 +01:00
|
|
|
break;
|
2019-12-08 23:20:41 +01:00
|
|
|
}
|
2019-11-30 19:46:30 +01:00
|
|
|
default:
|
|
|
|
|
assert(false);
|
|
|
|
|
}
|
|
|
|
|
outputs[uint32_t(i)] = out;
|
|
|
|
|
}
|
|
|
|
|
tx.Set("outputs", outputs);
|
2019-11-29 22:12:08 +01:00
|
|
|
|
|
|
|
|
return tx;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-09 14:57:21 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-27 21:29:59 +01:00
|
|
|
void Flowee::PromiseCallback::reject(Napi::Value value)
|
|
|
|
|
{
|
|
|
|
|
if (!present)
|
|
|
|
|
return;
|
|
|
|
|
assert(!value.IsNull()); // null values make Reject below throw
|
|
|
|
|
present = false;
|
|
|
|
|
p.Reject(value);
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 09:47:42 +01:00
|
|
|
thread_local Streaming::BufferPool g_buffer;
|
|
|
|
|
Streaming::BufferPool& Flowee::globalPool(int reserveSize)
|
|
|
|
|
{
|
|
|
|
|
g_buffer.reserve(reserveSize);
|
|
|
|
|
return g_buffer;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-30 17:13:29 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 09:47:42 +01:00
|
|
|
Streaming::BufferPool &pool = globalPool(32);
|
2019-12-30 17:13:29 +01:00
|
|
|
int i2 = 31; // back to front
|
|
|
|
|
while (i < hash.size()) {
|
|
|
|
|
const int8_t k = hash[i];
|
|
|
|
|
const uint8_t v = static_cast<uint8_t>(HexDigit(k));
|
|
|
|
|
if (k > 'f' || v == 0xFF)
|
|
|
|
|
return Streaming::ConstBuffer(); // invalid input
|
|
|
|
|
if ((i % 2) == 0) {
|
|
|
|
|
pool.begin()[i2] = static_cast<char>(v << 4);
|
|
|
|
|
} else {
|
|
|
|
|
pool.begin()[i2--] += v;
|
|
|
|
|
}
|
|
|
|
|
++i;
|
|
|
|
|
}
|
|
|
|
|
return pool.commit(32);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2021-02-26 19:37:16 +01:00
|
|
|
Streaming::ConstBuffer Flowee::parseAddress(const Napi::String &input, std::string *prefix, bool *isHash)
|
2019-12-30 17:13:29 +01:00
|
|
|
{
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-26 19:37:16 +01:00
|
|
|
std::string expectedPrefix;
|
|
|
|
|
if (prefix)
|
|
|
|
|
expectedPrefix = *prefix;
|
|
|
|
|
else
|
|
|
|
|
expectedPrefix = CashAddressMainnet;
|
|
|
|
|
CashAddress::Content c = CashAddress::decodeCashAddrContent(str, expectedPrefix);
|
2019-12-30 17:13:29 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-11-29 22:12:08 +01:00
|
|
|
|
2019-11-09 19:08:38 +01:00
|
|
|
Napi::Object setupBindings(Napi::Env env, Napi::Object exports)
|
2019-11-09 18:18:56 +01:00
|
|
|
{
|
2019-12-01 23:03:56 +01:00
|
|
|
ContextData *data = new ContextData(env);
|
2019-11-09 18:18:56 +01:00
|
|
|
data->setupBindings(env, exports);
|
|
|
|
|
return exports;
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-08 23:20:41 +01:00
|
|
|
|
2019-11-09 19:08:38 +01:00
|
|
|
NODE_API_MODULE(flowee, setupBindings)
|