mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-16 20:22:32 +00:00
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:
parent
9dcdcb0ffe
commit
ad9fa9bf9d
4
.settings/org.eclipse.core.resources.prefs
Normal file
4
.settings/org.eclipse.core.resources.prefs
Normal 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
789
block-explorer.html
Normal 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
39
pom.xml
@ -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>
|
||||
|
@ -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
114
src/api/AdminResource.java
Normal 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";
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
|
||||
package api;
|
||||
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
|
25
src/api/ApiDefinition.java
Normal file
25
src/api/ApiDefinition.java
Normal 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 {
|
||||
|
||||
}
|
@ -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),
|
||||
|
@ -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!"));
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
160
src/api/TransactionsResource.java
Normal file
160
src/api/TransactionsResource.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -7,6 +7,7 @@ import java.sql.SQLException;
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
@Deprecated
|
||||
public class NoDataFoundException extends SQLException {
|
||||
|
||||
public NoDataFoundException() {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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");
|
||||
|
@ -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>();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user