More work on API plus basic block explorer

Added FATJAR packaging support to pom.xml

Added some "summary" fields to API calls but more need doing.
Corrected path clash from having unnecessary @OpenAPIDefinition annotations.
Added API "tags" to group similar calls (address-based, block-related, etc.)
Fixed addresses/lastreference/{address}
Implemented addresses/lastreference/{address}/unconfirmed
Implemented addresses/assets/{address}
Added /admin/stop and /admin/uptime API calls.
Moved general API info into new src/api/ApiDefinition.java
Added CORS support to ApiService
Added /transactions/address/{address} and /transactions/block/{signature}

Replaced references to test.Common.* to do with repository factory.
 This fixes issues with building FATJAR due to references to test classes
 that are omitted from FATJAR.

Changes to AccountBalanceData, BlockData and TransactionData
 to support JAX-RS rendering to JSON.

Added getUnconfirmedLastReference() to Account.

Added getAllBalances(address) to account repository
 - returns all asset balances for that address.

Added getAllSignaturesInvolvingAddress(address) to account repository
 but currently only uses TransactionRecipients HSQLDB table.
 (And even that wasn't automatically populated).

Included: very basic block explorer to be opened in browser as a file:
block-explorer.html
This commit is contained in:
catbref 2018-12-04 16:34:55 +00:00
parent 9dcdcb0ffe
commit ad9fa9bf9d
26 changed files with 1377 additions and 84 deletions

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
encoding/<project>=UTF-8
encoding/src=UTF-8
encoding/tests=UTF-8

789
block-explorer.html Normal file
View File

@ -0,0 +1,789 @@
<!DOCTYPE html>
<html>
<head>
<title>Block Explorer</title>
<style>
body { color: #404040; font-family: sans-serif; }
table { width: 100%; border-collapse: collapse; border: 1px solid #dddddd; margin-bottom: 40px; }
tr:nth-child(odd) { background-color: #eeeeee; }
th { background-color: #eeeeee; }
td { text-align: center; }
.sig { font-size: 60%; color: #405906; }
.ref { font-size: 60%; color: #609040; }
</style>
<script>
// USEFUL CODE STARTS HERE
function publicKeyToAddress(publicKey) {
var ADDRESS_VERSION = 58; // For normal user accounts starting with "Q"
// var ADDRESS_VERSION = 23; // For Automated Transaction accounts starting with "A"
var publicKeyHashSHA256 = SHA256.digest(publicKey);
var ripemd160 = new RIPEMD160(); // 'Grandfathered' broken implementation of MD160
var publicKeyHash = ripemd160.digest(publicKeyHashSHA256);
var addressArray = new Uint8Array();
addressArray = appendBuffer(addressArray, [ADDRESS_VERSION]);
addressArray = appendBuffer(addressArray, publicKeyHash);
var checkSum = SHA256.digest(SHA256.digest(addressArray));
addressArray = appendBuffer(addressArray, checkSum.subarray(0, 4));
return Base58.encode(addressArray);
}
function renderAddressTransactions(e) {
var transactions = e.target.response;
var address = this.responseURL.split("/")[5];
if (transactions.length == 0) {
document.body.innerHTML += 'No transactions involving address';
return;
}
var html = '<table id="transactions"><tr><th>Type</th><th>Timestamp</th><th>Creator</th><th>Fee</th><th>Signature</th><th>Reference</th></tr>';
for (var i=0; i<transactions.length; ++i) {
var tx = transactions[i];
var txTimestamp = new Date(tx.timestamp).toLocaleString();
var txCreatorAddress = publicKeyToAddress(base64ToArray(tx.creatorPublicKey)); // currently base64 but likely to be base58 in the future
var row = '<tr><td>' + tx.type + '</td>' +
'<td>' + txTimestamp + '</td>' +
'<td class="addr">' + addressAsLink(txCreatorAddress) + '</td>' +
'<td>' + tx.fee + ' QORA</td>' +
'<td class="sig">' + Base58.encode(base64ToArray(tx.signature)) + '</td>' +
'<td class="ref">' + Base58.encode(base64ToArray(tx.reference)) + '</td></tr>';
html += row;
}
html += '</table>';
document.body.innerHTML += html;
}
function renderAddressInfo(e) {
var balances = e.target.response;
var address = this.responseURL.split("/")[5];
var html = '<h1>Address ' + address + '</h1>';
html += '<table><tr><th>Asset ID</th><th>Balance</th></tr>';
for (var i=0; i<balances.length; ++i) {
var balanceInfo = balances[i];
html += '<tr><td>' + balanceInfo.assetId + '</td><td>' + balanceInfo.balance + '</td></tr>';
}
html += '</table>';
document.body.innerHTML = html;
XHR({
url: "http://localhost:9085/transactions/address/" + address,
onload: renderAddressTransactions,
responseType: "json"
});
}
function fetchAddressInfo(address) {
XHR({
url: "http://localhost:9085/addresses/assets/" + address,
onload: renderAddressInfo,
responseType: "json"
});
}
function addressAsLink(address) {
return '<a href="#" onclick="fetchAddressInfo(' + "'" + address + "'" + ')">' + address + '</a>';
}
function renderBlockTransactions(e) {
var transactions = e.target.response;
if (transactions.length == 0) {
document.body.innerHTML += 'No transactions in block';
return;
}
var html = '<table id="transactions"><tr><th>Type</th><th>Timestamp</th><th>Creator</th><th>Fee</th><th>Signature</th><th>Reference</th></tr>';
for (var i=0; i<transactions.length; ++i) {
var tx = transactions[i];
var txTimestamp = new Date(tx.timestamp).toLocaleString();
var txCreatorAddress = publicKeyToAddress(base64ToArray(tx.creatorPublicKey)); // currently base64 but likely to be base58 in the future
var row = '<tr><td>' + tx.type + '</td>' +
'<td>' + txTimestamp + '</td>' +
'<td class="addr">' + addressAsLink(txCreatorAddress) + '</td>' +
'<td>' + tx.fee + ' QORA</td>' +
'<td class="sig">' + Base58.encode(base64ToArray(tx.signature)) + '</td>' +
'<td class="ref">' + Base58.encode(base64ToArray(tx.reference)) + '</td></tr>';
html += row;
}
html += '</table>';
document.body.innerHTML += html;
}
function renderBlockInfo(e) {
var blockData = e.target.response;
// These properties are currently emitted as base64 by API but likely to be base58 in the future, so convert them
var props = [ "signature", "reference", "transactionsSignature", "generatorPublicKey", "generatorSignature" ];
for (var i=0; i<props.length; ++i) {
var p = props[i];
blockData[p] = Base58.encode(base64ToArray(blockData[p]));
}
// convert generator public key into address
blockData.generator = publicKeyToAddress(base64ToArray(blockData.generatorPublicKey)); // currently base64 but likely to be base58 in the future
var html = '<h1>Block ' + blockData.height + '</h1>';
html += '<table id="block"><tr><th>Property</th><th>Value</th></tr>';
for (var p in blockData) {
html += '<tr><td>' + p + '</td>';
if (p.indexOf("ignature") != -1) {
html += '<td class="sig">';
} else if (p.indexOf("eference") != -1) {
html += '<td class="ref">';
} else {
html += '<td>';
}
if (p == "generator") {
html += addressAsLink(blockData[p]);
} else {
html += blockData[p];
}
if (p.indexOf("ees") != -1)
html += " QORA";
html += '</td></tr>';
}
html += '</table>';
document.body.innerHTML = html;
// Fetch block's transactions
XHR({
url: "http://localhost:9085/transactions/block/" + blockData.signature,
onload: renderBlockTransactions,
responseType: "json"
});
}
function fetchBlockInfo(height) {
XHR({
url: "http://localhost:9085/blocks/byheight/" + height,
onload: renderBlockInfo,
responseType: "json"
});
}
function listBlock(e) {
var blockData = e.target.response;
var ourHeight = blockData.height;
var blockTimestamp = new Date(blockData.timestamp).toLocaleString();
var blockGeneratorAddress = publicKeyToAddress(base64ToArray(blockData.generatorPublicKey)); // currently base64 but likely to be base58 in the future
var ourRow = document.createElement('TR');
ourRow.innerHTML = '<td><a href="#" onclick="fetchBlockInfo(' + ourHeight + ')">' + ourHeight + '</a></td>' +
'<td>' + blockTimestamp + '</td>' +
'<td class="addr">' + addressAsLink(blockGeneratorAddress) + '</td>' +
'<td>' + blockData.generatingBalance + '</td>' +
'<td>' + blockData.transactionCount + '</td>' +
'<td>' + blockData.totalFees + ' QORA</td>';
var table = document.getElementById('blocks');
var rows = table.getElementsByTagName('TR');
for (var r = 1; r < rows.length; ++r)
if (ourHeight > rows[r].cells[0].innerText) {
table.insertBefore(ourRow, rows[r]);
return;
}
table.appendChild(ourRow);
}
function showShutdown() {
document.body.innerHTML = '<h1>Shutdown</h1>';
}
function shutdownAPI() {
XHR({
url: "http://localhost:9085/admin/stop",
onload: showShutdown,
responseType: "json"
});
}
function listBlocksFrom(height) {
document.body.innerHTML = '<h1>Blocks</h1>';
document.body.innerHTML += '<table id="blocks"><tr><th>Height</th><th>Time</th><th>Generator</th><th>Gen.Balance</th><th>TX</th><th>Fees</th></tr></table>';
if (height > 20)
document.body.innerHTML += '<a href="#" onclick="listBlocksFrom(' + (height - 20) + ')">Previous blocks...</a>';
document.body.innerHTML += '<p><a href="#" onclick="fetchBlockInfo(356928)">Block with lots of transactions</a>';
document.body.innerHTML += '<p><a href="#" onclick="shutdownAPI()">Shutdown</a>';
for (var h = height; h > 0 && h >= height - 20; --h)
XHR({
url: "http://localhost:9085/blocks/byheight/" + h,
onload: listBlock,
responseType: "json"
});
return false;
}
function initialBlocks(e) {
var height = e.target.response;
console.log("initial block height: " + height);
listBlocksFrom(height);
}
function windowOnLoad() {
XHR({
url: "http://localhost:9085/blocks/height",
onload: initialBlocks,
responseType: "json"
});
}
window.addEventListener('load', windowOnLoad, false);
// UTILITY FUNCTIONS BELOW (Base64, Base58, SHA256, etc.)
function base64ToArray(base64) {
var raw = window.atob(base64);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for(i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}
function appendBuffer (buffer1, buffer2) {
buffer1 = new Uint8Array(buffer1);
buffer2 = new Uint8Array(buffer2);
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(buffer1, 0);
tmp.set(buffer2, buffer1.byteLength);
return tmp;
}
// BASE58
(function() {
var ALPHABET, ALPHABET_MAP, Base58, i;
Base58 = (typeof module !== "undefined" && module !== null ? module.exports : void 0) || (window.Base58 = {});
ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
ALPHABET_MAP = {};
i = 0;
while (i < ALPHABET.length) {
ALPHABET_MAP[ALPHABET.charAt(i)] = i;
i++;
}
Base58.encode = function(buffer) {
var carry, digits, j;
if (buffer.length === 0) {
return "";
}
i = void 0;
j = void 0;
digits = [0];
i = 0;
while (i < buffer.length) {
j = 0;
while (j < digits.length) {
digits[j] <<= 8;
j++;
}
digits[0] += buffer[i];
carry = 0;
j = 0;
while (j < digits.length) {
digits[j] += carry;
carry = (digits[j] / 58) | 0;
digits[j] %= 58;
++j;
}
while (carry) {
digits.push(carry % 58);
carry = (carry / 58) | 0;
}
i++;
}
i = 0;
while (buffer[i] === 0 && i < buffer.length - 1) {
digits.push(0);
i++;
}
return digits.reverse().map(function(digit) {
return ALPHABET[digit];
}).join("");
};
Base58.decode = function(string) {
var bytes, c, carry, j;
if (string.length === 0) {
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(0);
}
i = void 0;
j = void 0;
bytes = [0];
i = 0;
while (i < string.length) {
c = string[i];
if (!(c in ALPHABET_MAP)) {
throw "Base58.decode received unacceptable input. Character '" + c + "' is not in the Base58 alphabet.";
}
j = 0;
while (j < bytes.length) {
bytes[j] *= 58;
j++;
}
bytes[0] += ALPHABET_MAP[c];
carry = 0;
j = 0;
while (j < bytes.length) {
bytes[j] += carry;
carry = bytes[j] >> 8;
bytes[j] &= 0xff;
++j;
}
while (carry) {
bytes.push(carry & 0xff);
carry >>= 8;
}
i++;
}
i = 0;
while (string[i] === "1" && i < string.length - 1) {
bytes.push(0);
i++;
}
return new (typeof Uint8Array !== "undefined" && Uint8Array !== null ? Uint8Array : Buffer)(bytes.reverse());
};
}).call(this);
// SHA256
SHA256 = {};
SHA256.K = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b,
0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01,
0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7,
0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152,
0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08,
0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f,
0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
// The digest function returns the hash value (digest)
// as a 32 byte (typed) array.
// message: the string or byte array to hash
SHA256.digest = function(message) {
var h0 = 0x6a09e667;
var h1 = 0xbb67ae85;
var h2 = 0x3c6ef372;
var h3 = 0xa54ff53a;
var h4 = 0x510e527f;
var h5 = 0x9b05688c;
var h6 = 0x1f83d9ab;
var h7 = 0x5be0cd19;
var K = SHA256.K;
if (typeof message == 'string') {
var s = unescape(encodeURIComponent(message)); // UTF-8
message = new Uint8Array(s.length);
for (var i = 0; i < s.length; i++) {
message[i] = s.charCodeAt(i) & 0xff;
}
}
var length = message.length;
var byteLength = Math.floor((length + 72) / 64) * 64;
var wordLength = byteLength / 4;
var bitLength = length * 8;
var m = new Uint8Array(byteLength);
m.set(message);
m[length] = 0x80;
m[byteLength - 4] = bitLength >>> 24;
m[byteLength - 3] = (bitLength >>> 16) & 0xff;
m[byteLength - 2] = (bitLength >>> 8) & 0xff;
m[byteLength - 1] = bitLength & 0xff;
var words = new Int32Array(wordLength);
var byteIndex = 0;
for (var i = 0; i < words.length; i++) {
var word = m[byteIndex++] << 24;
word |= m[byteIndex++] << 16;
word |= m[byteIndex++] << 8;
word |= m[byteIndex++];
words[i] = word;
}
var w = new Int32Array(64);
for (var j = 0; j < wordLength; j += 16) {
for (i = 0; i < 16; i++) {
w[i] = words[j + i];
}
for (i = 16; i < 64; i++) {
var v = w[i - 15];
var s0 = (v >>> 7) | (v << 25);
s0 ^= (v >>> 18) | (v << 14);
s0 ^= (v >>> 3);
v = w[i - 2];
var s1 = (v >>> 17) | (v << 15);
s1 ^= (v >>> 19) | (v << 13);
s1 ^= (v >>> 10);
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) & 0xffffffff;
}
var a = h0;
var b = h1;
var c = h2;
var d = h3;
var e = h4;
var f = h5;
var g = h6;
var h = h7;
for (i = 0; i < 64; i++) {
s1 = (e >>> 6) | (e << 26);
s1 ^= (e >>> 11) | (e << 21);
s1 ^= (e >>> 25) | (e << 7);
var ch = (e & f) ^ (~e & g);
var temp1 = (h + s1 + ch + K[i] + w[i]) & 0xffffffff;
s0 = (a >>> 2) | (a << 30);
s0 ^= (a >>> 13) | (a << 19);
s0 ^= (a >>> 22) | (a << 10);
var maj = (a & b) ^ (a & c) ^ (b & c);
var temp2 = (s0 + maj) & 0xffffffff;
h = g
g = f
f = e
e = (d + temp1) & 0xffffffff;
d = c;
c = b;
b = a;
a = (temp1 + temp2) & 0xffffffff;
}
h0 = (h0 + a) & 0xffffffff;
h1 = (h1 + b) & 0xffffffff;
h2 = (h2 + c) & 0xffffffff;
h3 = (h3 + d) & 0xffffffff;
h4 = (h4 + e) & 0xffffffff;
h5 = (h5 + f) & 0xffffffff;
h6 = (h6 + g) & 0xffffffff;
h7 = (h7 + h) & 0xffffffff;
}
var hash = new Uint8Array(32);
for (i = 0; i < 4; i++) {
hash[i] = (h0 >>> (8 * (3 - i))) & 0xff;
hash[i + 4] = (h1 >>> (8 * (3 - i))) & 0xff;
hash[i + 8] = (h2 >>> (8 * (3 - i))) & 0xff;
hash[i + 12] = (h3 >>> (8 * (3 - i))) & 0xff;
hash[i + 16] = (h4 >>> (8 * (3 - i))) & 0xff;
hash[i + 20] = (h5 >>> (8 * (3 - i))) & 0xff;
hash[i + 24] = (h6 >>> (8 * (3 - i))) & 0xff;
hash[i + 28] = (h7 >>> (8 * (3 - i))) & 0xff;
}
return hash;
}
// The hash function returns the hash value as a hex string.
// message: the string or byte array to hash
SHA256.hash = function(message) {
var digest = SHA256.digest(message);
var hex = '';
for (i = 0; i < digest.length; i++) {
var s = '0' + digest[i].toString(16);
hex += s.length > 2 ? s.substring(1) : s;
}
return hex;
}
function XHR(options) {
var xhr = new XMLHttpRequest();
if ("form" in options) {
options.data = new FormData(options.form);
if ( !("url" in options) )
options.url = options.form.action;
if ( !("method" in options) && ("method" in options.form) )
options.method = options.form.method;
}
if ("json" in options) {
options.data = JSON.stringify(options.json);
options.method = "POST";
options.responseType = "json";
}
if ( !("method" in options) )
options.method = "GET";
if ("responseType" in options)
try {
xhr.responseType = options.responseType;
} catch(e) {
console.log("XMLHttpRequest doesn't support responseType of '" + options.responseType + "'");
xhr.bodgeJSON = true;
}
if ("onload" in options) {
if (options.responseType == "json" && xhr.bodgeJSON)
xhr.addEventListener("load", function(e) { var e = { target: { response: JSON.parse(e.target.response) } }; options.onload(e) }, false);
else
xhr.addEventListener("load", options.onload, false);
}
xhr.open(options.method, options.url);
if ("json" in options)
xhr.setRequestHeader( "Content-Type", "application/json" );
if ("contentType" in options)
xhr.setRequestHeader( "Content-Type", options.contentType );
xhr.send(options.data);
return xhr;
}
// RIPEMD160
var RIPEMD160 = (function () {
function RIPEMD160() {
this.MDbuf = [];
this.MDbuf[0] = 1732584193;
this.MDbuf[1] = -271733879;
this.MDbuf[2] = -1732584194;
this.MDbuf[3] = 271733878;
this.MDbuf[4] = -1009589776;
this.working = new Int32Array(16);
this.working_ptr = 0;
this.msglen = 0;
}
RIPEMD160.prototype.reset = function () {
this.MDbuf = [];
this.MDbuf[0] = 1732584193;
this.MDbuf[1] = -271733879;
this.MDbuf[2] = -1732584194;
this.MDbuf[3] = 271733878;
this.MDbuf[4] = -1009589776;
this.working = new Int32Array(16);
this.working_ptr = 0;
this.msglen = 0;
};
RIPEMD160.prototype.compress = function (X) {
var index = 0;
var a;
var b;
var c;
var d;
var e;
var A;
var B;
var C;
var D;
var E;
var temp;
var s;
A = a = this.MDbuf[0];
B = b = this.MDbuf[1];
C = c = this.MDbuf[2];
D = d = this.MDbuf[3];
E = e = this.MDbuf[4];
for (; index < 16; index++) {
temp = a + (b ^ c ^ d) + X[RIPEMD160.IndexArray[0][index]];
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = RIPEMD160.ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
temp = A + (B ^ (C | ~D)) + X[RIPEMD160.IndexArray[1][index]] + 1352829926;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = RIPEMD160.ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 32; index++) {
temp = a + ((b & c) | (~b & d)) + X[RIPEMD160.IndexArray[0][index]] + 1518500249;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = RIPEMD160.ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
temp = A + ((B & D) | (C & ~D)) + X[RIPEMD160.IndexArray[1][index]] + 1548603684;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = RIPEMD160.ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 48; index++) {
temp = a + ((b | ~c) ^ d) + X[RIPEMD160.IndexArray[0][index]] + 1859775393;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = RIPEMD160.ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
temp = A + ((B | ~C) ^ D) + X[RIPEMD160.IndexArray[1][index]] + 1836072691;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = RIPEMD160.ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 64; index++) {
temp = a + ((b & d) | (c & ~d)) + X[RIPEMD160.IndexArray[0][index]] + -1894007588;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = RIPEMD160.ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
temp = A + ((B & C) | (~B & D)) + X[RIPEMD160.IndexArray[1][index]] + 2053994217;
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = RIPEMD160.ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
for (; index < 80; index++) {
temp = a + (b ^ (c | ~d)) + X[RIPEMD160.IndexArray[0][index]] + -1454113458;
a = e;
e = d;
d = (c << 10) | (c >>> 22);
c = b;
s = RIPEMD160.ArgArray[0][index];
b = ((temp << s) | (temp >>> (32 - s))) + a;
temp = A + (B ^ C ^ D) + X[RIPEMD160.IndexArray[1][index]];
A = E;
E = D;
D = (C << 10) | (C >>> 22);
C = B;
s = RIPEMD160.ArgArray[1][index];
B = ((temp << s) | (temp >>> (32 - s))) + A;
}
D += c + this.MDbuf[1];
this.MDbuf[1] = this.MDbuf[2] + d + E;
this.MDbuf[2] = this.MDbuf[3] + e + A;
this.MDbuf[3] = this.MDbuf[4] + a + B;
this.MDbuf[4] = this.MDbuf[0] + b + C;
this.MDbuf[0] = D;
};
RIPEMD160.prototype.MDfinish = function (array, lswlen, mswlen) {
var X = array;
X[(lswlen >> 2) & 15] ^= 1 << (((lswlen & 3) << 3) + 7);
if (((lswlen & 63) > 55)) {
this.compress(X);
for (var i = 0; i < 14; i++) {
X[i] = 0;
}
}
X[14] = lswlen << 3;
X[15] = (lswlen >> 29) | (mswlen << 3);
this.compress(X);
};
RIPEMD160.prototype.update = function (input) {
for (var i = 0; i < input.length; i++) {
this.working[this.working_ptr >> 2] ^= input[i] << ((this.working_ptr & 3) << 3);
this.working_ptr++;
if ((this.working_ptr == 64)) {
this.compress(this.working);
for (var j = 0; j < 16; j++) {
this.working[j] = 0;
}
this.working_ptr = 0;
}
}
this.msglen += input.length;
};
RIPEMD160.prototype.digestBin = function () {
this.MDfinish(this.working, this.msglen, 0);
//var res = new Int8Array();
var res = [];
for (var i = 0; i < 20; i++) {
res[i] = ((this.MDbuf[i >> 2] >>> ((i & 3) << 3)) & 255);
}
return new Uint8Array(res);
};
RIPEMD160.prototype.digest = function (input) {
this.update(new Int8Array(input));
return this.digestBin();
};
RIPEMD160.ArgArray = [[11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6], [8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11]];
RIPEMD160.IndexArray = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13], [5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11]];
return RIPEMD160;
})();
</script>
</head>
<body>
Making initial call to API...
<p>
If nothing happens then check API is running!
</body>
</html>

39
pom.xml
View File

@ -5,6 +5,7 @@
<artifactId>qora-core</artifactId>
<version>2.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<swagger-ui.version>3.19.0</swagger-ui.version>
</properties>
<build>
@ -19,6 +20,36 @@
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>
Start
</mainClass>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Class-Path>. ..</Class-Path>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>Qora</finalName>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
<!-- unpack swagger-ui to target folder -->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
@ -42,7 +73,7 @@
</execution>
</executions>
</plugin>
<!-- inject correct url to swagger json file into swwagger-ui -->
<!-- inject correct url to swagger json file into swagger-ui -->
<plugin>
<groupId>com.google.code.maven-replacer-plugin</groupId>
<artifactId>replacer</artifactId>
@ -154,6 +185,12 @@
<version>9.4.11.v20180605</version>
<type>jar</type>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-servlets -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlets</artifactId>
<version>9.4.11.v20180605</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>

View File

@ -1,17 +1,19 @@
package api;
import data.account.AccountData;
import data.block.BlockData;
import globalization.Translator;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@ -19,6 +21,8 @@ import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import data.account.AccountBalanceData;
import qora.account.Account;
import qora.assets.Asset;
import qora.crypto.Crypto;
@ -28,11 +32,11 @@ import utils.Base58;
@Path("addresses")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@OpenAPIDefinition(
extensions = @Extension(name = "translation", properties = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/AddressesResource")
})
}
)
@Tag(name = "addresses")
public class AddressesResource {
@Context
@ -51,7 +55,8 @@ public class AddressesResource {
@GET
@Path("/lastreference/{address}")
@Operation(
description = "Returns the 64-byte long base58-encoded signature of last transaction where the address is delivered as creator. Or the first incoming transaction. Returns \"false\" if there is no transactions.",
summary = "Fetch reference for next transaction to be created by address",
description = "Returns the 64-byte long base58-encoded signature of last transaction created by address, failing that: the first incoming transaction to address. Returns \"false\" if there is no transactions.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET lastreference:address"),
@ -82,16 +87,15 @@ public class AddressesResource {
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
account.getLastReference();
lastReference = account.getLastReference();
} catch (ApiException e) {
throw e;
} catch (Exception e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
}
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
}
if(lastReference == null || lastReference.length == 0) {
return "false";
} else {
@ -102,7 +106,8 @@ public class AddressesResource {
@GET
@Path("/lastreference/{address}/unconfirmed")
@Operation(
description = "Returns the 64-byte long base58-encoded signature of last transaction including unconfirmed where the address is delivered as creator. Or the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the 64-byte long base58-encoded signature of last transaction, including unconfirmed, created by address, failing that: the first incoming transaction. Returns \\\"false\\\" if there is no transactions.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET lastreference:address:unconfirmed"),
@ -127,19 +132,36 @@ public class AddressesResource {
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/lastreference", request);
// XXX: is this method needed?
throw new UnsupportedOperationException();
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
lastReference = account.getUnconfirmedLastReference();
} catch (ApiException e) {
throw e;
} catch (Exception e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
}
if(lastReference == null || lastReference.length == 0) {
return "false";
} else {
return Base58.encode(lastReference);
}
}
@GET
@Path("/validate/{address}")
@Operation(
description = "Validates the given address. Returns true/false.",
summary = "Validates the given address",
description = "Returns true/false.",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET validate:address"),
@ExtensionProperty(name="description.key", value="operation:description")
@ExtensionProperty(name="summary.key", value="operation:summary"),
@ExtensionProperty(name="description.key", value="operation:description"),
})
},
responses = {
@ -203,7 +225,7 @@ public class AddressesResource {
}
@GET
@Path("balance/{address}")
@Path("/balance/{address}")
@Operation(
description = "Returns the confirmed balance of the given address.",
extensions = {
@ -218,7 +240,7 @@ public class AddressesResource {
responses = {
@ApiResponse(
description = "the balance",
content = @Content(schema = @Schema(implementation = BigDecimal.class)),
content = @Content(schema = @Schema(name = "balance", type = "number")),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@ -236,7 +258,6 @@ public class AddressesResource {
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(Asset.QORA);
} catch (ApiException e) {
throw e;
} catch (Exception e) {
@ -245,7 +266,7 @@ public class AddressesResource {
}
@GET
@Path("assetbalance/{assetid}/{address}")
@Path("/assetbalance/{assetid}/{address}")
@Operation(
description = "Returns the confirmed balance of the given address for the given asset key.",
extensions = {
@ -278,7 +299,6 @@ public class AddressesResource {
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
return account.getConfirmedBalance(assetid);
} catch (ApiException e) {
throw e;
} catch (Exception e) {
@ -287,7 +307,7 @@ public class AddressesResource {
}
@GET
@Path("assets/{address}")
@Path("/assets/{address}")
@Operation(
description = "Returns the list of assets for this address with balances.",
extensions = {
@ -302,7 +322,7 @@ public class AddressesResource {
responses = {
@ApiResponse(
description = "the list of assets",
content = @Content(schema = @Schema(implementation = String.class)),
content = @Content(array = @ArraySchema(schema = @Schema(implementation = AccountBalanceData.class))),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
@ -311,14 +331,23 @@ public class AddressesResource {
)
}
)
public String getAssetBalance(@PathParam("address") String address) {
public List<AccountBalanceData> getAssets(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET addresses/assets", request);
throw new UnsupportedOperationException();
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getAccountRepository().getAllBalances(address);
} catch (ApiException e) {
throw e;
} catch (Exception e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
}
}
@GET
@Path("balance/{address}/{confirmations}")
@Path("/balance/{address}/{confirmations}")
@Operation(
description = "Calculates the balance of the given address after the given confirmations.",
extensions = {

114
src/api/AdminResource.java Normal file
View File

@ -0,0 +1,114 @@
package api;
import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import repository.DataException;
import repository.RepositoryManager;
@Path("admin")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/AdminResource")
}
)
@Tag(name = "admin")
public class AdminResource {
@Context
HttpServletRequest request;
private static final long startTime = System.currentTimeMillis();
private ApiErrorFactory apiErrorFactory;
public AdminResource() {
this(new ApiErrorFactory(Translator.getInstance()));
}
public AdminResource(ApiErrorFactory apiErrorFactory) {
this.apiErrorFactory = apiErrorFactory;
}
@GET
@Path("/uptime")
@Operation(
summary = "Fetch running time of server",
description = "Returns uptime in milliseconds",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="operation:description")
})
},
responses = {
@ApiResponse(
description = "uptime in milliseconds",
content = @Content(schema = @Schema(implementation = String.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String uptime() {
Security.checkApiCallAllowed("GET admin/uptime", request);
return Long.toString(System.currentTimeMillis() - startTime);
}
@GET
@Path("/stop")
@Operation(
summary = "Shutdown",
description = "Shutdown",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="operation:description")
})
},
responses = {
@ApiResponse(
description = "\"true\"",
content = @Content(schema = @Schema(implementation = String.class)),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public String shutdown() {
Security.checkApiCallAllowed("GET admin/stop", request);
try {
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
ApiService.getInstance().stop();
}
}).start();
return "true";
}
}

View File

@ -1,4 +1,3 @@
package api;
import com.fasterxml.jackson.databind.node.ArrayNode;

View File

@ -0,0 +1,25 @@
package api;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.tags.Tag;
@OpenAPIDefinition(
info = @Info( title = "Qora API", description = "NOTE: byte-arrays currently returned as Base64 but this is likely to change to Base58" ),
tags = {
@Tag(name = "addresses"),
@Tag(name = "admin"),
@Tag(name = "blocks"),
@Tag(name = "transactions")
},
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="title.key", value="info:title")
})
}
)
public class ApiDefinition {
}

View File

@ -1,4 +1,3 @@
package api;
public enum ApiError {
@ -7,6 +6,8 @@ public enum ApiError {
JSON(1, 400),
NO_BALANCE(2, 422),
NOT_YET_RELEASED(3, 422),
UNAUTHORIZED(4, 401),
REPOSITORY_ISSUE(5, 500),
//VALIDATION
INVALID_SIGNATURE(101, 400),

View File

@ -34,6 +34,8 @@ public class ApiErrorFactory {
this.errorMessages.put(ApiError.JSON, createErrorMessageEntry(ApiError.JSON, "failed to parse json message"));
this.errorMessages.put(ApiError.NO_BALANCE, createErrorMessageEntry(ApiError.NO_BALANCE, "not enough balance"));
this.errorMessages.put(ApiError.NOT_YET_RELEASED, createErrorMessageEntry(ApiError.NOT_YET_RELEASED, "that feature is not yet released"));
this.errorMessages.put(ApiError.UNAUTHORIZED, createErrorMessageEntry(ApiError.UNAUTHORIZED, "api call unauthorized"));
this.errorMessages.put(ApiError.REPOSITORY_ISSUE, createErrorMessageEntry(ApiError.REPOSITORY_ISSUE, "repository error"));
//VALIDATION
this.errorMessages.put(ApiError.INVALID_SIGNATURE, createErrorMessageEntry(ApiError.INVALID_SIGNATURE, "invalid signature"));
@ -68,13 +70,13 @@ public class ApiErrorFactory {
this.errorMessages.put(ApiError.WALLET_ADDRESS_NO_EXISTS, createErrorMessageEntry(ApiError.WALLET_ADDRESS_NO_EXISTS, "address does not exist in wallet"));
this.errorMessages.put(ApiError.WALLET_LOCKED, createErrorMessageEntry(ApiError.WALLET_LOCKED, "wallet is locked"));
this.errorMessages.put(ApiError.WALLET_ALREADY_EXISTS, createErrorMessageEntry(ApiError.WALLET_ALREADY_EXISTS, "wallet already exists"));
this.errorMessages.put(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, createErrorMessageEntry(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, "user denied api call"));
this.errorMessages.put(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, createErrorMessageEntry(ApiError.WALLET_API_CALL_FORBIDDEN_BY_USER, "wallet denied api call"));
//BLOCK
this.errorMessages.put(ApiError.BLOCK_NO_EXISTS, createErrorMessageEntry(ApiError.BLOCK_NO_EXISTS, "block does not exist"));
//TRANSACTIONS
this.errorMessages.put(ApiError.TRANSACTION_NO_EXISTS, createErrorMessageEntry(ApiError.TRANSACTION_NO_EXISTS, "transactions does not exist"));
this.errorMessages.put(ApiError.TRANSACTION_NO_EXISTS, createErrorMessageEntry(ApiError.TRANSACTION_NO_EXISTS, "transaction does not exist"));
this.errorMessages.put(ApiError.PUBLIC_KEY_NOT_FOUND, createErrorMessageEntry(ApiError.PUBLIC_KEY_NOT_FOUND, "public key not found"));
//NAMING
@ -130,7 +132,7 @@ public class ApiErrorFactory {
//MESSAGES
this.errorMessages.put(ApiError.MESSAGE_FORMAT_NOT_HEX, createErrorMessageEntry(ApiError.MESSAGE_FORMAT_NOT_HEX, "the Message format is not hex - correct the text or use isTextMessage = true"));
this.errorMessages.put(ApiError.MESSAGE_BLANK, createErrorMessageEntry(ApiError.MESSAGE_BLANK, "The message attribute is missing or content is blank"));
this.errorMessages.put(ApiError.NO_PUBLIC_KEY, createErrorMessageEntry(ApiError.NO_PUBLIC_KEY, "The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to him."));
this.errorMessages.put(ApiError.NO_PUBLIC_KEY, createErrorMessageEntry(ApiError.NO_PUBLIC_KEY, "The recipient has not yet performed any action in the blockchain.\nYou can't send an encrypted message to them."));
this.errorMessages.put(ApiError.MESSAGESIZE_EXCEEDED, createErrorMessageEntry(ApiError.MESSAGESIZE_EXCEEDED, "Message size exceeded!"));
}

View File

@ -1,6 +1,7 @@
package api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
@ -10,8 +11,10 @@ import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.FilterHolder;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
@ -21,16 +24,19 @@ public class ApiService {
private final Server server;
private final Set<Class<?>> resources;
public ApiService() {
// resources to register
this.resources = new HashSet<Class<?>>();
this.resources.add(AddressesResource.class);
this.resources.add(AdminResource.class);
this.resources.add(BlocksResource.class);
this.resources.add(TransactionsResource.class);
this.resources.add(OpenApiResource.class); // swagger
this.resources.add(ApiDefinition.class); // for API definition
this.resources.add(AnnotationPostProcessor.class); // for API resource annotations
ResourceConfig config = new ResourceConfig(this.resources);
// create RPC server
this.server = new Server(Settings.getInstance().getRpcPort());
@ -49,25 +55,31 @@ public class ApiService {
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
context.setContextPath("/");
rewriteHandler.setHandler(context);
FilterHolder filterHolder = new FilterHolder(CrossOriginFilter.class);
filterHolder.setInitParameter("allowedOrigins", "*");
filterHolder.setInitParameter("allowedMethods", "GET, POST");
context.addFilter(filterHolder, "/*", null);
// API servlet
ServletContainer container = new ServletContainer(config);
ServletHolder apiServlet = new ServletHolder(container);
apiServlet.setInitOrder(1);
context.addServlet(apiServlet, "/*");
// Swagger-UI static content
ClassLoader loader = this.getClass().getClassLoader();
File swaggerUIResourceLocation = new File(loader.getResource("resources/swagger-ui/").getFile());
ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class);
swaggerUIServlet.setInitParameter("resourceBase", swaggerUIResourceLocation.getAbsolutePath());
swaggerUIServlet.setInitParameter("dirAllowed","true");
swaggerUIServlet.setInitParameter("pathInfoOnly","true");
context.addServlet(swaggerUIServlet,"/api-documentation/*");
ServletHolder swaggerUIServlet = new ServletHolder("static-swagger-ui", DefaultServlet.class);
swaggerUIServlet.setInitParameter("resourceBase", swaggerUIResourceLocation.getAbsolutePath());
swaggerUIServlet.setInitParameter("dirAllowed", "true");
swaggerUIServlet.setInitParameter("pathInfoOnly", "true");
context.addServlet(swaggerUIServlet, "/api-documentation/*");
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/index.html")); // redirect to swagger ui start page
}
//XXX: replace singleton pattern by dependency injection?
// XXX: replace singleton pattern by dependency injection?
private static ApiService instance;
public static ApiService getInstance() {
@ -77,26 +89,26 @@ public class ApiService {
return instance;
}
Iterable<Class<?>> getResources() {
return resources;
}
public void start() {
try {
//START RPC
// START RPC
server.start();
} catch (Exception e) {
//FAILED TO START RPC
// FAILED TO START RPC
}
}
public void stop() {
try {
//STOP RPC
// STOP RPC
server.stop();
} catch (Exception e) {
//FAILED TO STOP RPC
// FAILED TO STOP RPC
}
}
}

View File

@ -2,13 +2,14 @@ package api;
import data.block.BlockData;
import globalization.Translator;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;
@ -19,18 +20,18 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import qora.block.Block;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import utils.Base58;
@Path("blocks")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@OpenAPIDefinition(
extensions = @Extension(name = "translation", properties = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/BlocksResource")
})
}
)
@Tag(name = "blocks")
public class BlocksResource {
@Context
@ -49,6 +50,7 @@ public class BlocksResource {
@GET
@Path("/{signature}")
@Operation(
summary = "Fetch block using base58 signature",
description = "returns the block that matches the given signature",
extensions = {
@Extension(name = "translation", properties = {
@ -104,6 +106,7 @@ public class BlocksResource {
@GET
@Path("/first")
@Operation(
summary = "Fetch genesis block",
description = "returns the genesis block",
extensions = @Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET first"),
@ -138,6 +141,7 @@ public class BlocksResource {
@GET
@Path("/last")
@Operation(
summary = "Fetch last/newest block in blockchain",
description = "returns the last valid block",
extensions = @Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET last"),
@ -172,6 +176,7 @@ public class BlocksResource {
@GET
@Path("/child/{signature}")
@Operation(
summary = "Fetch child block using base58 signature of parent block",
description = "returns the child block of the block that matches the given signature",
extensions = {
@Extension(name = "translation", properties = {
@ -199,36 +204,31 @@ public class BlocksResource {
// decode signature
byte[] signatureBytes;
try
{
try {
signatureBytes = Base58.decode(signature);
}
catch(Exception e)
{
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
} catch (NumberFormatException e) {
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signatureBytes);
// check if block exists
if(blockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
int height = blockData.getHeight();
BlockData childBlockData = repository.getBlockRepository().fromHeight(height + 1);
BlockData childBlockData = repository.getBlockRepository().fromReference(signatureBytes);
// check if child exists
if(childBlockData == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return childBlockData;
} catch (ApiException e) {
throw e;
} catch (Exception e) {
throw this.apiErrorFactory.createError(ApiError.UNKNOWN, e);
}
} catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET

View File

@ -5,6 +5,8 @@ import javax.servlet.http.HttpServletRequest;
public class Security {
public static void checkApiCallAllowed(final String messageToDisplay, HttpServletRequest request) {
// TODO
// TODO
// throw this.apiErrorFactory.createError(ApiError.UNAUTHORIZED);
}
}

View File

@ -0,0 +1,160 @@
package api;
import globalization.Translator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import qora.crypto.Crypto;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import data.transaction.TransactionData;
import repository.DataException;
import repository.Repository;
import repository.RepositoryManager;
import repository.TransactionRepository;
import utils.Base58;
@Path("transactions")
@Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="/Api/TransactionsResource")
}
)
@Tag(name = "transactions")
public class TransactionsResource {
@Context
HttpServletRequest request;
private ApiErrorFactory apiErrorFactory;
public TransactionsResource() {
this(new ApiErrorFactory(Translator.getInstance()));
}
public TransactionsResource(ApiErrorFactory apiErrorFactory) {
this.apiErrorFactory = apiErrorFactory;
}
@GET
@Path("/address/{address}")
@Operation(
summary = "Fetch transactions involving address",
description = "Returns list of transactions",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET block:signature"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_ADDRESS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "list of transactions",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public List<TransactionData> getAddressTransactions(@PathParam("address") String address) {
Security.checkApiCallAllowed("GET transactions/address", request);
if (!Crypto.isValidAddress(address))
throw this.apiErrorFactory.createError(ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository txRepo = repository.getTransactionRepository();
List<byte[]> signatures = txRepo.getAllSignaturesInvolvingAddress(address);
// Pagination would take effect here (or as part of the repository access)
// Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<TransactionData>(signatures.size());
for (byte[] signature : signatures)
transactions.add(txRepo.fromSignature(signature));
return transactions;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/block/{signature}")
@Operation(
summary = "Fetch transactions via block signature",
description = "Returns list of transactions",
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="path", value="GET block:signature"),
@ExtensionProperty(name="description.key", value="operation:description")
}),
@Extension(properties = {
@ExtensionProperty(name="apiErrors", value="[\"INVALID_SIGNATURE\", \"BLOCK_NO_EXISTS\"]", parseValue = true),
})
},
responses = {
@ApiResponse(
description = "list of transactions",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransactionData.class))),
extensions = {
@Extension(name = "translation", properties = {
@ExtensionProperty(name="description.key", value="success_response:description")
})
}
)
}
)
public List<TransactionData> getBlockTransactions(@PathParam("signature") String signature) {
Security.checkApiCallAllowed("GET transactions/block", request);
// decode signature
byte[] signatureBytes;
try {
signatureBytes = Base58.decode(signature);
} catch (NumberFormatException e) {
throw this.apiErrorFactory.createError(ApiError.INVALID_SIGNATURE, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<TransactionData> transactions = repository.getBlockRepository().getTransactionsFromSignature(signatureBytes);
// check if block exists
if(transactions == null)
throw this.apiErrorFactory.createError(ApiError.BLOCK_NO_EXISTS);
return transactions;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw this.apiErrorFactory.createError(ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -6,11 +6,15 @@ import org.apache.logging.log4j.Logger;
import qora.block.BlockChain;
import qora.block.BlockGenerator;
import repository.DataException;
import repository.RepositoryFactory;
import repository.RepositoryManager;
import repository.hsqldb.HSQLDBRepositoryFactory;
import utils.Base58;
public class blockgenerator {
private static final Logger LOGGER = LogManager.getLogger(blockgenerator.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static void main(String[] args) {
if (args.length != 1) {
@ -29,7 +33,8 @@ public class blockgenerator {
}
try {
test.Common.setRepository();
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
LOGGER.error("Couldn't connect to repository", e);
System.exit(2);
@ -58,7 +63,7 @@ public class blockgenerator {
}
try {
test.Common.closeRepository();
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
// TODO Auto-generated catch block
e.printStackTrace();

View File

@ -2,15 +2,25 @@ package data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class AccountBalanceData {
// Properties
protected String address;
protected long assetId;
protected BigDecimal balance;
private String address;
private long assetId;
private BigDecimal balance;
// Constructors
// necessary for JAX-RS serialization
@SuppressWarnings("unused")
private AccountBalanceData() {
}
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
this.address = address;
this.assetId = assetId;

View File

@ -1,10 +1,15 @@
package data.block;
import com.google.common.primitives.Bytes;
import java.io.Serializable;
import java.math.BigDecimal;
import com.google.common.primitives.Bytes;
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockData implements Serializable {
private static final long serialVersionUID = -7678329659124664620L;

View File

@ -4,8 +4,13 @@ import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import qora.transaction.Transaction.TransactionType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public abstract class TransactionData {
// Properties shared with all transaction types

View File

@ -7,6 +7,7 @@ import java.sql.SQLException;
*
*/
@SuppressWarnings("serial")
@Deprecated
public class NoDataFoundException extends SQLException {
public NoDataFoundException() {

View File

@ -3,10 +3,14 @@ import qora.block.Block;
import qora.block.BlockChain;
import repository.DataException;
import repository.Repository;
import repository.RepositoryFactory;
import repository.RepositoryManager;
import repository.hsqldb.HSQLDBRepositoryFactory;
public class orphan {
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: orphan <new-blockchain-tip-height>");
@ -16,7 +20,8 @@ public class orphan {
int targetHeight = Integer.parseInt(args[0]);
try {
test.Common.setRepository();
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println("Couldn't connect to repository: " + e.getMessage());
System.exit(2);
@ -43,7 +48,7 @@ public class orphan {
}
try {
test.Common.closeRepository();
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
e.printStackTrace();
}

View File

@ -1,6 +1,8 @@
package qora.account;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -8,6 +10,7 @@ import org.apache.logging.log4j.Logger;
import data.account.AccountBalanceData;
import data.account.AccountData;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.assets.Asset;
import qora.block.Block;
import qora.block.BlockChain;
@ -144,6 +147,32 @@ public class Account {
return accountData.getReference();
}
/**
* Fetch last reference for account, considering unconfirmed transactions.
*
* @return byte[] reference, or null if no reference or account not found.
* @throws DataException
*/
public byte[] getUnconfirmedLastReference() throws DataException {
// Newest unconfirmed transaction takes priority
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
byte[] reference = null;
for (TransactionData transactionData : unconfirmedTransactions) {
String address = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
if (address.equals(this.accountData.getAddress()))
reference = transactionData.getSignature();
}
if (reference != null)
return reference;
// No unconfirmed transactions
return getLastReference();
}
/**
* Set last reference for account.
*

View File

@ -1,5 +1,7 @@
package repository;
import java.util.List;
import data.account.AccountBalanceData;
import data.account.AccountData;
@ -19,6 +21,8 @@ public interface AccountRepository {
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
public List<AccountBalanceData> getAllBalances(String address) throws DataException;
public void save(AccountBalanceData accountBalanceData) throws DataException;
public void delete(String address, long assetId) throws DataException;

View File

@ -20,6 +20,8 @@ public interface TransactionRepository {
@Deprecated
public BlockData getBlockDataFromSignature(byte[] signature) throws DataException;
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException;
/**
* Returns list of unconfirmed transactions in timestamp-else-signature order.
*

View File

@ -3,6 +3,8 @@ package repository.hsqldb;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import data.account.AccountBalanceData;
import data.account.AccountData;
@ -84,6 +86,27 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public List<AccountBalanceData> getAllBalances(String address) throws DataException {
List<AccountBalanceData> balances = new ArrayList<AccountBalanceData>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT asset_id, balance FROM AccountBalances WHERE account = ?", address)) {
if (resultSet == null)
return balances;
do {
long assetId = resultSet.getLong(1);
BigDecimal balance = resultSet.getBigDecimal(2).setScale(8);
balances.add(new AccountBalanceData(address, assetId, balance));
} while (resultSet.next());
return balances;
} catch (SQLException e) {
throw new DataException("Unable to fetch account balances from repository", e);
}
}
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");

View File

@ -265,6 +265,27 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public List<byte[]> getAllSignaturesInvolvingAddress(String address) throws DataException {
List<byte[]> signatures = new ArrayList<byte[]>();
// XXX We need a table for all parties involved in a transaction, not just recipients
try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM TransactionRecipients WHERE recipient = ?", address)) {
if (resultSet == null)
return signatures;
do {
byte[] signature = resultSet.getBytes(1);
signatures.add(signature);
} while (resultSet.next());
return signatures;
} catch (SQLException e) {
throw new DataException("Unable to fetch involved transaction signatures from repository", e);
}
}
@Override
public List<TransactionData> getAllUnconfirmedTransactions() throws DataException {
List<TransactionData> transactions = new ArrayList<TransactionData>();

View File

@ -4,13 +4,17 @@ import data.transaction.TransactionData;
import qora.block.BlockChain;
import repository.DataException;
import repository.Repository;
import repository.RepositoryFactory;
import repository.RepositoryManager;
import repository.hsqldb.HSQLDBRepositoryFactory;
import transform.TransformationException;
import transform.transaction.TransactionTransformer;
import utils.Base58;
public class txhex {
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: txhex <base58-tx-signature>");
@ -20,7 +24,8 @@ public class txhex {
byte[] signature = Base58.decode(args[0]);
try {
test.Common.setRepository();
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println("Couldn't connect to repository: " + e.getMessage());
System.exit(2);
@ -42,7 +47,7 @@ public class txhex {
}
try {
test.Common.closeRepository();
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
e.printStackTrace();
}

View File

@ -43,7 +43,9 @@ import qora.block.BlockChain;
import qora.crypto.Crypto;
import repository.DataException;
import repository.Repository;
import repository.RepositoryFactory;
import repository.RepositoryManager;
import repository.hsqldb.HSQLDBRepositoryFactory;
import transform.TransformationException;
import transform.block.BlockTransformer;
import transform.transaction.ATTransactionTransformer;
@ -54,6 +56,7 @@ import utils.Triple;
public class v1feeder extends Thread {
private static final Logger LOGGER = LogManager.getLogger(v1feeder.class);
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
private static final int INACTIVITY_TIMEOUT = 60 * 1000; // milliseconds
private static final int CONNECTION_TIMEOUT = 2 * 1000; // milliseconds
@ -526,7 +529,8 @@ public class v1feeder extends Thread {
readLegacyATs(legacyATPathname);
try {
test.Common.setRepository();
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
LOGGER.error("Couldn't connect to repository", e);
System.exit(2);
@ -552,7 +556,7 @@ public class v1feeder extends Thread {
LOGGER.info("Exiting v1feeder");
try {
test.Common.closeRepository();
RepositoryManager.closeRepositoryFactory();
} catch (DataException e) {
e.printStackTrace();
}