Compare commits

...

269 Commits

Author SHA1 Message Date
catbref
7bb060781e Bump to v1.3.0 - including new trade-portal feature 2020-08-06 15:40:11 +01:00
catbref
a1ab0b7c31 Added more nodes to initial list 2020-08-06 15:38:52 +01:00
catbref
fae2afd010 Remove obsolete github repo from potential auto-update sites 2020-08-06 15:23:13 +01:00
catbref
76c0a5a4fa Increase default Bitcoin transaction fee to 5000 sats 2020-08-06 13:12:54 +01:00
catbref
cdb65657b6 Added qortal.ru nodes to initial nodes list used when creating DB 2020-08-06 09:51:14 +01:00
catbref
9007dfe779 Added API call POST /crosschain/btc/send for sending Bitcoin 2020-08-06 09:46:08 +01:00
catbref
99d09a9877 Change HTTP response codes for BTC_BALANCE_ISSUE and BTC_TOO_SOON from 422 to 402 & 408 2020-08-06 09:45:38 +01:00
catbref
afcf51399e Minor post-merge fix-up 2020-08-06 09:18:08 +01:00
catbref
47679b7f6c Merge branch 'trade-bot' 2020-08-06 09:00:45 +01:00
catbref
8f2985862d Update BTC-ACCT 'cancel' API call to expect AT creator's as sender 2020-08-06 08:52:51 +01:00
catbref
23a524b464 BTC-ACCT: change AT so 'cancel' MESSAGE needs to come from AT creator's address (not trade address) so fee can be used instead of PoW for faster cancels 2020-08-06 08:23:49 +01:00
catbref
ce8992867d Include last 24 hours of CANCELLED & REFUNDED trade offers in first message 2020-08-05 20:58:25 +01:00
catbref
c89de7adfb Add creatorAddress, qortAmount and (last updated) timestamp to trade-bot entries 2020-08-05 16:00:40 +01:00
catbref
cac68ccc14 Added trade-bot websocket 2020-08-05 13:23:24 +01:00
catbref
d507383487 Rework ApiWebSocket so it can manage sessions and in readiness to conversion from "notifiers" to event-bus 2020-08-05 13:23:07 +01:00
catbref
ce5cf87094 Added unified, simple event bus to eventually replace controller "notifiers" 2020-08-05 13:20:19 +01:00
catbref
ec2c9d2a44 Improve /crosschain/tradebot/respond with varied API errors such as BTC_BALANCE_ISSUE, BTC_NETWORK_ISSUE, etc. instead of just "false" 2020-08-05 10:05:09 +01:00
catbref
36d0abe635 WIP: trade-bot: log warning when we can't fund P2SH-B for some reason 2020-08-05 10:04:25 +01:00
catbref
615381ca5a Fix BTC spend txn building to be less aggressive about caching/checking spent keys 2020-08-05 10:03:08 +01:00
catbref
6b83499216 WIP: trade-bot: add support for showing trade partner's Qortal receiving address in trade offer summaries 2020-08-04 20:35:22 +01:00
catbref
faa2e9502b WIP: trade-bot: include creation/latest timestamp (as appropriate) in trade offer summaries via websocket 2020-08-04 17:00:36 +01:00
catbref
cd07240ce7 Add BTC.getWalletBalance(xprv) and add API call to access that.
Also improved BTC.WalletAwareUTXOProvider to derive more keys itself
instead of throwing and relying on caller to do the work.
Added benefit of cleaning up caller code and being more efficient.
Needed because not all receiving/change addresses were being picked up.
2020-08-04 16:37:44 +01:00
catbref
91518464c2 WIP: trade-bot: fix empty bitcoin wallet edge case when finding UTXOs 2020-08-04 12:26:09 +01:00
catbref
25bf315e23 WIP: trade-bot: tradeoffers websocket initial message with OFFERING/REDEEMED and fixed subsequent messages 2020-08-04 11:21:57 +01:00
catbref
a8743b1bd3 ElectrumX network main-net servers 2020-08-03 19:28:55 +01:00
catbref
f90bd6ee45 WIP: trade-bot: added WS for streaming existing/new trades in OFFERING state 2020-08-03 17:57:22 +01:00
catbref
a351756883 WIP: trade-bot: add missing JavaTypeAdapter to TradeBotData.bitcoinAmount
Also: unify "receiveAddress" to "receivingAddress"

Fix missed changes from "recipient" to "partner" in BTCACCT, etc.
2020-08-03 15:52:17 +01:00
catbref
ea9b0d4588 WIP: trade-bot: initial API call for listing completed trades
Also: renamed trade bot field/column "receiving_public_key_hash"
to "receiving_account_info" as Alice's trade bot uses it to
store Alice's Qortal address, not PKH.

Added some extra simplistic repository calls to support above,
like BlockRepository.getTimestampFromHeight,
ATRepository.getCreatorPublicKey(atAddress)
2020-08-03 14:54:45 +01:00
catbref
e9c85c946e WIP: trade-bot: two more unit tests to cover some edge cases 2020-08-03 10:49:47 +01:00
catbref
876bfb525b WIP: trade-bot: more receive address support, some terminology clarification
Bitcoin receive address no longer stored in AT but dealt with by trade-bot.
This allows 'Bob' to have his BTC sent anywhere he likes when redeeming P2SH-A
thus saving a step, typically incurred by UI. DB shape change due to this.

Similarly, AT code has been updated to expect a Qortal receiving address when
Alice sends MESSAGE to redeem AT.

This means both trade-bot entries (Alice/Bob) can be safely wiped once trade completes.

Some terms were confusing like "trade recipient" which actually referred to
Alice and so have been unified as "trade partner" as to not be confused with
(say) "recipient address"

The MESSAGEs sent from Alice to Bob, from Bob to AT and from Alice to AT have been
given more useful names: 'offer', 'trade' and 'redeem'. There is also a cancel
MESSAGE sent from Bob to AT to cancel AT before trading occurs.

Some API calls have been renamed in light of above.

AT's 'mode' has been expanded from simply OFFER/TRADE to:
OFFERING, TRADING, REFUNDED, REDEEMED, CANCELLED

Tests updated, but MORE TESTING REQUIRED BEFORE RELEASE
2020-08-03 09:36:46 +01:00
catbref
6be67d0d92 WIP: trade-bot: make sure the "trade" private key is valid for both Curve25519 and secp256k1 2020-07-30 08:12:45 +01:00
catbref
16581766c6 WIP: trade-bot: detect and remove mempool entries from ElectrumX "listunspent" results 2020-07-29 20:48:06 +01:00
catbref
7fd7104f46 WIP: trade-bot: add flag to be set by AT if redeem happens so trade-bot detects redeem instead of refund 2020-07-29 20:25:28 +01:00
catbref
d2cae7c8b5 WIP: trade-bot: use correct Bob Bitcoin receive address in log entry 2020-07-29 18:14:47 +01:00
catbref
83955acd22 WIP: trade-bot: allow trade-bot entries to be deleted if in BOB_WAITING_FOR_AT_CONFIRM state. Also, return false (instead of throwing internal error) if trade-bot entry does not exist 2020-07-29 18:13:27 +01:00
catbref
d85b746021 WIP: trade-bot: add xprv validation method to BTC class and use that for API call /crosschain/tradebot/respond instead of vague byte-length check 2020-07-29 18:11:47 +01:00
catbref
e2dc91c1ea Fix API call DELETE /crosschain/tradeoffer regarding PoW MESSAGE reference 2020-07-29 10:38:32 +01:00
catbref
098e2623d6 WIP: cross-chain AT now stores bitcoin receiving PKH 2020-07-28 17:21:54 +01:00
catbref
2df045396d Bump to v1.2.3 2020-07-28 11:27:21 +01:00
catbref
6c182a3567 Allow minting accounts to be removed from node using public key as well as private key 2020-07-28 10:45:06 +01:00
catbref
340d6dfc8d Add websocket error handler support 2020-07-27 10:16:21 +01:00
catbref
eb27b0d3e2 Blocks websocket now returns simpler block info 2020-07-24 14:52:14 +01:00
catbref
7377893050 WebSocket improvements, inc. bump Jetty to v9.4.29-20200521
Various issues in Jetty v9.4.22 (and some later versions too)
cause websockets to use up all available threads.

Bumped Jetty to v9.4.29 to resolve some of these issues.

Changed some Qortal-side websocket code to minimize
locking on websocket notifiers. Websocket messages now
sent async, although the returned Futures are discarded,
as it's up to the remote end to consume fast enough.

Changed Controller to only request a SysTray update before
synchronization if there's a chance node might change height.
Similarly, Controller only requests SysTray update after
synchronization if chain tip has actually changed.
Both of the above together should reduce the number of
messages sent out via the admin status websockets.
2020-07-24 10:34:42 +01:00
catbref
21d7a4eed1 Improved AT PUT_TX_AFTER_TIMESTAMP_INTO_A function
Previous version fetched all the blocks from previous 'timestamp'
to current height, checking each transaction. (very slow)

New implementation leverages repository to do the heavy lifting.

Could potentially benefit from some DB indexes in the future?

Added unit test to cover.
2020-07-23 08:43:27 +01:00
catbref
fb2c2b1d09 Added API call GET /blocks/summaries
Returns summary info about a range of blocks.

(Not to be confused with network-related BlockSummaries)
2020-07-20 13:05:43 +01:00
catbref
6f2dd6c8d0 Added some more useful tools/scripts, mostly for Linux-based curious node owners 2020-07-17 12:22:48 +01:00
catbref
4cc0e7845f Add instructions and files used to build installers for Windows
For this commit, the included .aip file, and qortal.jar, match
what was used to produce the installer for release v1.2.2.

In a future commit, maybe remove qortal.jar as it is only included
here to illustrate current location in build tree.

Updates to .aip file could be, and maybe even should be, committed.

This build toolchain uses AdvancedInstaller v16 or better but
may require an (expensive) enterprise licence. It is possible
to obtain an 'open source'-use free licence from AdvancedInstaller
by contacting them directly. However this may result in restricted
functionality with AdvancedInstaller and some installer features,
e.g. multi-language support, may have be to removed.
2020-07-17 12:08:25 +01:00
catbref
ca8eabc425 Commit 2 of 2: adding new CIYAM AT v.1.3.5 2020-07-17 11:46:39 +01:00
catbref
94c83d6a93 Commit 1 of 2: removing old CIYAM AT v1.3.4 2020-07-17 11:46:21 +01:00
catbref
dea2f34c52 Trade-bot: more comments, more documentation, more ElectrumX servers.
Bitcoin main-net ElectrumX server list added to ElectrumX class,
albeit commented out at this point until it is decided that trade-bot
is ready for production use. (Simply remove the leading //s)

More comments and documentation has been added to TradeBot class
to further describe the actions taken.

It is important to note that:

Bitcoin wallet access is required by trade-bot

and so:

A Bitcoin WALLET PRIVATE KEY is stored in the database by trade-bot

and hence, if you use trade-bot:

DO NOT DISTRIBUTE YOUR DB FILES TO ANYONE ELSE!

Furthermore it should be obvious that this functionality is provided on
a 'best effort", not guaranteed, basis, therefore:

YOUR FUNDS ARE AT RISK!

If you are unsure about any aspect, or cannot afford to lose your funds,
or it's possible that unexpected outcomes occur, then DO NOT USE.

To use trade-bot on Bitcoin TESTNET then this to your settings JSON file:
  "bitcoinNet": "TEST3",

See Settings.java line 100, and BTC class for more info.
2020-07-17 11:28:22 +01:00
catbref
b294f5e333 WIP: More defensive ElectrumX calls. Bring non-trade-bot API calls up to date 2020-07-17 08:39:45 +01:00
catbref
f9b726a75d WIP: TradeBot - added refunding code 2020-07-17 08:39:45 +01:00
catbref
579645d6b7 WIP: trade-bot now does complete end-to-end trade (more work needed)
bitcoinj now uses ElectrumX as an UTXO provider in order to keep track
of coins in BIP32 deterministic wallet.

Trade responder (Alice) needs to pass a BIP32 extended private key to API
so trade-bot can create unattended spends.

Both Alice and Bob can find their final funds in accounts using the
ephemeral 'tradePrivateKey' from trade-bot state data.

Most cross-chain API calls are now only allowed from localhost.

Most Bitcoin fees pegged at 0.00001000 BTC.

More work needed to handle refunds in case of trade failures.
(See XXX comment tags in TradeBot.java)
2020-07-17 08:39:45 +01:00
catbref
e729571a21 WIP: trade-bot: do not run trade-bot if not up-to-date 2020-07-17 08:39:45 +01:00
catbref
f179139967 WIP: trade-bot: Alice P2SH_a progress
Qortal AT now includes suggested tradeTimeout again as a constant so trade partner/recipient can use that to calculate a suitable lockTimeA. CODE_HASH changed!

Renamed some secret_hash to hash_of_secret.

Changed TradeBotStates.trade_state back to TINYINT and adjusted values in TradeBotData.State enum to suit.
Added lockTimeA to TradeBotData & repository.

Added JAXB-only extra representations of Bitcoin PKHs as addresses.

Fixed incorrect expected length in BTCACCT.extractOfferMessageData().

CrossChainTradeData.refundTimeout now only present in TRADE mode.

Added BTC.pkhToAddress().

Added initial TradeBot.handleAliceWaitingForP2shA().

Enforce only one TradeBot thread running using 'activeFlag' atomic boolean.

Replace incorrect SHA256 with HASH160 for hashOfSecretA in TradeBot.startResponse().
2020-07-17 08:39:45 +01:00
catbref
ee5119e4dd WIP: trade-bot. move trade-bot hook, fix bugs, etc.
Controller now calls TradeBot.onChainTipChange() inside thread
started by Controller.onNewBlock(), instead of blocking
Controller.setChainTip().

DB TradeBotStates has trade_foreign_public_key changed to VARBINARY(33)
as Bitcoin pubkeys aren't uniformly 32 bytes!
Also, trade_state changed from TINYINT to SMALLINT to cover enum value range.

TradeBot.createTrade() incorrectly used Crypto.digest() to create hash-of-secret
instead of Crypto.hash160(). Also corrected tradeState to
BOB_WAITING_FOR_AT_CONFIRM. Also added missing fee calculation.

Added missing repository.saveChanges() to TradeBot methods.

Added balance check to API POST /crosschain/tradebot before passing
request to TradeBot.createTrade(), which also ensures there's a
usable account last-reference too.
2020-07-17 08:39:45 +01:00
catbref
11bf5ac6fc WIP: remove trade_timeout from DB TradeBotStates & TradeBotCreateRequest 2020-07-17 08:39:45 +01:00
catbref
c3eb385066 WIP: cross-chain trading with new lockTimes, requires AT v1.3.5 2020-07-17 08:39:45 +01:00
catbref
886c9156a5 WIP: cross-chain trading AT passes AtTests now 2020-07-17 08:39:45 +01:00
catbref
23062c59cd WIP: trade-bot, particularly the new two-P2SH Qortal AT code 2020-07-17 08:39:45 +01:00
catbref
da254058c5 WIP: split P2SH from BTCACCT, add more fields to TradeBotData, remove initial QORT payout 2020-07-17 08:39:45 +01:00
catbref
a6fa4fc613 WIP: trade-bot MESSAGE support 2020-07-17 08:39:45 +01:00
catbref
593b61ea4b Reduce bitcoinj exposure to classes outside of org.qortal.crosschain package.
BTC.getBalance() now returns Long instead of Coin.

BTC.FORMAT.format(Coin) changed to BTC.format(Coin or long).

Added BTC.deriveP2shAddress(byte[] redeemScriptBytes).
2020-07-17 08:39:45 +01:00
catbref
04d691991a WIP: more work on trade-bot 2020-07-17 08:39:45 +01:00
catbref
faa6e82bef WIP on trade-bot 2020-07-17 08:39:45 +01:00
catbref
65ccb80aa4 AT-related changes: new Qortal functions, tests, etc.
Added GET_MESSAGE_LENGTH_FROM_TX_IN_A
and PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.

Replaced AT-1.3.4 with version including bug-fix for off-by-one
data address bounds checking.

Moved long-from-bytes method to BitTwiddling class.

Renamed some methods to make it more obvious they work with
little/big endian data.
2020-07-17 08:39:45 +01:00
catbref
cc13d1d0f1 WIP commit 2020-07-17 08:39:45 +01:00
catbref
ead84d70d1 Initial skeleton code for Trade Bot 2020-07-17 08:39:45 +01:00
catbref
275146fb55 Return REWARD_SHARE_UNKNOWN when trying to cancel non-existent reward-share 2020-07-14 09:53:14 +01:00
catbref
d81729d9f7 Bump to v1.2.2 2020-07-03 12:14:31 +01:00
catbref
e74a249388 Collate network PoW computes into a fixed-sized pool, with dead peer detection.
Also added Named/DaemonThreadFactory classes.

Network EPC now uses NamedThreadFactory for easier debugging.

Added settings field "networkPoWComputePoolSize", default 2, which
seems to work with both low-power ARM boards and high-power desktops.
2020-07-03 09:31:46 +01:00
catbref
d8c5e557d8 Update uiLocalServers, autoUpdateRepos and bump to v1.2.1 2020-06-30 15:41:53 +01:00
catbref
984e8b5227 Network optimizations: if we're not up to date then don't request, or send, unconfirmed transaction lists 2020-06-30 14:26:12 +01:00
catbref
469bf2a63e Improve inbound peer handshaking
If a node accepts a connection from an inbound peer
then remote peer will send RESPONSE first
and local node would previously change handshaking state
to COMPLETED while computing their own RESPONSE.

This meant that the local node would sometimes also start
sending post-handshake messages to the remote peer,
e.g. TRANSACTION_SIGNATURES.

Remote peer is only expecting a RESPONSE message, so would
close connection.

So we introduce an extra handshaking state "RESPONDING" for use
by local node while they compute RESPONSE in a separate thread.
Once the RESPONSE has been sent, local node moves to COMPLETED
state and called onHandshakeCompleted() as per usual.

Note that the code path when connecting outbound to a remote peer
is not changed, and the RESPONDING state is not used.

Also in this commit:

Network.onPeerReady now bypasses call to onMessage and instead
calls onHandshakingMessage() directly to avoid race condition
where peer's handshake status could change between
onPeerReady's caller and onMessage() calling peer.getHandshakeStatus()
2020-06-30 13:37:14 +01:00
catbref
e05fcd6655 Final genesis block & bump to v1.2.0 2020-06-29 15:45:10 +01:00
catbref
3a7751910e Fix /websockets/chat/messages so it disregards group-membership change notifications 2020-06-29 08:38:19 +01:00
catbref
3c139f3e53 Minor fix-up to allow go-live:
Re-add (for now) Ed25519 HSQLDB conversion.
Catch DataException in BlockChain.isGenesisBlockValid and return false.
Remove duplicate bug-fix for LeaveGroupTransactions DB table.

Interim, near-final blockchain.json
2020-06-26 15:06:04 +01:00
catbref
2c14a12464 Check for unknown Qortal-only AT function codes & add safety to AT running in general 2020-06-25 14:22:28 +01:00
catbref
faa6405d5f Reference fixes for MESSAGE transactions & tests to cover 2020-06-24 17:15:17 +01:00
catbref
e2e4555009 Added /websockets/admin/status and improved GET version.
NodeStatus contructor now fills in fields, which themselves are now 'final'.
NodeStatus also includes numberOfConnections and height as per systray.

AdminResource.status() unified with websocket version.
2020-06-24 11:54:06 +01:00
catbref
448e984995 Fix some minor group-related bugs
Incorrect column names when saving a group ban.

Missing column in LeaveGroupTransactions.

More stringent validity checks in group-kick, group-ban and remove-group-admin.

Added loads more tests to cover group actions.
2020-06-24 11:24:19 +01:00
catbref
ec1954bae1 Notify Chat websocket listeners if group membership changes
Also unified newTransactionExecutor and newBlockExecutor into
callbackExecutor.
2020-06-24 11:22:26 +01:00
catbref
66276a6f65 Merge branch 'ssl' into launch 2020-06-23 14:29:45 +01:00
catbref
c00ab2f87c Add support for HTTPS for API
Requires entries 'sslKeystorePathname' and 'sslKeystorePassword'
in settings.json.

With SSL enabled, API will auto-detect HTTP or HTTPs on the same port.

Included tools/build-keystore.sh to help build keystore from
Let's Encrypt certificates.
2020-06-23 14:27:40 +01:00
catbref
99f3ab9921 /chat/active/{address} now produces entries for groups where {address} is a member, even if there are no messages 2020-06-22 14:16:57 +01:00
catbref
75b15c6639 Improve /chat/active/{address} output to include latest message's sender address, and registered name if applicable 2020-06-19 13:38:38 +01:00
catbref
e5e60a5032 Fix badly named API calls refering to block signers as block minters!
Renamed GET /blocks/minters to /blocks/signers
Renamed GET /blocks/minter/{address} to /blocks/signer/{address}

Changed corresponding repository methods and data classes.
2020-06-16 16:58:34 +01:00
catbref
b9d2bbb78b New /websockets/blocks & some controller/block tidying
Controller.onBlockMinted() now .onNewBlock(BlockData)
which saves having to fetch from repository.
Controller.onNewBlock also takes care of updating Controller's
cached chain tip, requesting SysTray refresh, broadcasting
new tip info to peers and notifying websockets.

BlockMinter and Controller.actuallySynchronize updated
to use unified .onNewBlock.

BlocksWebsocket also returns blocks on demand, given either
integer block height or base58 block signature.

Added support to return ApiError via websockets.
2020-06-15 14:07:09 +01:00
catbref
3d79408574 API GET /blocks/signature/{signature} now returns BLOCK_UNKNOWN instead of null
Also removed unnecessary catch-and-throw of ApiExceptions
2020-06-15 13:18:33 +01:00
catbref
67b184acc9 Chat websockets!
ws://hostname:port/websockets/chat/active/{address}
ws://hostname:port/websockets/chat/messages?txGroupId=XXX
ws://hostname:port/websockets/chat/messages?involving=AAA&involving=BBB
2020-06-12 15:10:55 +01:00
catbref
11040ae60a Added ws://hostname:apiport/websockets/chat/active/{address}
Unified Transaction.importAsUnconfirmed() and Controller.onNetworkTransactionMessage()
to both call Controller.onNewTransaction().

Modified Controller.onNewTransaction() to only send transaction signature to
other peers, instead of full transaction. Peers can request full transaction if they
don't have it.

Controller.onNewTransaction() also calls ChatNotifier, which in turn
notifies websocket handlers about new CHAT transactions.

Added jetty websocket dependency to pom.xml
2020-06-12 10:24:22 +01:00
catbref
a338202ded Fix incorrect PoW buffer usage length in verify & adjust difficulties
CHAT: 8 or 14
MESSAGE: 14
PUBLICIZE: 15
Handshake: 8

Added test to cover verify bug
2020-06-10 10:09:06 +01:00
catbref
e0398490ae Override reference checking for PUBLICIZE transaction type 2020-06-08 09:16:10 +01:00
catbref
847093edac Fix incorrect PoW buffer length usage 2020-06-08 09:01:55 +01:00
catbref
758a42db36 Fix incorrect txType in PUBLICIZE layout 2020-06-05 12:04:59 +01:00
catbref
c2b253df55 Remove extraneous import to cure warning 2020-06-04 18:10:49 +01:00
catbref
cc3adc6720 Correct API models from using "Bitcoin refund/redeem address" to pubkeyhash 2020-06-04 15:51:44 +01:00
catbref
d77acd9eb9 Delete "old" peer from in-memory known peer cache too 2020-06-04 10:57:49 +01:00
catbref
5ad2bc1940 Merge branch 'MESSAGE-PoW' into launch 2020-06-04 10:22:20 +01:00
catbref
ea4c51026b Merge branch 'block-rewards' into launch 2020-06-04 10:22:14 +01:00
catbref
d0b4a1f12f Added PoW to MESSAGE (for zero fee). DB and tx layout changes. 2020-06-04 10:20:02 +01:00
catbref
50b912e229 Improved tools/publish-auto-update.pl 2020-06-03 11:49:49 +01:00
catbref
5ffddd0169 Changes to block reward distribution
Any reward leftover from ditributing to legacy QORA holders is reallocated to either:
founders if any online
or
account-level-based reward candidates, if no founders online

We should get pretty close to 100% block reward distribution, barring rounding artifacts.

More documentation and tests.

Removed BlockChain's founderShare as it is calculated in Block on a per-block basis instead.
2020-06-02 10:42:45 +01:00
catbref
b5512dfa91 Rework block rewards to be faster and only reward *online* founders.
Now we sum generic block reward + transaction fees before performing
distribution only once.

Added Map to collate account-balance changes during block reward
distribution so the final changes can be applied in one batch,
reducing DB load.

Some other optimizations like a faster ExpandedAccount.getShareBin().

Passes test EXCEPT RewardTests.testLegacyQoraReward(), pending decision
on how to reallocate 'unspent' block reward.
2020-06-01 16:50:28 +01:00
catbref
2493d5f7a8 Fix wrong test blockchain config being used for legacy qora holder testing 2020-06-01 16:44:56 +01:00
catbref
bef1828404 Add support for multiple P2SH funding transactions rather than requiring only one 2020-05-29 19:10:20 +01:00
catbref
0ae232b8ba Fix return result from ElectrumX.broadcastTransaction 2020-05-29 19:09:45 +01:00
catbref
cdf0795881 Add extra test case to MemoryPoW 2020-05-28 14:15:42 +01:00
catbref
31e85226f4 WASM version of MemoryPoW! 2020-05-28 14:09:53 +01:00
catbref
8df3c68df9 Update auto-update tools to work with testnet and on non-master branch (e.g. a testnet branch) 2020-05-27 11:49:59 +01:00
catbref
ca0deb2bf6 Allow tools/build-auto-update.sh to work on non-master branch (with warning) 2020-05-27 11:08:56 +01:00
catbref
6eea7c2aa1 Disallow registering/updating to a name that looks like an address 2020-05-27 10:56:03 +01:00
catbref
9aabf93523 Merge branch 'PUBLICIZE-txn' into launch 2020-05-27 10:46:27 +01:00
catbref
322e2cdc41 API call /crosschain/p2sh/redeem returns BTC_BALANCE_ISSUE in preference to INVALID_ADDRESS when P2SH balance is too low/zero 2020-05-27 10:44:37 +01:00
catbref
df395c77db Fix up BTCACCT.findP2shSecret given reduced data available since switch to ElectrumX 2020-05-27 10:43:14 +01:00
catbref
274002c473 Fix BTC.getMedianBlockTime() and update tests 2020-05-27 09:29:11 +01:00
catbref
3d4fc38fcb Replaced bitcoinj networking with ElectrumX.
No more bitcoinj peer-group stalls, or slow startups,
or downloading tons of block headers, or checkpoint files.

Now we use ElectrumX protocol to query info from random servers.

Also:
BTC.hash160 callers now use Crypto.hash160 instead.
Added BitTwiddling.fromLEBytes() returns int.

Unit tests seem OK, but needs complete testnet ACCT walkthrough.
2020-05-26 17:47:37 +01:00
catbref
d50f16b8a9 PUBLICIZE transaction for on-chain record of public key 2020-05-25 15:20:21 +01:00
catbref
59de22883b Use CHAT, not MESSAGE, MAX_DATA_SIZE in ChatTransactionTransformer 2020-05-25 08:30:06 +01:00
catbref
db73afaf88 Remove Block.orphan() forced repository debugging 2020-05-25 07:27:42 +01:00
catbref
3afbd7aa51 Recheck for duplicate connection after handshaking to cover race condition with simultaneous bi-directional connections 2020-05-25 07:24:19 +01:00
catbref
0c32afa07f New network handshaking. NOT backwards compatible!
Old Qora v1 message types removed.
Message type values changed.

Network handshaking reworked to fix multiple-connections issue.
Instead of using some random peerID, we now use proper keypairs and a challenge-response handshake to prevent doppelgangers/ID-theft.
This results in simpler handshaking code as we don't have to perform some arcane doppelganger resolution.

Handshaking still uses proof-of-work for challenge-response, but switched to newer MemoryPoW.

API call GET /peers no longer has 'buildTimestamp' field, but does now have 'nodeId' field.

Network no longer has a whole raft of getXXXpeers() due to simplified handshaking.
Quite a few method calls changed to simply Network.getHandshakedPeers(), which is also faster.
2020-05-22 17:16:45 +01:00
catbref
bd543a526b Update uses of old Public/PrivateKeyAccount static methods to Crypto 2020-05-22 17:13:55 +01:00
catbref
b262044a52 Move some crypto methods from Public/PrivateKeyAccount to Crypto for reuse by new network handshaking 2020-05-22 17:06:48 +01:00
catbref
200a97184c Include transaction reference in chat messages returned by API call GET /chat/messages 2020-05-22 08:00:43 +01:00
catbref
0164bca2d7 Exclude io.druid.java-util from build, removing tons of libsigar 2020-05-21 17:05:17 +01:00
catbref
5f4b66e5b0 Save public keys from CHAT transactions so they can be fetched via API. 2020-05-20 15:53:43 +01:00
catbref
9c48343581 Potential fix for rare HSQLDB "serialization failure" in Transaction.importAsUnconfirmed() 2020-05-20 07:33:21 +01:00
catbref
219f82f562 Modify API call GET /chats/active/{address} to return info on all joined groups.
Previously GET /chats/active/{address} would only return an active group chat
entry where 'address' was a member AND there was an existing CHAT
transaction with the same tx_group_id (and no recipient).

Now the response contains entries for ALL groups where 'address' is a member,
regardless of an existing CHAT transactions, omitting the 'timestamp' entry
if there are none.
2020-05-19 17:12:41 +01:00
catbref
51bfd49e25 Re-add missing senderName & recipientName to output of API call GET /chat/messages 2020-05-19 15:29:12 +01:00
catbref
7102f4a727 Fix for incorrect amounts reported by API 2020-05-19 15:20:20 +01:00
catbref
28991a926f Fix incorrect getDataLength() in RegisterNameTransactionTransformer 2020-05-19 14:55:26 +01:00
catbref
74f89af841 Enforce version 2+ for DEPLOY_AT 2020-05-19 08:35:25 +01:00
catbref
b4284515e7 Unify transaction NAME_NOT_LOWER_CASE checks to Unicode NAME_NOT_NORMALIZED version 2020-05-19 08:31:36 +01:00
catbref
032c5d0d07 Add missing fee check to TRANSFER_PRIVS 2020-05-19 08:25:53 +01:00
catbref
72100fe1d8 Refactor Unicode 'reduced' name code from side-effects into 'data' objects.
CREATE_GROUP, ISSUE_ASSET, REGISTER_NAME and UPDATE_NAME transactions affected.

The code to actually generate 'reduced' name was called inside isValid() and
relied on setting the corresponding transaction data object field so that it would
be saved by isValid()'s caller. Although this worked, it wasn't a very clean
solution.

Now the 'reduced' name is generated by transaction data object's constructors so
it is always present.

Also removed name/group/asset reduceName(String) methods as they were all the
same single-line call to Unicode.sanitize().
2020-05-19 08:08:21 +01:00
catbref
ed178e744d Merge branch 'asset-unicode' into launch 2020-05-19 07:57:06 +01:00
catbref
94f7079c2e Unicode homoglyph support to Assets 2020-05-19 07:56:17 +01:00
catbref
f1638aa9d9 Removed "owner" from CREATE_GROUP and added Unicode homoglyph support.
Group owner now derived from CREATE_GROUP transaction creator's public key.

Added 'reduced' group name to GroupData, with corresponding change to DB.
Renamed GroupData.getIsOpen() to simply isOpen().

Tidied up CreateGroupTransactionData, adding 'reduced' group name.
Renamed getIsOpen() to simply isOpen().
Added code to generated reduced group name when building genesis block.

Added Group.MIN_NAME_SIZE of 3.

DB tables changed to add reduced_group_name where appropriate,
removing owner where necessary.

Added GroupRepository.reducedGroupNameExists(String).

Fixed up test blockchain configs in src/test/resources/test-chain-v2*.json.
2020-05-18 17:27:32 +01:00
catbref
a7b9215ace Merge branch 'message-wo-recipient' into launch 2020-05-18 10:12:54 +01:00
catbref
956ad7bfa8 Merge branch 'asset-fixes' into launch 2020-05-18 10:12:42 +01:00
catbref
4baf442cb8 Merge branch 'name-fixes' into launch 2020-05-18 10:12:31 +01:00
catbref
24eb7c6933 Allow MESSAGE transactions to have no recipient.
This allows on-chain messages to a group, including NO_GROUP / groupID zero.

No-recipient messages cannot have an amount - where would it go?

Changed MESSAGE serialization layout to add boolean indicating
whether recipient is present.

Changed MESSAGE serialization layout so assetID is after amount,
and only present if amount is non-zero.

Changed DB table structures to cover above.

Added unit tests to cover above.
2020-05-18 09:09:35 +01:00
catbref
38a2af8cd5 Tidy up Assets by removing 'owner' from ISSUE_ASSET.
Owner now derived from issuer's public key.
Maximum asset name length reduced to 40 characters.

Repository table changes.

"owner" removed from test blockchain configs and "issuerPublicKey" used instead
where applicable.

Some getters in the form of "getIs___()" renamed to simply "is____()".
2020-05-15 16:22:13 +01:00
catbref
7447ab20a9 Add index for finding Registered Names using 'reduced' form 2020-05-15 14:18:51 +01:00
catbref
197c742ce7 Major work on Registered Names
Changes include:

* Allowing renaming
* Tracking last-updated timestamps
* More stringent Unicode processing
* Way more unit tests
* Max name length reduction to 40 chars

Note: HSQLDB repository table changes
2020-05-15 14:08:46 +01:00
catbref
f6ed3388a4 BTC tidy-up 2020-05-15 07:45:24 +01:00
catbref
c61690f3e6 BTC class does not need to extend Thread! 2020-05-14 13:18:44 +01:00
catbref
9a94873d0e Fix broken Long vs Long comparison in Block.areAtsValid() 2020-05-14 13:18:17 +01:00
catbref
5c8bda37d1 Rework BTC class for better startup & shutdown.
Controller no longer starts up BTC support during main startup.
This does mean that BTC startup is deferred until first BTC-related
action, and that the first BTC-related action will take much longer
to complete.

Added tests to cover startup/shutdown.

This also fixes splash logo stuck on-screen and broken Controller
shutdown when using REGTEST bitcoin network AND there is no
local regtest bitcoin server running.
2020-05-14 12:52:26 +01:00
catbref
fbb73ee88e Suppress extraneous bitcoinj logging output 2020-05-14 12:52:08 +01:00
catbref
fa08041696 BlockTransformer should skip AT transactions when calculating block length 2020-05-14 12:51:44 +01:00
catbref
f01a34a461 Throw an API error for inappropriate calls on OFFER-state cross-chain ATs.
P2SH-related API calls under /crosschain/ aren't applicable for
cross-chain ATs that are still in OFFER state, only TRADE state.
2020-05-14 12:48:41 +01:00
catbref
c0242fe78b Correct comment and example Windows dirname entry in log4j2.properties 2020-05-14 11:42:22 +01:00
catbref
ef790a8cb1 Fix missing groupId 0 entry in output from API call GET /chat/active/{address} 2020-05-13 16:30:28 +01:00
catbref
cea0cee9a8 Names: fixes to allow name change and tests to cover 2020-05-13 15:07:36 +01:00
catbref
d9f784ed2b Registered names: changing 'owner' and allowing renaming.
REGISTER_NAME has an "owner" field which can be different from the actual
registrant (transaction creator's public key, used for signing transaction).

This allowed people to register names to be owned by someone else, thus breaking
the whole "one name per account" aspect.

So now "owner" is removed from REGISTER_NAME, and the actual owner address is
derived from transaction creator's public key, as you would expect.

Similarly, UPDATE_NAME has a corresponding "newOwner" field which has been removed.

In addition, UPDATE_NAME now allows users to change their registered name using a new
"newName" field.

Various changes made to DB, Name class, etc. to accomodate above, along with some minor
bug-fixes and comment improvements/corrections.

Needs new unit tests to cover both new functionality and old!
2020-05-13 10:19:56 +01:00
catbref
f29ae656b9 More work on CHAT
Always add group 0 info to output of API call GET /chats/active/{address}.
No groupName entry as it's "no group" or "group-less" or "not group related".
Timestamp also might be omitted if no message found.

Fix output of POST /chats/compute so it doesn't include zeroed 64-byte signature.
2020-05-12 20:27:09 +01:00
catbref
a9852e5305 More work on CHAT API / support.
Renamed GET /chats/search to /chats/messages.

Added GET /chats/active/{address} to return lists of group chats
and direct chats involving {address}, where a chat message exists.
2020-05-12 14:28:41 +01:00
catbref
32470fa641 Improve CHAT API and repository support.
Change CHAT API call GET /chat/search to better support the two
main scenarios of:

group-based chatting: supply txGroupId only
private chatting: supply 2 'involving' addresses only

Added some DB indexes to cater for above.

GET /chat/search now returns specialized ChatMessage objects
instead of ChatTransactions. This is to reduce unnecessary fetching
of data from repository, and onward sending to API client.
2020-05-12 10:02:41 +01:00
catbref
0d1c08bf96 Add index on DB Names.owner to help find names by owner 2020-05-12 10:02:26 +01:00
catbref
026c904ce4 Change processing of network TRANSACTION_SIGNATURES message.
Previously Controller would loop through the transaction signatures,
discard those already known, and then requesting the full transaction
via peer.getResponse(). This would tie up a networking thread for some
time and also potentially cause repository deadlocks, although the latter
could have been fixed another way.

However, the code after peer.getResponse() was identical to the code
processing an incoming TRANSACTION message. Now instead of requesting
and waiting for then processing each transaction, Controller simply
sends the peer a GET_TRANSACTION for each unknown transaction signature.

As the peer responds with corresponding TRANSACTION messages, these can
be processed individually with shorter period of locking.
2020-05-12 08:03:11 +01:00
catbref
59ae070c83 Fix API call POST /peers so it returns "false" for existing peer, instead of throwing / Internal Server Error 2020-05-11 12:59:56 +01:00
catbref
2ab695f308 NTP: don't call shutdownNow() on null instanceExecutor
When using fixed NTP offset, e.g. via "testNtpoffset" in settings.json,
Controller calls NTP.shutdownNow() which throws a NPE because
NTP.instanceExecutor is null.
2020-05-11 12:59:45 +01:00
catbref
f0ff77cd31 Fix ChatTransaction w.r.t. txGroupId meaning. Relax no-QORT PoW difficulty. 2020-05-11 12:58:46 +01:00
catbref
e241d9fa67 Split CHAT compute into separate API call to help UI 2020-05-11 12:58:05 +01:00
catbref
5e9b0cd03c Fix GroupInvites.expires_when in DB to re-allow NULL 2020-05-11 12:55:58 +01:00
catbref
a5c437913f Transaction.isValidTxGroupId() changed from private to protected - needed so ChatTransaction can override 2020-05-11 12:55:22 +01:00
catbref
3fa7da5115 Fix for incorrect blocksMinted count. Added test to cover 2020-05-08 08:51:56 +01:00
catbref
6d8f41ab05 Fix long overflow in Block.distributeBlockRewardToQoraHolders()
Sadly no native 128bit integer support in Java 11 so resorting to using
BigInteger.

Added/improved unit tests to cover.
2020-05-07 16:37:40 +01:00
catbref
c7c419a3cd Bump version in pom.xml to v1.1.0 ready for launch 2020-05-07 13:30:34 +01:00
catbref
3094ec3c26 Massive clean-up of DB & conversion to long for timestamps
Collated all development changes to DB so now we build
initial DB structure directly with final layout.
i.e. no ALTER TABLE, etc.

Reordered HSQLDB 'CREATE TYPE' statements into alphabetical order
for easier maintainability.

Replaced TIMESTAMP WITH TIME ZONE with simple BIGINT ("EpochMillis").
Timezone conversion is now a presentation task, rather than having
pretty values in database.

Removed associated conversion methods, like toOffsetDateTime(),
fromOffsetDateTime() and getZonedTimestampMilli().

Renamed some DB columns to make them more obviously timestamps, like:
Names.registered is now Names.registered_when.

Removed IFNULL(balance, 0) from HSQLDBAccountRepository as balances
are never null, or actually never 0 either.

Added more tests to increase API call, and hence repository, coverage.

Removed unused "milestone block" from Transactions.
2020-05-07 12:16:22 +01:00
catbref
359a35931e Fix broken Transaction.getUnconfirmedTransactions() and getInvalidTransactions() 2020-05-07 12:15:09 +01:00
catbref
9e0001c4f6 Improved comments, variable names, etc. for some repository interfaces 2020-05-07 10:22:44 +01:00
catbref
53112709fe Improve comment & tidy annotation in GroupData 2020-05-07 10:11:35 +01:00
catbref
d1bc500ab9 Correct two unit tests from BigDecimal to long/int. 2020-05-06 15:03:41 +01:00
catbref
74b5401e84 Merge chain-stall, blocksMinted and other fixes 2020-05-06 08:01:51 +01:00
catbref
d2559f36ce Fix for Block not correctly adjusting accounts' blocksMinted values.
Added BlocksMintedCountTests to cover above.
2020-05-05 16:10:54 +01:00
catbref
0cc9cd728e Fix for chain-stall relating to freshly cancelled reward-shares.
In some cases, a freshly cancelled reward-share could still have
an associated signed timestamp. Block.mint() failed to spot this
and used an incorrect "online account" index when building the
to-be-minted block.

Block.mint() now checks that AccountRepository.getRewardShareIndex()
doesn't return null, i.e. indicating that the associated reward-share
for that "online account" no longer exists.

In turn, AccountRepository.getRewardShareIndex() didn't fulfill its
contract of returning null when the passed public key wasn't present
in the repository. So this method has been corrected also.

AccountRepository.rewardShareExists(byte[] publicKey) : boolean added.

BlockMinter had another bug where it didn't check the return from
Block.remint() for null properly. This has been fixed.

BlockMinter now has additional logging, with cool-off to prevent log
spam, for situations where minting could not happen.

Unit test (DisagreementTests) added to cover cancelled reward-share
case above. BlockMinter testing support slightly modified to help.
2020-05-05 11:09:46 +01:00
catbref
e5cf76f3e0 Replace throwing IllegalStateException with more defensive log & null in Block/BlockMinter 2020-05-04 15:50:10 +01:00
catbref
44e8b3e6e7 Log when BlockMinter fails to acquire blockchain lock after waiting 2020-05-04 14:33:10 +01:00
catbref
1bca152d9c Reduce minBlockchainPeers for now 2020-05-04 14:32:42 +01:00
catbref
4edc3ee121 Fix AT transaction reference lookup/generation in light of new last-ref scheme 2020-05-04 09:32:18 +01:00
catbref
e9f29767c8 Change Transaction.countUnconfirmedByCreator() to disregard CHAT transactions.
This is because CHAT transactions have intrinsic anti-spam/DoS prevention
by requiring proof of work.
2020-05-04 09:29:07 +01:00
catbref
e2916b130b No need to store repository handle in Block$ExpandedAccount 2020-05-04 09:19:17 +01:00
catbref
538e117abd Clean up Transaction.isStillValidUnconfirmed()
Brought more into line with isValidUnconfirmed().
No need to update creator's lastReference under new last-ref scheme.

Correspondingly, no need to acquire blockchain lock or repository
shenanigans in getUnconfirmedTransactions() and getInvalidTransactions()
for the same reason.

getInvalidTransactions() seems to be unused and may well be cleaned up
in a future commit.
2020-05-04 09:14:52 +01:00
catbref
71e80bd02f Convert to Account.modifyAssetBalance()
Change code of the form (assetId aspect not shown):

account.setConfirmedBalance( account.getConfirmedBalance(), amount )

to:

account.modifyAssetBalance( amount )

Also tidied "0 - value" to use unary negate: "- value"
2020-05-04 08:45:31 +01:00
catbref
800103225b Remove pointless "return" in DeployAtTransaction 2020-05-04 08:18:59 +01:00
catbref
cfb7a3cc4c Minor terminology correction in GenesisBlock 2020-05-04 08:18:33 +01:00
catbref
3185cf23df Replace throwing IllegalStateException with more defensive log & null in Block/BlockMinter 2020-05-04 08:18:15 +01:00
catbref
3ac1b36549 Restrict API call POST /chat to prevent CPU abuse 2020-05-04 08:17:05 +01:00
catbref
55e99062ca CHAT PoW difficulty now much greater if sender has no QORT balance 2020-05-01 11:01:52 +01:00
catbref
edb56b74da Merge branch 'chat' into launch 2020-05-01 10:51:38 +01:00
catbref
233ace23de AT transactions now either have null message or null amount&assetId.
AT transaction transformer changed to refuse to deserialize AT transactions,
as they should never appear on the wire.

Ditto for GENESIS transactions.
2020-05-01 10:42:32 +01:00
catbref
e1f3b9a7a3 Update QortalATAPI.putTransactionAfterTimestampIntoA() to use Transaction.getRecipientAddresses 2020-05-01 10:20:25 +01:00
catbref
6be88ac86e In BTCACCT, use Account to fetch balance instead of direct from DB 2020-05-01 10:19:28 +01:00
catbref
d03cca2e76 Merge branch 'BTC-ACCT' into launch 2020-05-01 10:09:54 +01:00
catbref
e86143426b Fix potentially overflowing multiply in Block reward processing.
Change BlockChain config to use AmountTypeAdapter instead of
creating duplicated long versions of BigDecimal values.

Some tidying to Amounts class.
2020-05-01 08:57:15 +01:00
catbref
a309f8de9e Fix dead code warning in Account 2020-05-01 08:56:49 +01:00
catbref
df15f81b9f Fix whitespace 2020-05-01 08:56:27 +01:00
catbref
6ab50e4dff Fix some SonarLint complaints 2020-05-01 08:56:03 +01:00
catbref
476d9e4c95 Converted tests from BigDecimal to long
Moved Asset.MULTIPLIER, etc. to Amounts class.

Had to reintroduce BigInteger for asset trading code.
Various helper methods added to Amounts class.

Payment.process/orphan no longer needs unused transaction
signature or reference.

Added post block process/orphan tidying, which currently deletes zero account balances to satisfy post-orphan checks in unit tests.

Fix for possible bug when orphaning TRANSFER_PRIVS.

Added RewardSharePercentTypeAdapter like AmountTypeAdapter.

Replaced a whole load of JAXB-special getters with type-adapters.

Tests looking good!
2020-05-01 08:41:35 +01:00
catbref
9eaf31707a Massive conversion from BigDecimal to long.
Now possible thanks to removing Qora v1 support.

Maximum asset quantities now unified to 10_000_000_000,
to 8 decimal places, removing prior 10 billion billion
indivisible maximum.

All values can now fit into a 64bit long.
(Except maybe when processing asset trades).

Added a general-use JAXB AmountTypeAdapter for converting
amounts to/from String/long.

Asset trading engine split into more methods for easier
readability.

Switched to using FIXED founder block reward distribution code,
ready for launch.

In HSQLDBDatabaseUpdates,
QortalAmount changed from DECIMAL(27, 0) to BIGINT
RewardSharePercent added to replace DECIMAL(5,2) with INT

Ripped out unused Transaction.isInvolved and Transaction.getAmount
in all subclasses.

Changed
  Transaction.getRecipientAccounts() : List<Account>
to
  Transaction.getRecipientAddresses() : List<String>
as only addresses are ever used.

Corrected returned values for above getRecipientAddresses() for
some transaction subclasses.

Added some account caching to some transactions to reduce repeated
loads during validation and then processing.

Transaction transformers:

Changed serialization of asset amounts from using 12 bytes to
now standard 8 byte long.

Updated transaction 'layouts' to reflect new sizes.

RewardShareTransactionTransformer still uses 8byte long to represent
reward share percent.

Updated some unit tests - more work needed!
2020-05-01 08:40:32 +01:00
catbref
e0007269b9 Initial attempt at transient CHAT transaction type.
CHAT transactions don't ever get included into a block.
They use a memory-intensive proof-of-work instead of a fee.
Reference field isn't checked but must be present.
Recipient is optional.
isText/isEncrypted as per MESSAGE, basically indicative flags only.

Some API support.

Memory PoW takes roughly 800ms on Ryzen 3600, maybe 2400ms on QORTector?
2020-04-28 16:45:09 +01:00
catbref
0006911e0a Account lastReference cache, now with Block support.
As this changes how lastReferences are checked and updated,
this is not suitable for rolling into current chain without a
"feature trigger", or chain restart!

Added unit tests.
2020-04-27 16:07:00 +01:00
catbref
e141e98ecc Interim commit with new AccountRefCache, but no tests and no Block support 2020-04-27 16:07:00 +01:00
catbref
40531284dd Convert old "genesis account" addresses in blockchain configs to new "null account" address 2020-04-27 16:06:47 +01:00
catbref
9e2663b11b Fixed old Qora v1 "GenesisAccount" by replacing with NullAccount
NullAccount has 'empty' public key (32 bytes of zeros) compared
with GenesisAccount's vague sometimes 8 bytes, sometimes 32 bytes
public key.

NullAccount has static public key and address, plus overridden
methods to speed up pointless calls like verify().

Genesis Block also tidied up, dropping old Qora v1 compatibility
and using proper block signature and public key to generate
minter's block signature.

Genesis Block transaction processing also simplified, with no need
to access repository to handle fake references, due to new
last-reference code (which will need to be merged).

Dropped support for old, broken RMD160 code.
2020-04-27 16:06:47 +01:00
catbref
2602bb01e1 Limit both divisible & indivisible asset quantites to 1e10 (with 8dp) to fit into 8byte long 2020-04-27 16:04:19 +01:00
catbref
ac15dfe789 Removed broken MD160 support in Crypto 2020-04-27 15:33:23 +01:00
catbref
cd066cf357 Remove more old Qora v1 compatibility code 2020-04-27 15:33:23 +01:00
catbref
bd521baade Removed code for providing compatibility with Qora v1
Qortal is never going to continue off the old Qora blockchain,
so removed all code regarding compatibility.

Removals include:
* various blockchain "feature triggers"
* special Qora-only broken code for various transaction signatures
* "old" asset pricing / trading
* pre-group txGroupId field in transactions
* compatibility unit tests

Possibly safe for roll-out on pre-genesis blockchain?
2020-04-27 15:33:23 +01:00
catbref
136188339d Combined account balance fixes needed for unit tests 2020-04-27 15:33:09 +01:00
catbref
48de33fe24 More informative error messages when parsing blockchain config 2020-04-27 15:26:55 +01:00
catbref
df4798e2a1 Networking improvements: cached known peers, fewer DB accesses, EPC spawn-failure hook 2020-04-24 16:57:20 +01:00
catbref
edb842f0d1 Correct field ordering in layout docs for TRANSFER_ASSET 2020-04-24 15:31:41 +01:00
catbref
b563fe567d Add missing field (poll owner's address) to layout docs for CREATE_POLL 2020-04-24 15:31:02 +01:00
catbref
b3dd0d89df Add missing field (group owner's address) to layout docs for CREATE_GROUP 2020-04-24 15:07:19 +01:00
catbref
360f6cd4f1 Bump to v1.0.7 2020-04-24 09:44:35 +01:00
catbref
f1e4528581 Fix Transaction.calcRecommendedFee() 2020-04-24 09:40:43 +01:00
catbref
1375372380 Added tests to cover validity checks on group min/max block delay values 2020-04-23 17:06:23 +01:00
catbref
a7d0ad27b1 Legacy QORA block reward fix
If there's no more unrewarded legacy QORA held,
then quickly return from Block.distributeBlockRewardToQoraHolders()
instead of causing divide-by-zero.
2020-04-23 17:00:08 +01:00
catbref
833a785996 More work on Bitcoin-side of cross-chain trading.
Tidied up duplicated cross-chain API code that
fetched Qortal AT info.

Added Bitcoin-related cross-chain API calls
for building, checking, refunding and redeeming
P2SH.

Added new Bitcoin-related API error codes.

Controller now starts up, and shuts down, bitcoinj.

Speed-up in BTC class so bitcoinj doesn't have
to throw away all peers and rediscover & reconnect
to them with every chain-related call.
2020-04-23 09:13:32 +01:00
catbref
94d18538d8 More work on cross-chain trading, including API calls.
Added API calls to aid Qortal-side of cross-chain trading.
POST /crosschain/build - for building Qortal AT
POST /crosschain/tradeoffer/recipient - for sending trade partner/recipient to AT
POST /crosschain/tradeoffer/secret - for sending secret to AT
DELETE /crosschain/tradeoffer - for cancelling AT

More fixes regarding Blocks processing/orphaning ATs.
More fixes regarding sending/receiving blocks containing AT data.
AT-related fix to genesis block.

Improved cross-chain trading AT code, removing offer-mode timeout
and replacing that with allowing AT creator to cancel offer/end AT
by sending AT the creator's own address as trade partner/recipient.
After all, they're not going to trade with themselves.

Added assertion to check BTCACCT.CODE_BYTES_HASH matches compiled code hash.

Added cross-chain AT's 'mode' for easier diagnosis, either OFFER or TRADE.

We can't use AT's signature to generate AT address because address is needed
before DEPLOY_AT transaction is signed. So we use a hash of signature-less
transaction bytes.

Corresponding changes to tests.
2020-04-23 09:13:32 +01:00
catbref
8baf42765e Improved cross-chain AT and more API support for same.
Reworked the cross-chain trading AT so it is now 2-stage:
stage 1: 'offer' mode
waiting for message from creator containing trade partner's address
stage 2: 'trade' mode
waiting for message from trade partner containing secret

Adjusted unit tests to cover above.

Changed QortalATAPI.putCreatorAddressIntoB from storing
creator's public key to actually storing creator's address.

Refactored BTCACCT.AtConstants to CrossChainTradeData.

Now we also store hash of AT's code bytes in DB so we can look up
ATs by what they do. Affects ATData class, ATRepository, etc.

Added "Automated Transactions" and "Cross-Chain" API sections.

New API call GET /at/byfunction/{codehash} for looking up ATs
by what they do, based on hash of their code bytes.

New API call GET /at/{ataddress} for fetching info for specific AT.

New API call GET /at/{ataddress}/data for fetch an AT's data segment.
Mostly for diagnosis of AT's current state.

New API call POST /at for building a raw, unsigned DEPLOY_AT transaction.

New API call GET /crosschain/tradeoffers for finding open BTC-QORT trading ATs.
2020-04-23 09:13:32 +01:00
catbref
b93dca1818 Include CIYAM AT v.1.3.4 jar 2020-04-23 09:13:32 +01:00
catbref
98506a038b Loads of work on CIYAM AT support, including BTC-QORT cross-chain trading.
We require AT v1.3.4 now!

Updated AT-related logging.

Added "isInitial" flag to AT state data so that state data created at
deployment is not added to serialized block data.

Updated BTC-QORT AT code and tests to cover various scenarios.

Added missing 'testNtpOffset' to various test versions of 'settings.json'.
Added missing 'ciyamAtSettings' to various test blockchain configs.

Loads of AT-related additions/fixes/etc. to core code, e.g Block
2020-04-23 09:13:32 +01:00
catbref
3eaeb927ec More work on QORT-BTC ACCT
Requires fix in CIYAM AT v1.3.2

New version of Qortal cross-trade AT code.

Change how Qortal addresses are managed in QortalATAPI from using
base58 strings (that are too long) to using hex form (25 bytes)
as they need to fix into 32 byte A/B register.

Generate AT addresses using DeployAtTransaction's signature instead
of convoluted hash of AT data like name, description, etc.

Add startTime as arg to GetTransaction test app.

Add missing fields (name, description, ATType, tags) to DeployAT test app.
2020-04-23 09:13:32 +01:00
catbref
7ded8954c6 Fix Transaction.calcRecommendedFee() 2020-04-23 09:13:32 +01:00
catbref
d2eb8b0c2b Legacy QORA block reward fix
If there's no more unrewarded legacy QORA held,
then quickly return from Block.distributeBlockRewardToQoraHolders()
instead of causing divide-by-zero.
2020-04-23 09:13:32 +01:00
catbref
2ed2cc0fab Correct package names and minor clean 2020-04-23 09:13:32 +01:00
catbref
87bb9090f5 CIYAM AT & cross-chain trading.
Bump CIYAM AT requirement to v1.3

Remove multi-blockchain AT aspect for now (BlockchainAPI).

For PUT_PREVIOUS_BLOCK_HASH_INTO_A we no longer use SHA256 to condense 64-byte block signature into 32 bytes.
Now we put block height into A1 and SHA192 of signature into A2 through A4.
This allows possible future lookup of block data using "block hash", with verification that it is the same block.

Some AT functions use "address in B" but sometimes we populate B with account's public key instead.
So the method "getAccountFromB" is smart and checks for an actual, textual address in B starting with 'Q', otherwise assumes B contains public key.

The Settings field "useBitcoinTestNet" (boolean) now replaced with "bitcoinNet" (String) with possible values MAIN (default), TEST3, REGTEST.
This allows for more varied development/testing scenarios.

Use correct Bitcoin nSequence value 0xFFFFFFFE for P2SH, i.e. enable locktime, disable RBF.

Roll REGTEST checkpoints file generator into main BTC class.

Yet another rewrite of Bitcoin P2SH scripts for BTC-QORT cross-chain trading.
Added associated test classes BuildP2SH, CheckP2SH, DeployAT (unfinished).
2020-04-23 09:13:32 +01:00
catbref
8844cc0076 GetTransaction test app to demo fetching any bitcoin transaction using bitcoinj. Plus some AT-API work 2020-04-23 09:13:32 +01:00
catbref
2c4bad6455 Interim commit of BTC-QORT cross-chain trade, with partial conversion from secret+hash to using "trade key" 2020-04-23 09:13:32 +01:00
catbref
5c0134c16a work in progress: btc-qort cross-chain trades
Streamlined BTC class and switched to memory block store.

Split BTCACCTTests into BTCACCT utility class and (so far)
three stand-alone apps: Initiate1, Refund2 and Respond2

Moved some Qortal-specific CIYAM AT constants into blockchain config.

Removed redundant BTCTests
2020-04-23 09:13:32 +01:00
catbref
369a45f5c0 BTC-ACCT progress
Bump bitcoinj to 0.15.5 for fixes.

lockTime is int (seconds since epoch), not long (ms since epoch).

Improve output of Initiate1.

Added (most of) Respond2.
2020-04-23 09:13:31 +01:00
catbref
d58b7c1f53 Work on BTC-ACCT
Bump CIYAM AT dependency to v1.2 for MachineState.toCreationBytes()
2020-04-23 09:13:31 +01:00
catbref
5011a2be22 Added isAdmin field to output of API call GET /groups/member/{address} 2020-04-22 16:32:55 +01:00
catbref
33010f82d8 No need to check AT transactions in Block.areTransactionsValid() 2020-04-16 13:16:30 +01:00
catbref
8dbd8c4e65 Performance improvement when checking block's online accounts signatures.
If the timestamp-pubkey-sig is still 'current' then it'll be in
Controller's list of current online accounts, so we can quickly scan
that list before falling back to the more expensive Ed25519 verify.

Added equals() and hashCode() to OnlineAccountData to support above.
2020-04-16 12:25:01 +01:00
catbref
c2a3c1271c Remove missed, extraneous last-reference check from CreateAssetOrderTransaction.isValid() 2020-04-16 09:24:04 +01:00
catbref
1e9a7ac87d Update SysTray for all synchronizing scenarios.
When synchronizing is forced via API call, the SysTray doesn't update
to reflect this.

We fix this by moving the SysTray updating code from
Controller.potentiallySynchronize() to the inner method
Controller.actuallySynchronize(), which is also the method called
directly by the API.
2020-04-15 17:36:12 +01:00
catbref
e25d24964c Make BlockMinter more aggressive about obtaining blockchain lock.
Previously BlockMinter & Synchronizer would both try opportunistic
locking, with no wait/timeout or fairness.

This could lead to a situation where a majority of nodes are
synchronizing, albeit only the top 1 or 2 blocks, but no node
manages to mint within the 'recent' period, so the chain stalls.

However, if a node is at/near the top of the chain then synchronization
shouldn't take very long so we let BlockMinter wait until to 30s
(approx. half typical block time) to obtain lock.

This makes minting blocks more likely in a BlockMinter/Sync fight
which helps keep the chain going.

Detecting chain stalls, and allowing minting if we have plenty of peers,
also produces blockchain 'islands' so isn't a simple fix at this point.
2020-04-15 17:30:49 +01:00
catbref
d90d84ab06 More descriptive tray mouseover, showing sync percent or connecting status
Added sync percent to API call GET /admin/status

Added SysTray i18n "CONNECTING" key to CheckTranslations test app.
2020-04-03 08:31:41 +01:00
catbref
2ddb1fa23e Bump version to v1.0.6 2020-03-31 08:04:35 +01:00
catbref
82f6e38adb Add PID tracking to stop.sh (thanks IceBurst) 2020-03-30 17:39:36 +01:00
catbref
00ac26cf27 Add script to build release ZIP 2020-03-30 17:39:36 +01:00
catbref
fa1aa1c8b2 Show informative page instead of "Forbidden" when user tries to access API documentation when disabled. 2020-03-30 17:39:36 +01:00
catbref
9156325ffc Reduce minimum networking requires to relax CPU usage somewhat 2020-03-30 17:39:36 +01:00
catbref
70131914b2 Auto-updates: increase checking interval & TCP timeouts
Bumped TCP timeouts for fetching auto-update from 5s (connect) and
3s (read) to 30s (connect) and 10s (read) to allow for nodes with
slower internet connections.

Increased interval between checking for auto-updates from 5 minutes
to 20 minutes to reduce load on update sources and also to reduce
the number of nodes that restart at any one time.

Obviously this new checking interval will only apply after the NEXT
auto-update...
2020-03-30 17:39:36 +01:00
catbref
bd87e6cc1a Increase retry interval and count in ApplyUpdate.
Used when checking that node has shutdown and when replacing old JAR with new update.

ApplyUpdate previously waited 5 seconds between checks/retries, for up to 5 times: 25 seconds.
Now waits 10 seconds, for up to 12 times: 120 seconds.
Hopefully this will give slower nodes enough time to shut down and prevent errors like these on Windows installs:

2020-03-24 12:05:50 INFO  ApplyUpdate:114 - Unable to replace JAR: qortal.jar: The process cannot access the file because it is being used by another process.
2020-03-30 17:39:36 +01:00
catbref
6c8e96daae Turn off repository backups by default.
They can be re-enabled by setting "repositoryBackupInterval" to a
non-zero value in settings.json. Note the value is in milliseconds!
2020-03-30 17:39:36 +01:00
catbref
cfb8f53849 Reduce DB space taken up by Blocks 2020-03-30 17:39:36 +01:00
catbref
7bb2f841ad Rip out historic account balances as they take up too much DB space. 2020-03-30 17:39:36 +01:00
catbref
558263521c Minor fix to auto-update tx publish script 2020-03-30 17:39:36 +01:00
catbref
1db8c06291 Merge pull request #2 from IceBurst/patch-1
Java Checks
2020-03-30 11:32:04 +01:00
catbref
edee08a7b5 Merge pull request #1 from nitrokrypt/master
Fixed Images
2020-03-30 11:25:34 +01:00
catbref
0594bdf1c7 Bump to v1.0.5 in pom.xml. Filter out build/test artifacts from shaded JAR. 2020-03-24 11:17:43 +00:00
catbref
72c299a331 Add SysTray notification for DB backup. More translations.
Added a setting "showBackupNotification", which is false by default,
that shows a tray notification when a repository backup occurs.

Above notification, and the auto-update notification, now refer to
the SysTray i18n translation lookup resources.
2020-03-24 09:26:40 +00:00
catbref
0b42a7ad63 Modify minOutboundPeers, maxPeers and maxNetworkThreadPoolSize default settings
Typical users don't need quite so many connections, so minOutboundPeers and
maxPeers reduced accordingly.

maxNetworkThreadPoolSize increased from 10 to 20.
2020-03-24 09:24:22 +00:00
catbref
51e59f6ab7 Change HSQLDB repository log fsync() interval from 500ms to 5s 2020-03-24 09:23:17 +00:00
catbref
38394de661 Reduce peer response timeout from 5s to 2s
5s is way too long, and even 2s might still be considered excessive.
However, reducing the timeout might also reduce the number of
network engine "spawn failures" due to too many threads tied up
waiting for ping responses from overloaded peers.

Does not affect peer handshaking: that has a separate timeout.
2020-03-24 09:20:50 +00:00
catbref
22f9755f4f Performance optimizations. Accounts/NTP/System.currentTimeMillis, etc.
Added Ed25519 private key to public key function accessible from SQL.
Added Ed25519 public key to Qortal address function accessible from SQL.

Used above functions to store minting account public key in SQL to
reduce the number of unnecessarily repeated Ed25519 conversions.

Used above functions to store reward-share minting's accounts address
to reduce the number of unneccessarily repeated PK-to-address conversions.

Reduced the usage of PublicKeyAccount to simply Account where possible,
to reduce the number of Ed25519 conversions.

Account.canMint(), Account.canRewardShare() and Account.getEffectiveMintingLevel()
now only perform 1 repository fetch instead of potentially 2 or more.

Cleaned up NTP main thread to reduce CPU load.
A fixed offset can be applied to NTP.getTime() responses, for both
scenarios when NTP is running or not. Useful for testing or simulating
distant remote peers.

Controller.onNetworkMessage() and Network.onMessage() have both had their
complexity simplified by extracting per-case code to separate methods.

Network's EPC engine's thread pool size no longer hard-coded, but comes
from Settings.maxNetworkThreadPoolSize, which is still 10 by default,
but can be increased for high-availability nodes.

Network's EPC task-producing code streamlined to reduce CPU load.

Generally reduced calls to System.currentTimeMillis(), especially
where the value would only be used in verbose logging situations,
and especially in high-call-volume methods, like within repository.
2020-03-23 11:14:05 +00:00
catbref
4cb2e113cb Log (and discard) duplicate outbound connections to the same peer 2020-03-23 11:07:08 +00:00
catbref
e0f024ef5c Performance improvements in networking ExecuteProduceConsume engine
Keep track of when EPC engine can't spawn a new thread as this
might indicate thread-pool exhaustion and cause some network
messages to be lost.

If logging level is NOT 'trace' (or 'all') then don't call
System.currentTimeMillis() as we'll never use the value.

Similarly, don't set thread names if not logging at 'trace' either.

Update EPC tests, particularly unified per-second/end-of-test stats
reporting.
2020-03-23 11:00:19 +00:00
catbref
f95cb99cdc Add support for logging PROOF network message calculation time 2020-03-23 10:57:23 +00:00
catbref
1f0170bb4b Increase timeout for transaction submission via API POST /transactions/process from 500ms to 30s 2020-03-23 10:54:35 +00:00
IceBurst
83bce3ce52 Java Checks
Validates Java is available and version is 11 or greater
2020-03-14 10:52:54 -04:00
nitrokrypt
bf288dbfc2 Fixed Images
Previous images had a small hole in the icon (probably a result of a background removal), I just filled it back in with white like it's supposed to be. Also, the previous square icons were streched into a square aspect ratio, these are unstreched.
2020-03-12 12:01:24 -05:00
409 changed files with 28407 additions and 10304 deletions

View File

@@ -0,0 +1,3 @@
{
"apiDocumentationEnabled": true
}

View File

@@ -0,0 +1,70 @@
rootLogger.level = info
# On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
# Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn
# Support optional, per-session HSQLDB debugging
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
logger.hsqldbRepository.level = debug
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = off
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
logger.jerseyEOF.level = off
# Suppress extraneous Jetty entries
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
logger.jetty.name = org.eclipse.jetty
logger.jetty.level = warn
# Even more extraneous Jetty output
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
logger.jettyRS.level = error
# Suppress extraneous slf4j entries
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
logger.slf4j.name = org.slf4j
logger.slf4j.level = warn
# Suppress extraneous Reflections entry
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
logger.orgReflections.name = org.reflections.Reflections
logger.orgReflections.level = off
logger.sunReflections.name = sun.reflect.Reflection
logger.sunReflections.level = off
appender.console.type = Console
appender.console.name = stdout
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile
appender.rolling.name = FILE
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.rolling.filePattern = ${dirname:-}${filename}.%i
appender.rolling.policy.type = SizeBasedTriggeringPolicy
appender.rolling.policy.size = 4MB
# Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite
# appender.rolling.append=true

View File

@@ -0,0 +1,33 @@
@echo off
:: BatchGotAdmin
:-------------------------------------
REM --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
echo Requesting administrative privileges...
goto UACPrompt
) else ( goto gotAdmin )
:UACPrompt
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
"%temp%\getadmin.vbs"
exit /B
:gotAdmin
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
pushd "%CD%"
CD /D "%~dp0"
:--------------------------------------
net stop "Windows Time"
w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org cn.pool.ntp.org 0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org"
net start "Windows Time"
sc config w32time start= auto

Binary file not shown.

1268
WindowsInstaller/Qortal.aip Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# Windows installer
## Prerequisites
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
## General build instructions
If this is your first time opening the `qortal.aip` file then you might need to adjust
configured paths, or create a dummy `D:` drive with the expected layout.
Typical build procedure:
* Overwrite the `qortal.jar` file in `Install-Files\`
* Open AdvancedInstaller with qortal.aip file
* If releasing a new version, change version number in:
+ "Product Information" side menu
+ "Product Details" side menu entry
+ "Product Details" tab in "Product Details" pane
+ "Product Version" entry box
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
* You should be prompted whether to generate a new product key, click "Generate New"
* Click "Build" button
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number

97
WindowsInstaller/dictionary.ail Executable file
View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DICTIONARY type="multilanguage">
<!-- Control table -->
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh" value="选择数据保存的文件夹?"/>
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Text">
<STRING lang="en" value="To store data in this folder, click &quot;[Text_Next]&quot;. To store data in a different folder, enter it below or click &quot;Browse&quot;."/>
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Description">
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
<STRING lang="zh" value="重新配置Windows以获得更准确的时间"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_1">
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_2">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_3">
<STRING lang="en" value="Your computer&apos;s clock needs to be accurate to within 0.5 seconds."/>
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Title">
<STRING lang="en" value="Windows clock accuracy"/>
<STRING lang="zh" value="Windows 时钟精度"/>
</ENTRY>
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
<STRING lang="zh" value="删除您下载的区块链"/>
</ENTRY>
<!-- RadioButton table -->
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
<STRING lang="en" value="Choose custom data storage folder..."/>
<STRING lang="zh" value="选择特定的文件夹存储"/>
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
</ENTRY>
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
<STRING lang="en" value="Use default location "/>
<STRING lang="zh" value="使用默认存储地点"/>
<STRING lang="zh_TW" value="使用默认存储地点"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
<STRING lang="zh" value="是将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
</ENTRY>
</DICTIONARY>

BIN
WindowsInstaller/qortal.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

View File

@@ -3,7 +3,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<version>1.0</version>
<artifactId>AT</artifactId>
<version>1.3.5</version>
<description>POM was created from install:install-file</description>
</project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.ciyam</groupId>
<artifactId>AT</artifactId>
<versioning>
<release>1.3.5</release>
<versions>
<version>1.3.4</version>
<version>1.3.5</version>
</versions>
<lastUpdated>20200717104214</lastUpdated>
</versioning>
</metadata>

Binary file not shown.

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<versioning>
<release>1.0</release>
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181105100741</lastUpdated>
</versioning>
</metadata>

View File

@@ -1,11 +1,15 @@
rootLogger.level = info
# On Windows, uncomment this:
# property.dirname = ${sys:user.home}\\AppData\\Roaming\\qortal\\
# On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
# Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn

33
pom.xml
View File

@@ -3,12 +3,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.0.4</version>
<version>1.3.0</version>
<packaging>jar</packaging>
<properties>
<bitcoin.version>0.15.4</bitcoin.version>
<bitcoinj.version>0.15.5</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.5</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>
@@ -16,7 +17,7 @@
<hsqldb.version>2.5.0-fixed</hsqldb.version>
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.22.v20191022</jetty.version>
<jetty.version>9.4.29.v20200521</jetty.version>
<log4j.version>2.12.1</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.12</slf4j.version>
@@ -257,6 +258,8 @@
<!-- Don't include original swagger-UI as we're including our own
modified version -->
<exclude>org.webjars:swagger-ui</exclude>
<!-- Don't include JUnit as it's for testing only! -->
<exclude>junit:junit</exclude>
</excludes>
</artifactSet>
<filters>
@@ -379,12 +382,14 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<scope>provided</scope><!-- needed for build, not for runtime -->
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.bohnman/package-info-maven-plugin -->
<dependency>
<groupId>com.github.bohnman</groupId>
<artifactId>package-info-maven-plugin</artifactId>
<version>${package-info-maven-plugin.version}</version>
<scope>provided</scope><!-- needed for build, not for runtime -->
</dependency>
<!-- HSQLDB for repository -->
<dependency>
@@ -401,14 +406,14 @@
<!-- CIYAM AT (automated transactions) -->
<dependency>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<version>1.0</version>
<artifactId>AT</artifactId>
<version>${ciyam-at.version}</version>
</dependency>
<!-- Bitcoin support -->
<dependency>
<groupId>org.bitcoinj</groupId>
<artifactId>bitcoinj-core</artifactId>
<version>${bitcoin.version}</version>
<version>${bitcoinj.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
@@ -446,6 +451,10 @@
<groupId>org.asynchttpclient</groupId>
<artifactId>async-http-client</artifactId>
</exclusion>
<exclusion>
<groupId>io.druid</groupId>
<artifactId>java-util</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- For NTP -->
@@ -499,6 +508,12 @@
<artifactId>mail</artifactId>
<version>1.5.0-b01</version>
</dependency>
<!-- Unicode homoglyph utilities -->
<dependency>
<groupId>net.codebox</groupId>
<artifactId>homoglyph</artifactId>
<version>1.2.0</version>
</dependency>
<!-- Jetty -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
@@ -527,6 +542,12 @@
<artifactId>jetty-client</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Websocket support -->
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId>
<version>${jetty.version}</version>
</dependency>
<!-- Jersey -->
<dependency>
<groupId>org.glassfish.jersey.core</groupId>

17
run.sh
View File

@@ -6,6 +6,23 @@ if [ "$USER" = "root" ]; then
exit
fi
# Validate Java is installed and the minimum version is available
MIN_JAVA_VER='11'
if command -v java > /dev/null 2>&1; then
version=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
version=$(echo $version | cut -d'.' -f1,2)
if [ `echo "${version}>=${MIN_JAVA_VER}" | bc` -eq 1 ]; then
echo 'Passed Java version check'
else
echo 'Please upgrade your Java to version 11 or greater'
exit 1
fi
else
echo 'Java is not available, please install Java 11 or greater'
exit 1
fi
# No qortal.jar but we have a Maven built one?
# Be helpful and copy across to correct location
if [ ! -e qortal.jar -a -f target/qortal*.jar ]; then

View File

@@ -36,8 +36,8 @@ public class ApplyUpdate {
private static final String NEW_JAR_FILENAME = AutoUpdate.NEW_JAR_FILENAME;
private static final String WINDOWS_EXE_LAUNCHER = "qortal.exe";
private static final long CHECK_INTERVAL = 5 * 1000L; // ms
private static final int MAX_ATTEMPTS = 5;
private static final long CHECK_INTERVAL = 10 * 1000L; // ms
private static final int MAX_ATTEMPTS = 12;
public static void main(String[] args) {
Security.insertProviderAt(new BouncyCastleProvider(), 0);

View File

@@ -1,7 +1,9 @@
package org.qortal.account;
import java.math.BigDecimal;
import java.util.List;
import static org.qortal.utils.Amounts.prettyAmount;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -9,12 +11,11 @@ import org.qortal.block.BlockChain;
import org.qortal.data.account.AccountBalanceData;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.utils.Base58;
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
public class Account {
private static final Logger LOGGER = LogManager.getLogger(Account.class);
@@ -51,39 +52,52 @@ public class Account {
return new AccountData(this.address);
}
public void ensureAccount() throws DataException {
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
}
// Balance manipulations - assetId is 0 for QORT
public BigDecimal getBalance(long assetId, int height) throws DataException {
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId, height);
if (accountBalanceData == null)
return BigDecimal.ZERO.setScale(8);
return accountBalanceData.getBalance();
}
public BigDecimal getConfirmedBalance(long assetId) throws DataException {
public long getConfirmedBalance(long assetId) throws DataException {
AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId);
if (accountBalanceData == null)
return BigDecimal.ZERO.setScale(8);
return 0;
return accountBalanceData.getBalance();
}
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
public void setConfirmedBalance(long assetId, long balance) throws DataException {
// Safety feature!
if (balance.compareTo(BigDecimal.ZERO) < 0) {
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", balance.toPlainString(), assetId, this.address);
if (balance < 0) {
String message = String.format("Refusing to set negative balance %s [assetId %d] for %s", prettyAmount(balance), assetId, this.address);
LOGGER.error(message);
throw new DataException(message);
}
// Delete account balance record instead of setting balance to zero
if (balance == 0) {
this.repository.getAccountRepository().delete(this.address, assetId);
return;
}
// Can't have a balance without an account - make sure it exists!
this.repository.getAccountRepository().ensureAccount(this.buildAccountData());
this.ensureAccount();
AccountBalanceData accountBalanceData = new AccountBalanceData(this.address, assetId, balance);
this.repository.getAccountRepository().save(accountBalanceData);
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, balance.toPlainString(), assetId));
LOGGER.trace(() -> String.format("%s balance now %s [assetId %s]", this.address, prettyAmount(balance), assetId));
}
// Convenience method
public void modifyAssetBalance(long assetId, long deltaBalance) throws DataException {
this.repository.getAccountRepository().modifyAssetBalance(this.getAddress(), assetId, deltaBalance);
LOGGER.trace(() -> String.format("%s balance %s by %s [assetId %s]",
this.address,
(deltaBalance >= 0 ? "increased" : "decreased"),
prettyAmount(Math.abs(deltaBalance)),
assetId));
}
public void deleteBalance(long assetId) throws DataException {
@@ -99,38 +113,11 @@ public class Account {
* @throws DataException
*/
public byte[] getLastReference() throws DataException {
byte[] reference = this.repository.getAccountRepository().getLastReference(this.address);
byte[] reference = AccountRefCache.getLastReference(this.repository, this.address);
LOGGER.trace(() -> String.format("Last reference for %s is %s", this.address, reference == null ? "null" : Base58.encode(reference)));
return reference;
}
/**
* Fetch last reference for account, considering unconfirmed transactions only, or return null.
* <p>
* NOTE: calls Transaction.getUnconfirmedTransactions which discards uncommitted
* repository changes.
*
* @return byte[] reference, or null if no unconfirmed transactions for this account.
* @throws DataException
*/
public byte[] getUnconfirmedLastReference() throws DataException {
// Newest unconfirmed transaction takes priority
List<TransactionData> unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository);
byte[] reference = null;
for (TransactionData transactionData : unconfirmedTransactions) {
String unconfirmedTransactionAddress = PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey());
if (unconfirmedTransactionAddress.equals(this.address))
reference = transactionData.getSignature();
}
final byte[] loggingReference = reference;
LOGGER.trace(() -> String.format("Last unconfirmed reference for %s is %s", this.address, loggingReference == null ? "null" : Base58.encode(loggingReference)));
return reference;
}
/**
* Set last reference for account.
*
@@ -143,7 +130,7 @@ public class Account {
AccountData accountData = this.buildAccountData();
accountData.setReference(reference);
this.repository.getAccountRepository().setLastReference(accountData);
AccountRefCache.setLastReference(this.repository, accountData);
}
// Default groupID manipulations
@@ -204,11 +191,15 @@ public class Account {
* @throws DataException
*/
public boolean canMint() throws DataException {
Integer level = this.getLevel();
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
if (accountData == null)
return false;
Integer level = accountData.getLevel();
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
return true;
if (this.isFounder())
if (Account.isFounder(accountData.getFlags()))
return true;
return false;
@@ -226,11 +217,15 @@ public class Account {
* @throws DataException
*/
public boolean canRewardShare() throws DataException {
Integer level = this.getLevel();
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
if (accountData == null)
return false;
Integer level = accountData.getLevel();
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
return true;
if (this.isFounder())
if (Account.isFounder(accountData.getFlags()))
return true;
return false;
@@ -264,14 +259,14 @@ public class Account {
* @throws DataException
*/
public int getEffectiveMintingLevel() throws DataException {
if (this.isFounder())
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
Integer level = this.getLevel();
if (level == null)
AccountData accountData = this.repository.getAccountRepository().getAccount(this.address);
if (accountData == null)
return 0;
return level;
if (Account.isFounder(accountData.getFlags()))
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
return accountData.getLevel();
}
/**
@@ -290,7 +285,7 @@ public class Account {
if (rewardShareData == null)
return 0;
PublicKeyAccount rewardShareMinter = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
return rewardShareMinter.getEffectiveMintingLevel();
}

View File

@@ -0,0 +1,217 @@
package org.qortal.account;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BinaryOperator;
import org.qortal.data.account.AccountData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Pair;
/**
* Account lastReference caching
* <p>
* When checking an account's lastReference, the value returned should be the
* most recent value set after processing the most recent block.
* <p>
* However, when processing a batch of transactions, e.g. during block processing or validation,
* each transaction needs to check, and maybe update, multiple accounts' lastReference values.
* <p>
* Because the intermediate updates would affect future checks, we set up a cache of that
* maintains a consistent value for fetching lastReference, but also tracks the latest new
* value, without the overhead of repository calls.
* <p>
* Thus, when batch transaction processing is finished, only the latest new lastReference values
* can be committed to the repository, via {@link AccountRefCache#commit()}.
* <p>
* Getting and setting lastReferences values are done the usual way via
* {@link Account#getLastReference()} and {@link Account#setLastReference(byte[])} which call
* package-visibility methods in <tt>AccountRefCache</tt>.
* <p>
* If {@link Account#getLastReference()} or {@link Account#setLastReference(byte[])} are called
* outside of caching then lastReference values are fetched/set directly from/to the repository.
* <p>
* <tt>AccountRefCache</tt> implements <tt>AutoCloseable</tt> for (typical) use in a try-with-resources block.
*
* @see Account#getLastReference()
* @see Account#setLastReference(byte[])
* @see org.qortal.block.Block#process()
*/
public class AccountRefCache implements AutoCloseable {
private static final Map<Repository, RefCache> CACHE = new HashMap<>();
private static class RefCache {
private final Map<String, byte[]> getLastReferenceValues = new HashMap<>();
private final Map<String, Pair<byte[], byte[]>> setLastReferenceValues = new HashMap<>();
/**
* Function for merging publicKey from new data with old publicKey from map.
* <p>
* Last reference is <tt>A</tt> element in pair.<br>
* Public key is <tt>B</tt> element in pair.
*/
private static final BinaryOperator<Pair<byte[], byte[]>> mergePublicKey = (oldPair, newPair) -> {
// If passed new pair contains non-null publicKey, then we use that one in preference.
if (newPair.getB() == null)
// Otherwise, inherit publicKey from old map value.
newPair.setB(oldPair.getB());
// We always use new lastReference from new pair.
return newPair;
};
public byte[] getLastReference(Repository repository, String address) throws DataException {
synchronized (this.getLastReferenceValues) {
byte[] lastReference = getLastReferenceValues.get(address);
if (lastReference != null)
// address is present in map, lastReference not null
return lastReference;
// address is present in map, just lastReference is null
if (getLastReferenceValues.containsKey(address))
return null;
lastReference = repository.getAccountRepository().getLastReference(address);
this.getLastReferenceValues.put(address, lastReference);
return lastReference;
}
}
public void setLastReference(AccountData accountData) {
// We're only interested in lastReference and publicKey
Pair<byte[], byte[]> newPair = new Pair<>(accountData.getReference(), accountData.getPublicKey());
synchronized (this.setLastReferenceValues) {
setLastReferenceValues.merge(accountData.getAddress(), newPair, mergePublicKey);
}
}
Map<String, Pair<byte[], byte[]>> getNewLastReferences() {
return setLastReferenceValues;
}
}
private Repository repository;
/**
* Constructs a new account reference cache, unique to passed <tt>repository</tt> handle.
*
* @param repository
* @throws IllegalStateException if a cache already exists for <tt>repository</tt>
*/
public AccountRefCache(Repository repository) {
RefCache refCache = new RefCache();
synchronized (CACHE) {
if (CACHE.putIfAbsent(repository, refCache) != null)
throw new IllegalStateException("Account reference cache entry already exists");
}
this.repository = repository;
}
/**
* Save all cached setLastReference account-reference values into repository.
* <p>
* Closes cache to prevent any future setLastReference() attempts post-commit.
*
* @throws DataException
*/
public void commit() throws DataException {
RefCache refCache;
// Also duplicated in close(), this prevents future setLastReference() attempts post-commit.
synchronized (CACHE) {
refCache = CACHE.remove(this.repository);
}
if (refCache == null)
throw new IllegalStateException("Tried to commit non-existent account reference cache");
Map<String, Pair<byte[], byte[]>> newLastReferenceValues = refCache.getNewLastReferences();
for (Entry<String, Pair<byte[], byte[]>> entry : newLastReferenceValues.entrySet()) {
AccountData accountData = new AccountData(entry.getKey());
accountData.setReference(entry.getValue().getA());
if (entry.getValue().getB() != null)
accountData.setPublicKey(entry.getValue().getB());
this.repository.getAccountRepository().setLastReference(accountData);
}
}
@Override
public void close() {
synchronized (CACHE) {
CACHE.remove(this.repository);
}
}
/**
* Returns lastReference value for account.
* <p>
* If cache is not in effect for passed <tt>repository</tt> handle,
* then this method fetches lastReference directly from repository.
* <p>
* If cache <i>is</i> in effect, then this method returns cached
* lastReference, which is <b>not</b> affected by calls to
* <tt>setLastReference</tt>.
* <p>
* Typically called by corresponding method in Account class.
*
* @param repository
* @param address account's address
* @return account's lastReference, or null if account unknown, or lastReference not set
* @throws DataException
*/
/*package*/ static byte[] getLastReference(Repository repository, String address) throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.get(repository);
}
if (refCache == null)
return repository.getAccountRepository().getLastReference(address);
return refCache.getLastReference(repository, address);
}
/**
* Sets lastReference value for account.
* <p>
* If cache is not in effect for passed <tt>repository</tt> handle,
* then this method sets lastReference directly in repository.
* <p>
* If cache <i>is</i> in effect, then this method caches the new
* lastReference, which is <b>not</b> returned by calls to
* <tt>getLastReference</tt>.
* <p>
* Typically called by corresponding method in Account class.
*
* @param repository
* @param accountData
* @throws DataException
*/
/*package*/ static void setLastReference(Repository repository, AccountData accountData) throws DataException {
RefCache refCache;
synchronized (CACHE) {
refCache = CACHE.get(repository);
}
if (refCache == null) {
repository.getAccountRepository().setLastReference(accountData);
return;
}
refCache.setLastReference(accountData);
}
}

View File

@@ -1,13 +0,0 @@
package org.qortal.account;
import org.qortal.repository.Repository;
public final class GenesisAccount extends PublicKeyAccount {
public static final byte[] PUBLIC_KEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
public GenesisAccount(Repository repository) {
super(repository, PUBLIC_KEY);
}
}

View File

@@ -0,0 +1,24 @@
package org.qortal.account;
import org.qortal.crypto.Crypto;
import org.qortal.repository.Repository;
public final class NullAccount extends PublicKeyAccount {
public static final byte[] PUBLIC_KEY = new byte[32];
public static final String ADDRESS = Crypto.toAddress(PUBLIC_KEY);
public NullAccount(Repository repository) {
super(repository, PUBLIC_KEY, ADDRESS);
}
protected NullAccount() {
}
@Override
public boolean verify(byte[] signature, byte[] message) {
// Can't sign, hence can't verify.
return false;
}
}

View File

@@ -2,18 +2,11 @@ package org.qortal.account;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.qortal.crypto.BouncyCastle25519;
import org.qortal.crypto.Crypto;
import org.qortal.repository.Repository;
public class PrivateKeyAccount extends PublicKeyAccount {
private static final int SIGNATURE_LENGTH = 64;
private static final int SHARED_SECRET_LENGTH = 32;
private final byte[] privateKey;
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
@@ -49,24 +42,11 @@ public class PrivateKeyAccount extends PublicKeyAccount {
}
public byte[] sign(byte[] message) {
byte[] signature = new byte[SIGNATURE_LENGTH];
this.edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPublicKeyParams, null, message, 0, message.length, signature, 0);
return signature;
return Crypto.sign(this.edPrivateKeyParams, message);
}
public byte[] getSharedSecret(byte[] publicKey) {
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(this.privateKey);
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
return sharedSecret;
return Crypto.getSharedSecret(this.privateKey, publicKey);
}
public byte[] getRewardSharePrivateKey(byte[] publicKey) {

View File

@@ -1,7 +1,6 @@
package org.qortal.account;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.repository.Repository;
@@ -22,6 +21,18 @@ public class PublicKeyAccount extends Account {
this.publicKey = edPublicKeyParams.getEncoded();
}
protected PublicKeyAccount(Repository repository, byte[] publicKey, String address) {
super(repository, address);
this.publicKey = publicKey;
this.edPublicKeyParams = null;
}
protected PublicKeyAccount() {
this.publicKey = null;
this.edPublicKeyParams = null;
}
public byte[] getPublicKey() {
return this.publicKey;
}
@@ -34,15 +45,7 @@ public class PublicKeyAccount extends Account {
}
public boolean verify(byte[] signature, byte[] message) {
return PublicKeyAccount.verify(this.publicKey, signature, message);
}
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
try {
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
} catch (Exception e) {
return false;
}
return Crypto.verify(this.publicKey, signature, message);
}
public static String getAddress(byte[] publicKey) {

View File

@@ -0,0 +1,27 @@
package org.qortal.api;
import java.math.BigDecimal;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import org.qortal.utils.Amounts;
public class AmountTypeAdapter extends XmlAdapter<String, Long> {
@Override
public Long unmarshal(String input) throws Exception {
if (input == null)
return null;
return new BigDecimal(input).setScale(8).unscaledValue().longValue();
}
@Override
public String marshal(Long output) throws Exception {
if (output == null)
return null;
return Amounts.prettyAmount(output);
}
}

View File

@@ -5,6 +5,12 @@ import static java.util.stream.Collectors.toMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement
public enum ApiError {
// COMMON
// UNKNOWN(0, 500),
@@ -15,6 +21,7 @@ public enum ApiError {
REPOSITORY_ISSUE(5, 500),
NON_PRODUCTION(6, 403),
BLOCKCHAIN_NEEDS_SYNC(7, 503),
NO_TIME_SYNC(8, 503),
// VALIDATION
INVALID_SIGNATURE(101, 400),
@@ -117,7 +124,12 @@ public enum ApiError {
// MESSAGESIZE_EXCEEDED(1004, 400),
// Groups
GROUP_UNKNOWN(1101, 404);
GROUP_UNKNOWN(1101, 404),
// Bitcoin
BTC_NETWORK_ISSUE(1201, 500),
BTC_BALANCE_ISSUE(1202, 402),
BTC_TOO_SOON(1203, 408);
private static final Map<Integer, ApiError> map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError));

View File

@@ -3,6 +3,7 @@ package org.qortal.api;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -17,7 +18,7 @@ public class ApiErrorHandler extends ErrorHandler {
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (Settings.getInstance().isApiLoggingEnabled()) {
String requestURI = request.getRequestURI();

View File

@@ -0,0 +1,20 @@
package org.qortal.api;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
public class ApiErrorRoot {
private ApiError apiError;
@XmlJavaTypeAdapter(ApiErrorTypeAdapter.class)
@XmlElement(name = "error")
public ApiError getApiError() {
return this.apiError;
}
public void setApiError(ApiError apiError) {
this.apiError = apiError;
}
}

View File

@@ -0,0 +1,32 @@
package org.qortal.api;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class ApiErrorTypeAdapter extends XmlAdapter<ApiErrorTypeAdapter.AdaptedApiError, ApiError> {
public static class AdaptedApiError {
public int code;
public String description;
}
@Override
public ApiError unmarshal(AdaptedApiError adaptedInput) throws Exception {
if (adaptedInput == null)
return null;
return ApiError.fromCode(adaptedInput.code);
}
@Override
public AdaptedApiError marshal(ApiError output) throws Exception {
if (output == null)
return null;
AdaptedApiError adaptedOutput = new AdaptedApiError();
adaptedOutput.code = output.getCode();
adaptedOutput.description = output.name();
return adaptedOutput;
}
}

View File

@@ -149,8 +149,8 @@ public class ApiRequest {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setConnectTimeout(5000);
con.setReadTimeout(3000);
con.setConnectTimeout(30000);
con.setReadTimeout(10000);
ApiRequest.setConnectionSSL(con, ipAddress);
int status = con.getResponseCode();

View File

@@ -2,15 +2,31 @@ package org.qortal.api;
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.SecureRandom;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.DetectorConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.RequestLogWriter;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ErrorHandler;
import org.eclipse.jetty.server.handler.InetAccessHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
@@ -18,10 +34,17 @@ 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.eclipse.jetty.util.ssl.SslContextFactory;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import org.qortal.api.resource.AnnotationPostProcessor;
import org.qortal.api.resource.ApiDefinition;
import org.qortal.api.websocket.ActiveChatsWebSocket;
import org.qortal.api.websocket.AdminStatusWebSocket;
import org.qortal.api.websocket.BlocksWebSocket;
import org.qortal.api.websocket.ChatMessagesWebSocket;
import org.qortal.api.websocket.TradeBotWebSocket;
import org.qortal.api.websocket.TradeOffersWebSocket;
import org.qortal.settings.Settings;
public class ApiService {
@@ -53,9 +76,57 @@ public class ApiService {
public void start() {
try {
// Create API server
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
this.server = new Server(endpoint);
// SSL support if requested
String keystorePathname = Settings.getInstance().getSslKeystorePathname();
String keystorePassword = Settings.getInstance().getSslKeystorePassword();
if (keystorePathname != null && keystorePassword != null) {
// SSL version
if (!Files.isReadable(Path.of(keystorePathname)))
throw new RuntimeException("Failed to start SSL API due to broken keystore");
// BouncyCastle-specific SSLContext build
SSLContext sslContext = SSLContext.getInstance("TLS", "BCJSSE");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType(), "BC");
try (InputStream keystoreStream = Files.newInputStream(Paths.get(keystorePathname))) {
keyStore.load(keystoreStream, keystorePassword.toCharArray());
}
keyManagerFactory.init(keyStore, keystorePassword.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), null, new SecureRandom());
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
sslContextFactory.setSslContext(sslContext);
this.server = new Server();
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(Settings.getInstance().getApiPort());
SecureRequestCustomizer src = new SecureRequestCustomizer();
httpConfig.addCustomizer(src);
HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
this.server.addConnector(portUnifiedConnector);
} else {
// Non-SSL
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
this.server = new Server(endpoint);
}
// Error handler
ErrorHandler errorHandler = new ApiErrorHandler();
@@ -108,10 +179,28 @@ public class ApiService {
swaggerUIServlet.setInitParameter("pathInfoOnly", "true");
context.addServlet(swaggerUIServlet, "/api-documentation/*");
rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect to Swagger UI start page
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to Swagger UI start page
rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect empty path to API docs
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to add trailing slash if missing
} else {
// Simple pages that explains that API documentation is disabled
ClassLoader loader = this.getClass().getClassLoader();
ServletHolder swaggerUIServlet = new ServletHolder("api-docs-disabled", DefaultServlet.class);
swaggerUIServlet.setInitParameter("resourceBase", loader.getResource("api-docs-disabled/").toString());
swaggerUIServlet.setInitParameter("dirAllowed", "true");
swaggerUIServlet.setInitParameter("pathInfoOnly", "true");
context.addServlet(swaggerUIServlet, "/api-documentation/*");
rewriteHandler.addRule(new RedirectPatternRule("", "/api-documentation/")); // redirect empty path to API docs
rewriteHandler.addRule(new RedirectPatternRule("/api-documentation", "/api-documentation/")); // redirect to add trailing slash if missing
}
context.addServlet(AdminStatusWebSocket.class, "/websockets/admin/status");
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
// Start server
this.server.start();
} catch (Exception e) {

View File

@@ -0,0 +1,25 @@
package org.qortal.api;
import java.math.BigDecimal;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class RewardSharePercentTypeAdapter extends XmlAdapter<String, Integer> {
@Override
public Integer unmarshal(String input) throws Exception {
if (input == null)
return null;
return new BigDecimal(input).setScale(2).unscaledValue().intValue();
}
@Override
public String marshal(Integer output) throws Exception {
if (output == null)
return null;
return String.format("%d.%02d", output / 100, Math.abs(output % 100));
}
}

View File

@@ -1,11 +1,10 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.data.asset.OrderData;
@@ -29,12 +28,14 @@ public class AggregatedOrder {
}
@XmlElement(name = "price")
public BigDecimal getPrice() {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long getPrice() {
return this.orderData.getPrice();
}
@XmlElement(name = "unfulfilled")
public BigDecimal getUnfulfilled() {
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long getUnfulfilled() {
return this.orderData.getAmount();
}

View File

@@ -0,0 +1,25 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class BitcoinSendRequest {
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
public String xprv58;
@Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE")
public String receivingAddress;
@Schema(description = "Amount of BTC to send")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
public BitcoinSendRequest() {
}
}

View File

@@ -0,0 +1,71 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockInfo {
private byte[] signature;
private int height;
private long timestamp;
private int transactionCount;
private String minterAddress;
protected BlockInfo() {
/* For JAXB */
}
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
this.signature = signature;
this.height = height;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
this.minterAddress = minterAddress;
}
public BlockInfo(BlockData blockData) {
// Convert BlockData to BlockInfo, using additional data
this.minterAddress = "unknown?";
try (final Repository repository = RepositoryManager.getRepository()) {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
if (rewardShareData != null)
this.minterAddress = rewardShareData.getMintingAccount();
} catch (DataException e) {
// We'll carry on with placeholder minterAddress then...
}
this.signature = blockData.getSignature();
this.height = blockData.getHeight();
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
}
public byte[] getSignature() {
return this.signature;
}
public int getHeight() {
return this.height;
}
public long getTimestamp() {
return this.timestamp;
}
public int getTransactionCount() {
return this.transactionCount;
}
public String getMinterAddress() {
return this.minterAddress;
}
}

View File

@@ -6,7 +6,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.crypto.Crypto;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockMinterSummary {
public class BlockSignerSummary {
// Properties
@@ -20,22 +20,25 @@ public class BlockMinterSummary {
// Constructors
protected BlockMinterSummary() {
protected BlockSignerSummary() {
}
/** Constructs BlockMinterSummary in non-reward-share context. */
public BlockMinterSummary(byte[] blockMinterPublicKey, int blockCount) {
/** Constructs BlockSignerSummary in non-reward-share context. */
public BlockSignerSummary(byte[] blockMinterPublicKey, int blockCount) {
this.blockCount = blockCount;
this.mintingAccountPublicKey = blockMinterPublicKey;
this.mintingAccount = Crypto.toAddress(this.mintingAccountPublicKey);
}
/** Constructs BlockMinterSummary in reward-share context. */
public BlockMinterSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String recipientAccount) {
this(mintingAccountPublicKey, blockCount);
/** Constructs BlockSignerSummary in reward-share context. */
public BlockSignerSummary(byte[] rewardSharePublicKey, int blockCount, byte[] mintingAccountPublicKey, String minterAccount, String recipientAccount) {
this.rewardSharePublicKey = rewardSharePublicKey;
this.blockCount = blockCount;
this.mintingAccountPublicKey = mintingAccountPublicKey;
this.mintingAccount = minterAccount;
this.recipientAccount = recipientAccount;
}

View File

@@ -25,7 +25,8 @@ public class ConnectedPeer {
public String address;
public String version;
public Long buildTimestamp;
public String nodeId;
public Integer lastHeight;
@Schema(example = "base58")
@@ -45,10 +46,9 @@ public class ConnectedPeer {
this.peersConnectedWhen = peer.getPeersConnectionTimestamp();
this.address = peerData.getAddress().toString();
if (peer.getVersionMessage() != null) {
this.version = peer.getVersionMessage().getVersionString();
this.buildTimestamp = peer.getVersionMessage().getBuildTimestamp();
}
this.version = peer.getPeersVersionString();
this.nodeId = peer.getPeersNodeId();
PeerChainTipData peerChainTipData = peer.getChainTipData();
if (peerChainTipData != null) {

View File

@@ -0,0 +1,31 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinP2SHStatus {
@Schema(description = "Bitcoin P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)")
public String bitcoinP2shAddress;
@Schema(description = "Bitcoin P2SH balance")
public BigDecimal bitcoinP2shBalance;
@Schema(description = "Can P2SH redeem yet?")
public boolean canRedeem;
@Schema(description = "Can P2SH refund yet?")
public boolean canRefund;
@Schema(description = "Secret extracted by P2SH redeemer")
public byte[] secret;
public CrossChainBitcoinP2SHStatus() {
}
}

View File

@@ -0,0 +1,34 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinRedeemRequest {
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
public byte[] refundPublicKeyHash;
@Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cUvGNSnu14q6Hr1X7TESjYVTqBpFjj8GGLGjGdpJwD9NhSQKeYUk")
public byte[] redeemPrivateKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
public BigDecimal bitcoinMinerFee;
@Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
public byte[] secret;
@Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
public byte[] receivingAccountInfo;
public CrossChainBitcoinRedeemRequest() {
}
}

View File

@@ -0,0 +1,28 @@
package org.qortal.api.model;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinRefundRequest {
@Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
public byte[] refundPrivateKey;
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] redeemPublicKeyHash;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Bitcoin miner fee", example = "0.00001000")
public BigDecimal bitcoinMinerFee;
public CrossChainBitcoinRefundRequest() {
}
}

View File

@@ -0,0 +1,23 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBitcoinTemplateRequest {
@Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
public byte[] refundPublicKeyHash;
@Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] redeemPublicKeyHash;
@Schema(description = "Qortal AT address")
public String atAddress;
public CrossChainBitcoinTemplateRequest() {
}
}

View File

@@ -0,0 +1,39 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainBuildRequest {
@Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
public byte[] bitcoinPublicKeyHash;
@Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
public byte[] hashOfSecretB;
@Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
@Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
public Integer tradeTimeout;
public CrossChainBuildRequest() {
}
}

View File

@@ -0,0 +1,20 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainCancelRequest {
@Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] creatorPublicKey;
@Schema(description = "Qortal trade AT address")
public String atAddress;
public CrossChainCancelRequest() {
}
}

View File

@@ -0,0 +1,86 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.crosschain.CrossChainTradeData;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainOfferSummary {
// Properties
@Schema(description = "AT's Qortal address")
public String qortalAtAddress;
@Schema(description = "AT creator's Qortal address")
public String qortalCreator;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long btcAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
private int tradeTimeout;
private BTCACCT.Mode mode;
private long timestamp;
private String partnerQortalReceivingAddress;
protected CrossChainOfferSummary() {
/* For JAXB */
}
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
this.qortalCreator = crossChainTradeData.qortalCreator;
this.qortAmount = crossChainTradeData.qortAmount;
this.btcAmount = crossChainTradeData.expectedBitcoin;
this.tradeTimeout = crossChainTradeData.tradeTimeout;
this.mode = crossChainTradeData.mode;
this.timestamp = timestamp;
this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
}
public String getQortalAtAddress() {
return this.qortalAtAddress;
}
public String getQortalCreator() {
return this.qortalCreator;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
public int getTradeTimeout() {
return this.tradeTimeout;
}
public BTCACCT.Mode getMode() {
return this.mode;
}
public long getTimestamp() {
return this.timestamp;
}
public String getPartnerQortalReceivingAddress() {
return this.partnerQortalReceivingAddress;
}
}

View File

@@ -0,0 +1,29 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainSecretRequest {
@Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] partnerPublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
public byte[] secretA;
@Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
public byte[] secretB;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public CrossChainSecretRequest() {
}
}

View File

@@ -0,0 +1,23 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeRequest {
@Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
public byte[] tradePublicKey;
@Schema(description = "Qortal AT address")
public String atAddress;
@Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
public byte[] messageTransactionSignature;
public CrossChainTradeRequest() {
}
}

View File

@@ -0,0 +1,43 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.data.crosschain.CrossChainTradeData;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainTradeSummary {
private long tradeTimestamp;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long btcAmount;
protected CrossChainTradeSummary() {
/* For JAXB */
}
public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
this.tradeTimestamp = timestamp;
this.qortAmount = crossChainTradeData.qortAmount;
this.btcAmount = crossChainTradeData.expectedBitcoin;
}
public long getTradeTimestamp() {
return this.tradeTimestamp;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
}

View File

@@ -10,6 +10,7 @@ public class NodeInfo {
public long uptime;
public String buildVersion;
public long buildTimestamp;
public String nodeId;
public NodeInfo() {
}

View File

@@ -3,13 +3,34 @@ package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.controller.Controller;
import org.qortal.network.Network;
@XmlAccessorType(XmlAccessType.FIELD)
public class NodeStatus {
public boolean isMintingPossible;
public boolean isSynchronizing;
public final boolean isMintingPossible;
public final boolean isSynchronizing;
// Not always present
public final Integer syncPercent;
public final int numberOfConnections;
public final int height;
public NodeStatus() {
isMintingPossible = Controller.getInstance().isMintingPossible();
isSynchronizing = Controller.getInstance().isSynchronizing();
if (isSynchronizing)
syncPercent = Controller.getInstance().getSyncPercent();
else
syncPercent = null;
numberOfConnections = Network.getInstance().getHandshakedPeers().size();
height = Controller.getInstance().getChainHeight();
}
}

View File

@@ -0,0 +1,36 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotCreateRequest {
@Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
public byte[] creatorPublicKey;
@Schema(description = "QORT amount paid out on successful trade", example = "80.40200000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "81")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long fundingQortAmount;
@Schema(description = "Bitcoin amount wanted in return", example = "0.00864200")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long bitcoinAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
public int tradeTimeout;
@Schema(description = "Bitcoin address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE")
public String receivingAddress;
public TradeBotCreateRequest() {
}
}

View File

@@ -0,0 +1,23 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotRespondRequest {
@Schema(description = "Qortal AT address", example = "AH3e3jHEsGHPVQPDiJx4pYqgVi72auxgVy")
public String atAddress;
@Schema(description = "Bitcoin BIP32 extended private key", example = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TbTVGajEB55L1HYLg2aQMecZLXLre5YJcawpdFG66STVAWPJ")
public String xprv58;
@Schema(description = "Qortal address for receiving QORT from AT")
public String receivingAddress;
public TradeBotRespondRequest() {
}
}

View File

@@ -28,6 +28,7 @@ import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ApiOnlineAccount;
import org.qortal.api.model.RewardShareKeyRequest;
import org.qortal.asset.Asset;
@@ -36,18 +37,27 @@ import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.transaction.PublicizeTransactionData;
import org.qortal.data.transaction.RewardShareTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.PublicizeTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.PublicizeTransactionTransformer;
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@Path("/addresses")
@Tag(name = "Addresses")
public class AddressesResource {
@@ -66,32 +76,18 @@ public class AddressesResource {
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public AccountData getAccountInfo(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
accountData = new AccountData(address);
else {
// Unconfirmed transactions could update lastReference
Account account = new Account(repository, address);
// Use last reference based on unconfirmed transactions if possible
byte[] unconfirmedLastReference = account.getUnconfirmedLastReference();
if (unconfirmedLastReference != null)
// There are unconfirmed transactions so modify returned data
accountData.setReference(unconfirmedLastReference);
}
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
return accountData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -100,42 +96,37 @@ public class AddressesResource {
@GET
@Path("/lastreference/{address}")
@Operation(
summary = "Fetch reference for next transaction to be created by address, considering unconfirmed transactions",
description = "Returns the base58-encoded signature of the last confirmed/unconfirmed transaction created by address, failing that: 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 base58-encoded signature of the last confirmed transaction created by address, failing that: the first incoming transaction. Returns \"false\" if there is no last-reference.",
responses = {
@ApiResponse(
description = "the base58-encoded transaction signature",
description = "the base58-encoded last-reference",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public String getLastReferenceUnconfirmed(@PathParam("address") String address) {
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String getLastReference(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
byte[] lastReference = null;
try (final Repository repository = RepositoryManager.getRepository()) {
Account account = new Account(repository, address);
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Use last reference based on unconfirmed transactions if possible
lastReference = account.getUnconfirmedLastReference();
if (lastReference == null)
// No unconfirmed transactions so fallback to using one save in account data
lastReference = account.getLastReference();
} catch (ApiException e) {
throw e;
lastReference = accountData.getReference();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if(lastReference == null || lastReference.length == 0) {
if (lastReference == null || lastReference.length == 0)
return "false";
} else {
return Base58.encode(lastReference);
}
return Base58.encode(lastReference);
}
@GET
@@ -192,7 +183,7 @@ public class AddressesResource {
@Path("/balance/{address}")
@Operation(
summary = "Returns account balance",
description = "Returns account's balance, optionally of given asset and at given height",
description = "Returns account's QORT balance, or of other specified asset",
responses = {
@ApiResponse(
description = "the balance",
@@ -202,8 +193,7 @@ public class AddressesResource {
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.INVALID_ASSET_ID, ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE})
public BigDecimal getBalance(@PathParam("address") String address,
@QueryParam("assetId") Long assetId,
@QueryParam("height") Integer height) {
@QueryParam("assetId") Long assetId) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
@@ -215,12 +205,7 @@ public class AddressesResource {
else if (!repository.getAssetRepository().assetExists(assetId))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ASSET_ID);
if (height == null)
height = repository.getBlockRepository().getBlockchainHeight();
else if (height <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT);
return account.getBalance(assetId, height);
return Amounts.toBigDecimal(account.getConfirmedBalance(assetId));
} catch (ApiException e) {
throw e;
} catch (DataException e) {
@@ -414,4 +399,119 @@ public class AddressesResource {
}
}
@POST
@Path("/publicize")
@Operation(
summary = "Build raw, unsigned, PUBLICIZE transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = PublicizeTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String publicize(PublicizeTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/publicize/compute")
@Operation(
summary = "Compute nonce for raw, unsigned PUBLICIZE transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, unsigned PUBLICIZE transaction in base58 encoding",
example = "raw transaction base58"
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String computePublicize(String rawBytes58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
// We're expecting unsigned transaction, so append empty signature prior to decoding
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (transactionData.getType() != TransactionType.PUBLICIZE)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
PublicizeTransaction publicizeTransaction = (PublicizeTransaction) Transaction.fromData(repository, transactionData);
// Quicker validity check first before we compute nonce
ValidationResult result = publicizeTransaction.isValid();
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
throw TransactionsResource.createTransactionInvalidException(request, result);
publicizeTransaction.computeNonce();
// Re-check, but ignores signature
result = publicizeTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
// Strip zeroed signature
transactionData.setSignature(null);
byte[] bytes = PublicizeTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -36,8 +36,8 @@ import javax.ws.rs.core.MediaType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
@@ -117,6 +117,7 @@ public class AdminResource {
nodeInfo.uptime = System.currentTimeMillis() - Controller.startTime;
nodeInfo.buildVersion = Controller.getInstance().getVersionString();
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
return nodeInfo;
}
@@ -136,9 +137,6 @@ public class AdminResource {
NodeStatus nodeStatus = new NodeStatus();
nodeStatus.isMintingPossible = Controller.getInstance().isMintingPossible();
nodeStatus.isSynchronizing = Controller.getInstance().isSynchronizing();
return nodeStatus;
}
@@ -240,7 +238,7 @@ public class AdminResource {
// ignore
}
return new MintingAccountData(mintingAccountData.getPrivateKey(), rewardShareData);
return new MintingAccountData(mintingAccountData, rewardShareData);
}).collect(Collectors.toList());
return mintingAccounts;
@@ -284,11 +282,11 @@ public class AdminResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
// Qortal: check reward-share's minting account is still allowed to mint
PublicKeyAccount rewardShareMintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
Account rewardShareMintingAccount = new Account(repository, rewardShareData.getMinter());
if (!rewardShareMintingAccount.canMint())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.CANNOT_MINT);
MintingAccountData mintingAccountData = new MintingAccountData(seed);
MintingAccountData mintingAccountData = new MintingAccountData(mintingAccount.getPrivateKey(), mintingAccount.getPublicKey());
repository.getAccountRepository().save(mintingAccountData);
repository.saveChanges();
@@ -304,13 +302,13 @@ public class AdminResource {
@DELETE
@Path("/mintingaccounts")
@Operation(
summary = "Remove account/reward-share from use by BlockMinter, using private key",
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "private key"
type = "string", example = "public or private key"
)
)
),
@@ -321,13 +319,13 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
public String deleteMintingAccount(String seed58) {
public String deleteMintingAccount(String key58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim());
byte[] key = Base58.decode(key58.trim());
if (repository.getAccountRepository().delete(seed) == 0)
if (repository.getAccountRepository().delete(key) == 0)
return "false";
repository.saveChanges();

View File

@@ -13,7 +13,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Admin"),
@Tag(name = "Arbitrary"),
@Tag(name = "Assets"),
@Tag(name = "Automated Transactions"),
@Tag(name = "Blocks"),
@Tag(name = "Chat"),
@Tag(name = "Cross-Chain"),
@Tag(name = "Groups"),
@Tag(name = "Names"),
@Tag(name = "Payments"),

View File

@@ -0,0 +1,206 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.ciyam.at.MachineState;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58;
@Path("/at")
@Tag(name = "Automated Transactions")
public class AtResource {
@Context
HttpServletRequest request;
@GET
@Path("/byfunction/{codehash}")
@Operation(
summary = "Find automated transactions with matching functionality (code hash)",
responses = {
@ApiResponse(
description = "automated transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ATData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
})
public List<ATData> getByFunctionality(
@PathParam("codehash")
String codeHash58,
@Parameter(description = "whether to include ATs that can run, or not, or both (if omitted)")
@QueryParam("isExecutable")
Boolean isExecutable,
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
// Decode codeHash
byte[] codeHash;
try {
codeHash = Base58.decode(codeHash58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
}
// codeHash must be present and have correct length
if (codeHash == null || codeHash.length != 32)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Impose a limit on 'limit'
if (limit != null && limit > 100)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/{ataddress}")
@Operation(
summary = "Fetch info associated with AT address",
responses = {
@ApiResponse(
description = "automated transaction",
content = @Content(
schema = @Schema(implementation = ATData.class)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public ATData getByAddress(@PathParam("ataddress") String atAddress) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getATRepository().fromATAddress(atAddress);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/{ataddress}/data")
@Operation(
summary = "Fetch data segment associated with AT address",
responses = {
@ApiResponse(
description = "automated transaction",
content = @Content(
schema = @Schema(implementation = byte[].class)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public byte[] getDataByAddress(@PathParam("ataddress") String atAddress) {
try (final Repository repository = RepositoryManager.getRepository()) {
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
return dataBytes;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Operation(
summary = "Build raw, unsigned, DEPLOY_AT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = DeployAtTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, DEPLOY_AT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String createDeployAt(DeployAtTransactionData transactionData) {
if (Settings.getInstance().isApiRestricted())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
try (final Repository repository = RepositoryManager.getRepository()) {
Transaction transaction = Transaction.fromData(repository, transactionData);
ValidationResult result = transaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = DeployAtTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -22,9 +22,9 @@ import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.BlockMinterSummary;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.data.block.BlockData;
@@ -71,9 +71,11 @@ public class BlocksResource {
}
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().fromSignature(signature);
} catch (ApiException e) {
throw e;
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -120,8 +122,6 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return repository.getBlockRepository().getTransactionsFromSignature(signature, limit, offset, reverse);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -223,8 +223,6 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return childBlockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -253,8 +251,6 @@ public class BlocksResource {
public int getHeight() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockchainHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -297,8 +293,6 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData.getHeight();
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -330,8 +324,6 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -366,8 +358,6 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCK_UNKNOWN);
return blockData;
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
@@ -416,9 +406,9 @@ public class BlocksResource {
}
@GET
@Path("/minter/{address}")
@Path("/signer/{address}")
@Operation(
summary = "Fetch block summaries for blocks minted by address",
summary = "Fetch block summaries for blocks signed by address",
responses = {
@ApiResponse(
description = "block summaries",
@@ -433,7 +423,7 @@ public class BlocksResource {
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.PUBLIC_KEY_NOT_FOUND, ApiError.REPOSITORY_ISSUE})
public List<BlockSummaryData> getBlockSummariesByMinter(@PathParam("address") String address, @Parameter(
public List<BlockSummaryData> getBlockSummariesBySigner(@PathParam("address") String address, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
@@ -449,32 +439,30 @@ public class BlocksResource {
if (accountData == null || accountData.getPublicKey() == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.PUBLIC_KEY_NOT_FOUND);
return repository.getBlockRepository().getBlockSummariesByMinter(accountData.getPublicKey(), limit, offset, reverse);
} catch (ApiException e) {
throw e;
return repository.getBlockRepository().getBlockSummariesBySigner(accountData.getPublicKey(), limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/minters")
@Path("/signers")
@Operation(
summary = "Show summary of block minters",
description = "Returns count of blocks minted, optionally limited to minters/recipients in passed address(es).",
summary = "Show summary of block signers",
description = "Returns count of blocks signed, optionally limited to minters/recipients in passed address(es).",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockMinterSummary.class
implementation = BlockSignerSummary.class
)
)
)
)
}
)
public List<BlockMinterSummary> getBlockMinters(@QueryParam("address") List<String> addresses,
public List<BlockSignerSummary> getBlockSigners(@QueryParam("address") List<String> addresses,
@Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
@@ -487,7 +475,47 @@ public class BlocksResource {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
return repository.getBlockRepository().getBlockMinters(addresses, limit, offset, reverse);
return repository.getBlockRepository().getBlockSigners(addresses, limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/summaries")
@Operation(
summary = "Fetch only summary info about a range of blocks",
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
responses = {
@ApiResponse(
description = "blocks",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockInfo.class
)
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<BlockInfo> getBlockRange(
@QueryParam("start") Integer startHeight,
@QueryParam("end") Integer endHeight,
@Parameter(ref = "count") @QueryParam("count") Integer count) {
// Check up to 2 out of 3 params
if (startHeight != null && endHeight != null && count != null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check values
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -0,0 +1,247 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.crypto.Crypto;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.ChatTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ChatTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@Path("/chat")
@Tag(name = "Chat")
public class ChatResource {
@Context
HttpServletRequest request;
@GET
@Path("/messages")
@Operation(
summary = "Find chat messages",
description = "Returns CHAT messages that match criteria. Must provide EITHER 'txGroupId' OR two 'involving' addresses.",
responses = {
@ApiResponse(
description = "CHAT messages",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ChatMessage.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<ChatMessage> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("involving") List<String> involvingAddresses,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
// Check args meet expectations
if ((txGroupId == null && involvingAddresses.size() != 2)
|| (txGroupId != null && !involvingAddresses.isEmpty()))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check any provided addresses are valid
if (involvingAddresses.stream().anyMatch(address -> !Crypto.isValidAddress(address)))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (before != null && before < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (after != null && after < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getMessagesMatchingCriteria(
before,
after,
txGroupId,
involvingAddresses,
limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/active/{address}")
@Operation(
summary = "Find active chats (group/direct) involving address",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ActiveChats.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public ActiveChats getActiveChats(@PathParam("address") String address) {
if (address == null || !Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getActiveChats(address);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Operation(
summary = "Build raw, unsigned, CHAT transaction",
description = "Builds a raw, unsigned CHAT transaction but does NOT compute proof-of-work nonce. See POST /chat/compute.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ChatTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, CHAT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildChat(ChatTransactionData transactionData) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
ValidationResult result = chatTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/compute")
@Operation(
summary = "Compute nonce for raw, unsigned CHAT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "raw, unsigned CHAT transaction in base58 encoding",
example = "raw transaction base58"
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, CHAT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildChat(String rawBytes58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] rawBytes = Base58.decode(rawBytes58);
// We're expecting unsigned transaction, so append empty signature prior to decoding
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
if (transactionData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (transactionData.getType() != TransactionType.CHAT)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
// Quicker validity check first before we compute nonce
ValidationResult result = chatTransaction.isValid();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
chatTransaction.computeNonce();
// Re-check, but ignores signature
result = chatTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
// Strip zeroed signature
transactionData.setSignature(null);
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@@ -29,9 +30,8 @@ import org.qortal.data.network.PeerData;
import org.qortal.network.Network;
import org.qortal.network.PeerAddress;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.NTP;
@Path("/peers")
@Tag(name = "Peers")
@@ -81,11 +81,7 @@ public class PeersResource {
ApiError.REPOSITORY_ISSUE
})
public List<PeerData> getKnownPeers() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getNetworkRepository().getAllPeers();
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
return Network.getInstance().getAllKnownPeers();
}
@GET
@@ -166,18 +162,21 @@ public class PeersResource {
public String addPeer(String address) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
final Long addedWhen = NTP.getTime();
if (addedWhen == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NO_TIME_SYNC);
try {
PeerAddress peerAddress = PeerAddress.fromString(address);
PeerData peerData = new PeerData(peerAddress, System.currentTimeMillis(), "API");
repository.getNetworkRepository().save(peerData);
repository.saveChanges();
List<PeerAddress> newPeerAddresses = new ArrayList<>(1);
newPeerAddresses.add(peerAddress);
return "true";
boolean addResult = Network.getInstance().mergePeers("API", addedWhen, newPeerAddresses);
return addResult ? "true" : "false";
} catch (IllegalArgumentException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_NETWORK_ADDRESS);
} catch (ApiException e) {
throw e;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}

View File

@@ -363,6 +363,60 @@ public class TransactionsResource {
}
}
@GET
@Path("/creator/{publickey}")
@Operation(
summary = "Find matching transactions created by account with given public key",
responses = {
@ApiResponse(
description = "transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionData.class
)
)
)
)
}
)
@ApiErrors({
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
})
public List<TransactionData> findCreatorsTransactions(@PathParam("publickey") String publicKey58,
@Parameter(
description = "whether to include confirmed, unconfirmed or both",
required = true
) @QueryParam("confirmationStatus") ConfirmationStatus confirmationStatus, @Parameter(
ref = "limit"
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
// Decode public key
byte[] publicKey;
try {
publicKey = Base58.decode(publicKey58);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null,
publicKey, confirmationStatus, limit, offset, reverse);
// Expand signatures to transactions
List<TransactionData> transactions = new ArrayList<>(signatures.size());
for (byte[] signature : signatures)
transactions.add(repository.getTransactionRepository().fromSignature(signature));
return transactions;
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/sign")
@Operation(
@@ -470,21 +524,21 @@ public class TransactionsResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE);
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock(500, TimeUnit.MILLISECONDS))
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS))
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
try {
ValidationResult result = transaction.importAsUnconfirmed();
if (result != ValidationResult.OK)
throw createTransactionInvalidException(request, result);
// Notify controller of new transaction
Controller.getInstance().onNewTransaction(transactionData);
return "true";
} finally {
blockchainLock.unlock();
}
// Notify controller of new transaction
Controller.getInstance().onNewTransaction(transactionData, null);
return "true";
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
} catch (TransformationException e) {

View File

@@ -0,0 +1,92 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.crypto.Crypto;
import org.qortal.data.chat.ActiveChats;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class ActiveChatsWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(ActiveChatsWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
Map<String, String> pathParams = getPathParams(session, "/{address}");
String address = pathParams.get("address");
if (address == null || !Crypto.isValidAddress(address)) {
session.close(4001, "invalid address");
return;
}
AtomicReference<String> previousOutput = new AtomicReference<>(null);
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, address, previousOutput);
ChatNotifier.getInstance().register(session, listener);
this.onNotify(session, null, address, previousOutput);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
// If CHAT has a recipient (i.e. direct message, not group-based) and we're neither sender nor recipient, then it's of no interest
if (chatTransactionData != null) {
String recipient = chatTransactionData.getRecipient();
if (recipient != null && (!recipient.equals(ourAddress) && !chatTransactionData.getSender().equals(ourAddress)))
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress);
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, activeChats);
// Only output if something has changed
String output = stringWriter.toString();
if (output.equals(previousOutput.get()))
return;
previousOutput.set(output);
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}
}

View File

@@ -0,0 +1,73 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.NodeStatus;
import org.qortal.controller.StatusNotifier;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class AdminStatusWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(AdminStatusWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
AtomicReference<String> previousOutput = new AtomicReference<>(null);
StatusNotifier.Listener listener = timestamp -> onNotify(session, previousOutput);
StatusNotifier.getInstance().register(session, listener);
this.onNotify(session, previousOutput);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
StatusNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
private void onNotify(Session session,AtomicReference<String> previousOutput) {
try (final Repository repository = RepositoryManager.getRepository()) {
NodeStatus nodeStatus = new NodeStatus();
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, nodeStatus);
// Only output if something has changed
String output = stringWriter.toString();
if (output.equals(previousOutput.get()))
return;
previousOutput.set(output);
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}
}

View File

@@ -0,0 +1,120 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import org.eclipse.jetty.http.pathmap.UriTemplatePathSpec;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrorRoot;
@SuppressWarnings("serial")
abstract class ApiWebSocket extends WebSocketServlet {
private static final Map<Class<? extends ApiWebSocket>, List<Session>> SESSIONS_BY_CLASS = new HashMap<>();
protected static String getPathInfo(Session session) {
ServletUpgradeRequest upgradeRequest = (ServletUpgradeRequest) session.getUpgradeRequest();
return upgradeRequest.getHttpServletRequest().getPathInfo();
}
protected static Map<String, String> getPathParams(Session session, String pathSpec) {
UriTemplatePathSpec uriTemplatePathSpec = new UriTemplatePathSpec(pathSpec);
return uriTemplatePathSpec.getPathParams(getPathInfo(session));
}
protected static void sendError(Session session, ApiError apiError) {
ApiErrorRoot apiErrorRoot = new ApiErrorRoot();
apiErrorRoot.setApiError(apiError);
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, apiErrorRoot);
session.getRemote().sendString(stringWriter.toString());
} catch (IOException e) {
// Remote end probably closed
}
}
protected static void marshall(Writer writer, Object object) throws IOException {
Marshaller marshaller = createMarshaller(object.getClass());
try {
marshaller.marshal(object, writer);
} catch (JAXBException e) {
throw new IOException("Unable to create marshall object for websocket", e);
}
}
protected static void marshall(Writer writer, Collection<?> collection) throws IOException {
// If collection is empty then we're returning "[]" anyway
if (collection.isEmpty()) {
writer.append("[]");
return;
}
// Grab an entry from collection so we can determine type
Object entry = collection.iterator().next();
Marshaller marshaller = createMarshaller(entry.getClass());
try {
marshaller.marshal(collection, writer);
} catch (JAXBException e) {
throw new IOException("Unable to create marshall object for websocket", e);
}
}
private static Marshaller createMarshaller(Class<?> objectClass) {
try {
// Create JAXB context aware of object's class
JAXBContext jc = JAXBContextFactory.createContext(new Class[] { objectClass }, null);
// Create marshaller
Marshaller marshaller = jc.createMarshaller();
// Set the marshaller media type to JSON
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
// Tell marshaller not to include JSON root element in the output
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
return marshaller;
} catch (JAXBException e) {
throw new RuntimeException("Unable to create websocket marshaller", e);
}
}
public void onWebSocketConnect(Session session) {
synchronized (SESSIONS_BY_CLASS) {
SESSIONS_BY_CLASS.computeIfAbsent(this.getClass(), clazz -> new ArrayList<>()).add(session);
}
}
public void onWebSocketClose(Session session, int statusCode, String reason) {
synchronized (SESSIONS_BY_CLASS) {
SESSIONS_BY_CLASS.get(this.getClass()).remove(session);
}
}
protected List<Session> getSessions() {
synchronized (SESSIONS_BY_CLASS) {
return new ArrayList<>(SESSIONS_BY_CLASS.get(this.getClass()));
}
}
}

View File

@@ -0,0 +1,121 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.api.model.BlockInfo;
import org.qortal.controller.BlockNotifier;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class BlocksWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(BlocksWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
BlockNotifier.getInstance().register(session, listener);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
// We're expecting either a base58 block signature or an integer block height
if (message.length() > 128) {
// Try base58 block signature
byte[] signature;
try {
signature = Base58.decode(message);
} catch (NumberFormatException e) {
sendError(session, ApiError.INVALID_SIGNATURE);
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
int height = repository.getBlockRepository().getHeightFromSignature(signature);
if (height == 0) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
return;
}
if (message.length() > 10)
// Bigger than max integer value, so probably a ping - silently ignore
return;
// Try integer
int height;
try {
height = Integer.parseInt(message);
} catch (NumberFormatException e) {
sendError(session, ApiError.INVALID_HEIGHT);
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
}
private void onNotify(Session session, BlockInfo blockInfo) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, blockInfo);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time
}
}
}

View File

@@ -0,0 +1,152 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.ChatNotifier;
import org.qortal.data.chat.ChatMessage;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class ChatMessagesWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(ChatMessagesWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
List<String> txGroupIds = queryParams.get("txGroupId");
if (txGroupIds != null && txGroupIds.size() == 1) {
int txGroupId = Integer.parseInt(txGroupIds.get(0));
try (final Repository repository = RepositoryManager.getRepository()) {
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null,
null,
txGroupId,
null,
null, null, null);
sendMessages(session, chatMessages);
} catch (DataException e) {
// Not a good start
session.close(4001, "Couldn't fetch initial messages from repository");
return;
}
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, txGroupId);
ChatNotifier.getInstance().register(session, listener);
return;
}
List<String> involvingAddresses = queryParams.get("involving");
if (involvingAddresses == null || involvingAddresses.size() != 2) {
session.close(4001, "invalid criteria");
return;
}
try (final Repository repository = RepositoryManager.getRepository()) {
List<ChatMessage> chatMessages = repository.getChatRepository().getMessagesMatchingCriteria(
null,
null,
null,
involvingAddresses,
null, null, null);
sendMessages(session, chatMessages);
} catch (DataException e) {
// Not a good start
session.close(4001, "Couldn't fetch initial messages from repository");
return;
}
ChatNotifier.Listener listener = chatTransactionData -> onNotify(session, chatTransactionData, involvingAddresses);
ChatNotifier.getInstance().register(session, listener);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
if (chatTransactionData == null)
// There has been a group-membership change, but we're not interested
return;
// We only want group-based messages with our txGroupId
if (chatTransactionData.getRecipient() != null || chatTransactionData.getTxGroupId() != txGroupId)
return;
sendChat(session, chatTransactionData);
}
private void onNotify(Session session, ChatTransactionData chatTransactionData, List<String> involvingAddresses) {
// We only want direct/non-group messages where sender/recipient match our addresses
String recipient = chatTransactionData.getRecipient();
if (recipient == null)
return;
List<String> transactionAddresses = Arrays.asList(recipient, chatTransactionData.getSender());
if (!transactionAddresses.containsAll(involvingAddresses))
return;
sendChat(session, chatTransactionData);
}
private void sendMessages(Session session, List<ChatMessage> chatMessages) {
StringWriter stringWriter = new StringWriter();
try {
marshall(stringWriter, chatMessages);
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time?
}
}
private void sendChat(Session session, ChatTransactionData chatTransactionData) {
// Convert ChatTransactionData to ChatMessage
ChatMessage chatMessage;
try (final Repository repository = RepositoryManager.getRepository()) {
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData);
} catch (DataException e) {
// No output this time?
return;
}
sendMessages(session, Collections.singletonList(chatMessage));
}
}

View File

@@ -0,0 +1,119 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.controller.TradeBot;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.Base58;
@WebSocket
@SuppressWarnings("serial")
public class TradeBotWebSocket extends ApiWebSocket implements Listener {
/** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */
private static final Map<String, TradeBotData.State> PREVIOUS_STATES = new HashMap<>();
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeBotWebSocket.class);
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
if (tradeBotEntries == null)
// How do we properly fail here?
return;
PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getState)));
} catch (DataException e) {
// No output this time
}
EventBus.INSTANCE.addListener(this::listen);
}
@Override
public void listen(Event event) {
if (!(event instanceof TradeBot.StateChangeEvent))
return;
TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData();
String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey());
synchronized (PREVIOUS_STATES) {
if (PREVIOUS_STATES.get(tradePrivateKey58) == tradeBotData.getState())
// Not changed
return;
PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getState());
}
List<TradeBotData> tradeBotEntries = Collections.singletonList(tradeBotData);
for (Session session : getSessions())
sendEntries(session, tradeBotEntries);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
// Send all known trade-bot entries
try (final Repository repository = RepositoryManager.getRepository()) {
List<TradeBotData> tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
if (tradeBotEntries == null) {
session.close(4001, "repository issue fetching trade-bot entries");
return;
}
if (!sendEntries(session, tradeBotEntries)) {
session.close(4002, "websocket issue");
return;
}
} catch (DataException e) {
// No output this time
}
super.onWebSocketConnect(session);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
super.onWebSocketClose(session, statusCode, reason);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private boolean sendEntries(Session session, List<TradeBotData> tradeBotEntries) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, tradeBotEntries);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
}

View File

@@ -0,0 +1,212 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.BlockNotifier;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.utils.NTP;
@WebSocket
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeOffersWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
final boolean includeHistoric = queryParams.get("includeHistoric") != null;
final Map<String, BTCACCT.Mode> previousAtModes = new HashMap<>();
List<CrossChainOfferSummary> crossChainOfferSummaries;
try (final Repository repository = RepositoryManager.getRepository()) {
List<ATStateData> initialAtStates;
// We want ALL OFFERING trades
Boolean isFinished = Boolean.FALSE;
Integer dataByteOffset = BTCACCT.MODE_BYTE_OFFSET;
Long expectedValue = (long) BTCACCT.Mode.OFFERING.value;
Integer minimumFinalHeight = null;
initialAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (initialAtStates == null) {
session.close(4001, "repository issue fetching OFFERING trades");
return;
}
// Save initial AT modes
previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> BTCACCT.Mode.OFFERING)));
// Convert to offer summaries
crossChainOfferSummaries = produceSummaries(repository, initialAtStates, null);
if (includeHistoric) {
// We also want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
long timestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
if (minimumFinalHeight != 0) {
isFinished = Boolean.TRUE;
dataByteOffset = null;
expectedValue = null;
++minimumFinalHeight; // because height is just *before* timestamp
List<ATStateData> historicAtStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (historicAtStates == null) {
session.close(4002, "repository issue fetching historic trades");
return;
}
for (ATStateData historicAtState : historicAtStates) {
CrossChainOfferSummary historicOfferSummary = produceSummary(repository, historicAtState, null);
switch (historicOfferSummary.getMode()) {
case REDEEMED:
case REFUNDED:
case CANCELLED:
break;
default:
continue;
}
// Add summary to initial burst
crossChainOfferSummaries.add(historicOfferSummary);
// Save initial AT mode
previousAtModes.put(historicAtState.getATAddress(), historicOfferSummary.getMode());
}
}
}
} catch (DataException e) {
session.close(4003, "generic repository issue");
return;
}
if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
session.close(4004, "websocket issue");
return;
}
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo, previousAtModes);
BlockNotifier.getInstance().register(session, listener);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
/* ignored */
}
private void onNotify(Session session, BlockInfo blockInfo, final Map<String, BTCACCT.Mode> previousAtModes) {
List<CrossChainOfferSummary> crossChainOfferSummaries = null;
try (final Repository repository = RepositoryManager.getRepository()) {
// Find any new trade ATs since this block
final Boolean isFinished = null;
final Integer dataByteOffset = null;
final Long expectedValue = null;
final Integer minimumFinalHeight = blockInfo.getHeight();
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
null, null, null);
if (atStates == null)
return;
crossChainOfferSummaries = produceSummaries(repository, atStates, blockInfo.getTimestamp());
} catch (DataException e) {
// No output this time
}
synchronized (previousAtModes) { //NOSONAR squid:S2445 suppressed because previousAtModes is final and curried in lambda
// Remove any entries unchanged from last time
crossChainOfferSummaries.removeIf(offerSummary -> previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
// Don't send anything if no results
if (crossChainOfferSummaries.isEmpty())
return;
final boolean wasSent = sendOfferSummaries(session, crossChainOfferSummaries);
if (!wasSent)
return;
previousAtModes.putAll(crossChainOfferSummaries.stream().collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, CrossChainOfferSummary::getMode)));
}
}
private boolean sendOfferSummaries(Session session, List<CrossChainOfferSummary> crossChainOfferSummaries) {
try {
StringWriter stringWriter = new StringWriter();
marshall(stringWriter, crossChainOfferSummaries);
String output = stringWriter.toString();
session.getRemote().sendStringByFuture(output);
} catch (IOException e) {
// No output this time?
return false;
}
return true;
}
private static CrossChainOfferSummary produceSummary(Repository repository, ATStateData atState, Long timestamp) throws DataException {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
long atStateTimestamp;
if (crossChainTradeData.mode == BTCACCT.Mode.OFFERING)
// We want when trade was created, not when it was last updated
atStateTimestamp = atState.getCreation();
else
atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
return new CrossChainOfferSummary(crossChainTradeData, atStateTimestamp);
}
private static List<CrossChainOfferSummary> produceSummaries(Repository repository, List<ATStateData> atStates, Long timestamp) throws DataException {
List<CrossChainOfferSummary> offerSummaries = new ArrayList<>();
for (ATStateData atState : atStates)
offerSummaries.add(produceSummary(repository, atState, timestamp));
return offerSummaries;
}
}

View File

@@ -1,11 +1,13 @@
package org.qortal.asset;
import org.qortal.crypto.Crypto;
import org.qortal.data.asset.AssetData;
import org.qortal.data.transaction.IssueAssetTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.data.transaction.UpdateAssetTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
public class Asset {
@@ -21,12 +23,12 @@ public class Asset {
// Other useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MIN_NAME_SIZE = 3;
public static final int MAX_NAME_SIZE = 40;
public static final int MAX_DESCRIPTION_SIZE = 4000;
public static final int MAX_DATA_SIZE = 400000;
public static final long MAX_DIVISIBLE_QUANTITY = 10_000_000_000L; // but also to 8 decimal places
public static final long MAX_INDIVISIBLE_QUANTITY = 1_000_000_000_000_000_000L;
public static final long MAX_QUANTITY = 10_000_000_000L * Amounts.MULTIPLIER; // but also to 8 decimal places
// Properties
private Repository repository;
@@ -42,12 +44,14 @@ public class Asset {
public Asset(Repository repository, IssueAssetTransactionData issueAssetTransactionData) {
this.repository = repository;
String ownerAddress = Crypto.toAddress(issueAssetTransactionData.getCreatorPublicKey());
// NOTE: transaction's reference is used to look up newly assigned assetID on creation!
this.assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(),
this.assetData = new AssetData(ownerAddress, issueAssetTransactionData.getAssetName(),
issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(),
issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getData(),
issueAssetTransactionData.getIsUnspendable(),
issueAssetTransactionData.getTxGroupId(), issueAssetTransactionData.getSignature());
issueAssetTransactionData.isDivisible(), issueAssetTransactionData.getData(),
issueAssetTransactionData.isUnspendable(), issueAssetTransactionData.getTxGroupId(),
issueAssetTransactionData.getSignature(), issueAssetTransactionData.getReducedAssetName());
}
public Asset(Repository repository, long assetId) throws DataException {
@@ -118,10 +122,11 @@ public class Asset {
throw new IllegalStateException("Missing referenced transaction when orphaning UPDATE_ASSET");
switch (previousTransactionData.getType()) {
case ISSUE_ASSET:
case ISSUE_ASSET: {
IssueAssetTransactionData previousIssueAssetTransactionData = (IssueAssetTransactionData) previousTransactionData;
this.assetData.setOwner(previousIssueAssetTransactionData.getOwner());
String ownerAddress = Crypto.toAddress(previousIssueAssetTransactionData.getCreatorPublicKey());
this.assetData.setOwner(ownerAddress);
if (needDescription) {
this.assetData.setDescription(previousIssueAssetTransactionData.getDescription());
@@ -133,8 +138,9 @@ public class Asset {
needData = false;
}
break;
}
case UPDATE_ASSET:
case UPDATE_ASSET: {
UpdateAssetTransactionData previousUpdateAssetTransactionData = (UpdateAssetTransactionData) previousTransactionData;
this.assetData.setOwner(previousUpdateAssetTransactionData.getNewOwner());
@@ -152,7 +158,9 @@ public class Asset {
// Get signature for previous transaction in chain, just in case we need it
if (needDescription || needData)
previousTransactionSignature = previousUpdateAssetTransactionData.getOrphanReference();
break;
}
default:
throw new IllegalStateException("Invalid referenced transaction when orphaning UPDATE_ASSET");

View File

@@ -1,8 +1,8 @@
package org.qortal.asset;
import java.math.BigDecimal;
import static org.qortal.utils.Amounts.prettyAmount;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
@@ -11,13 +11,12 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.data.asset.AssetData;
import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.TradeData;
import org.qortal.repository.AssetRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
public class Order {
@@ -29,9 +28,11 @@ public class Order {
private OrderData orderData;
// Used quite a bit
private final boolean isOurOrderNewPricing;
private final long haveAssetId;
private final long wantAssetId;
private final boolean isAmountInWantAsset;
private final BigInteger orderAmount;
private final BigInteger orderPrice;
/** Cache of price-pair units e.g. QORT/GOLD, but use getPricePair() instead! */
private String cachedPricePair;
@@ -47,9 +48,12 @@ public class Order {
this.repository = repository;
this.orderData = orderData;
this.isOurOrderNewPricing = this.orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
this.haveAssetId = this.orderData.getHaveAssetId();
this.wantAssetId = this.orderData.getWantAssetId();
this.isAmountInWantAsset = haveAssetId < wantAssetId;
this.orderAmount = BigInteger.valueOf(this.orderData.getAmount());
this.orderPrice = BigInteger.valueOf(this.orderData.getPrice());
}
// Getters/Setters
@@ -60,16 +64,16 @@ public class Order {
// More information
public static BigDecimal getAmountLeft(OrderData orderData) {
return orderData.getAmount().subtract(orderData.getFulfilled());
public static long getAmountLeft(OrderData orderData) {
return orderData.getAmount() - orderData.getFulfilled();
}
public BigDecimal getAmountLeft() {
public long getAmountLeft() {
return Order.getAmountLeft(this.orderData);
}
public static boolean isFulfilled(OrderData orderData) {
return orderData.getFulfilled().compareTo(orderData.getAmount()) == 0;
return orderData.getFulfilled() == orderData.getAmount();
}
public boolean isFulfilled() {
@@ -86,13 +90,10 @@ public class Order {
* <p>
* @return granularity of matched-amount
*/
public static BigDecimal calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, BigDecimal price) {
// Multiplier to scale BigDecimal fractional amounts into integer domain
BigInteger multiplier = BigInteger.valueOf(1_0000_0000L);
public static long calculateAmountGranularity(boolean isAmountAssetDivisible, boolean isReturnAssetDivisible, long price) {
// Calculate the minimum increment for matched-amount using greatest-common-divisor
BigInteger returnAmount = multiplier; // 1 unit (* multiplier)
BigInteger matchedAmount = price.movePointRight(8).toBigInteger();
BigInteger returnAmount = Amounts.MULTIPLIER_BI; // 1 unit * multiplier
BigInteger matchedAmount = BigInteger.valueOf(price);
BigInteger gcd = returnAmount.gcd(matchedAmount);
returnAmount = returnAmount.divide(gcd);
@@ -100,20 +101,20 @@ public class Order {
// Calculate GCD in combination with divisibility
if (isAmountAssetDivisible)
returnAmount = returnAmount.multiply(multiplier);
returnAmount = returnAmount.multiply(Amounts.MULTIPLIER_BI);
if (isReturnAssetDivisible)
matchedAmount = matchedAmount.multiply(multiplier);
matchedAmount = matchedAmount.multiply(Amounts.MULTIPLIER_BI);
gcd = returnAmount.gcd(matchedAmount);
// Calculate the granularity at which we have to buy
BigDecimal granularity = new BigDecimal(returnAmount.divide(gcd));
BigInteger granularity = returnAmount.multiply(Amounts.MULTIPLIER_BI).divide(gcd);
if (isAmountAssetDivisible)
granularity = granularity.movePointLeft(8);
granularity = granularity.divide(Amounts.MULTIPLIER_BI);
// Return
return granularity;
return granularity.longValue();
}
/**
@@ -130,7 +131,7 @@ public class Order {
/** Calculate price pair. (e.g. QORT/GOLD)
* <p>
* Under 'new' pricing scheme, lowest-assetID asset is first,
* Lowest-assetID asset is first,
* so if QORT has assetID 0 and GOLD has assetID 10, then
* the pricing pair is QORT/GOLD.
* <p>
@@ -141,32 +142,32 @@ public class Order {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
if (isOurOrderNewPricing && haveAssetId > wantAssetId)
if (haveAssetId > wantAssetId)
cachedPricePair = wantAssetData.getName() + "/" + haveAssetData.getName();
else
cachedPricePair = haveAssetData.getName() + "/" + wantAssetData.getName();
}
/** Returns amount of have-asset to remove from order's creator's balance on placing this order. */
private BigDecimal calcHaveAssetCommittment() {
BigDecimal committedCost = this.orderData.getAmount();
private long calcHaveAssetCommittment() {
// Simple case: amount is in have asset
if (!this.isAmountInWantAsset)
return this.orderData.getAmount();
// If 'new' pricing and "amount" is in want-asset then we need to convert
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
committedCost = committedCost.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
return Amounts.roundUpScaledMultiply(this.orderAmount, this.orderPrice);
}
return committedCost;
private long calcHaveAssetRefund(long amount) {
// Simple case: amount is in have asset
if (!this.isAmountInWantAsset)
return amount;
return Amounts.roundUpScaledMultiply(BigInteger.valueOf(amount), this.orderPrice);
}
/** Returns amount of remaining have-asset to refund to order's creator's balance on cancelling this order. */
private BigDecimal calcHaveAssetRefund() {
BigDecimal refund = getAmountLeft();
// If 'new' pricing and "amount" is in want-asset then we need to convert
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
refund = refund.multiply(this.orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
return refund;
private long calcHaveAssetRefund() {
return calcHaveAssetRefund(getAmountLeft());
}
// Navigation
@@ -192,27 +193,19 @@ public class Order {
/**
* Returns AssetData for asset in effect for "amount" field.
* <p>
* For 'old' pricing, this is the have-asset.<br>
* For 'new' pricing, this is the asset with highest assetID.
* This is the asset with highest assetID.
*/
public AssetData getAmountAsset() throws DataException {
if (isOurOrderNewPricing && wantAssetId > haveAssetId)
return getWantAsset();
else
return getHaveAsset();
return (wantAssetId > haveAssetId) ? getWantAsset() : getHaveAsset();
}
/**
* Returns AssetData for other (return) asset traded.
* <p>
* For 'old' pricing, this is the want-asset.<br>
* For 'new' pricing, this is the asset with lowest assetID.
* This is the asset with lowest assetID.
*/
public AssetData getReturnAsset() throws DataException {
if (isOurOrderNewPricing && haveAssetId < wantAssetId)
return getHaveAsset();
else
return getWantAsset();
return (haveAssetId < wantAssetId) ? getHaveAsset() : getWantAsset();
}
// Processing
@@ -227,8 +220,6 @@ public class Order {
// NOTE: the following values are specific to passed orderData, not the same as class instance values!
final boolean isOrderNewAssetPricing = orderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
// Cached for readability
final long _haveAssetId = orderData.getHaveAssetId();
final long _wantAssetId = orderData.getWantAssetId();
@@ -236,43 +227,36 @@ public class Order {
final AssetData haveAssetData = this.repository.getAssetRepository().fromAssetId(_haveAssetId);
final AssetData wantAssetData = this.repository.getAssetRepository().fromAssetId(_wantAssetId);
final long amountAssetId = (isOurOrderNewPricing && _wantAssetId > _haveAssetId) ? _wantAssetId : _haveAssetId;
final long returnAssetId = (isOurOrderNewPricing && _haveAssetId < _wantAssetId) ? _haveAssetId : _wantAssetId;
final long amountAssetId = (_wantAssetId > _haveAssetId) ? _wantAssetId : _haveAssetId;
final long returnAssetId = (_haveAssetId < _wantAssetId) ? _haveAssetId : _wantAssetId;
final AssetData amountAssetData = this.repository.getAssetRepository().fromAssetId(amountAssetId);
final AssetData returnAssetData = this.repository.getAssetRepository().fromAssetId(returnAssetId);
LOGGER.debug(String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
LOGGER.debug(() -> String.format("%s %s", orderPrefix, Base58.encode(orderData.getOrderId())));
LOGGER.trace(String.format("%s have %s, want %s. '%s' pricing scheme.", weThey, haveAssetData.getName(), wantAssetData.getName(), isOrderNewAssetPricing ? "new" : "old"));
LOGGER.trace(() -> String.format("%s have %s, want %s.", weThey, haveAssetData.getName(), wantAssetData.getName()));
LOGGER.trace(String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
orderData.getAmount().stripTrailingZeros().toPlainString(),
orderData.getFulfilled().stripTrailingZeros().toPlainString(),
Order.getAmountLeft(orderData).stripTrailingZeros().toPlainString(),
LOGGER.trace(() -> String.format("%s amount: %s (ordered) - %s (fulfilled) = %s %s left", ourTheir,
prettyAmount(orderData.getAmount()),
prettyAmount(orderData.getFulfilled()),
prettyAmount(Order.getAmountLeft(orderData)),
amountAssetData.getName()));
BigDecimal maxReturnAmount = Order.getAmountLeft(orderData).multiply(orderData.getPrice()).setScale(8, RoundingMode.HALF_UP);
long maxReturnAmount = Amounts.roundUpScaledMultiply(Order.getAmountLeft(orderData), orderData.getPrice());
String pricePair = getPricePair();
LOGGER.trace(String.format("%s price: %s %s (%s %s tradable)", ourTheir,
orderData.getPrice().toPlainString(), getPricePair(),
maxReturnAmount.stripTrailingZeros().toPlainString(), returnAssetData.getName()));
LOGGER.trace(() -> String.format("%s price: %s %s (%s %s tradable)", ourTheir,
prettyAmount(orderData.getPrice()),
pricePair,
prettyAmount(maxReturnAmount),
returnAssetData.getName()));
}
public void process() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
/** The asset while working out amount that matches. */
AssetData matchingAssetData = isOurOrderNewPricing ? getAmountAsset() : wantAssetData;
/** The return asset traded if trade completes. */
AssetData returnAssetData = isOurOrderNewPricing ? getReturnAsset() : haveAssetData;
// Subtract have-asset from creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(this.calcHaveAssetCommittment()));
creator.modifyAssetBalance(haveAssetId, - this.calcHaveAssetCommittment());
// Save this order into repository so it's available for matching, possibly by itself
this.repository.getAssetRepository().save(this.orderData);
@@ -281,36 +265,28 @@ public class Order {
// Fetch corresponding open orders that might potentially match, hence reversed want/have assetIDs.
// Returned orders are sorted with lowest "price" first.
List<OrderData> orders = assetRepository.getOpenOrdersForTrading(wantAssetId, haveAssetId, isOurOrderNewPricing ? this.orderData.getPrice() : null);
LOGGER.trace("Open orders fetched from repository: " + orders.size());
List<OrderData> orders = this.repository.getAssetRepository().getOpenOrdersForTrading(wantAssetId, haveAssetId, this.orderData.getPrice());
LOGGER.trace(() -> String.format("Open orders fetched from repository: %d", orders.size()));
if (orders.isEmpty())
return;
matchOrders(orders);
}
private void matchOrders(List<OrderData> orders) throws DataException {
AssetData haveAssetData = getHaveAsset();
AssetData wantAssetData = getWantAsset();
/** The asset while working out amount that matches. */
AssetData matchingAssetData = getAmountAsset();
/** The return asset traded if trade completes. */
AssetData returnAssetData = getReturnAsset();
// Attempt to match orders
/*
* Potential matching order example ("old"):
*
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORT), amount=40 (GOLD), price=486 (QORT/GOLD)
* This translates to "we have 40 GOLD and want QORT at a price of 486 QORT per GOLD"
* If our order matched, we'd end up with 40 * 486 = 19,440 QORT.
*
* Their order:
* haveAssetId=0 (QORT), wantAssetId=[GOLD], amount=20,000 (QORT), price=0.00205761 (GOLD/QORT)
* This translates to "they have 20,000 QORT and want GOLD at a price of 0.00205761 GOLD per QORT"
*
* Their price, converted into 'our' units of QORT/GOLD, is: 1 / 0.00205761 = 486.00074844 QORT/GOLD.
* This is better than our requested 486 QORT/GOLD so this order matches.
*
* Using their price, we end up with 40 * 486.00074844 = 19440.02993760 QORT. They end up with 40 GOLD.
*
* If their order had 19,440 QORT left, only 19,440 * 0.00205761 = 39.99993840 GOLD would be traded.
*/
/*
* Potential matching order example ("new"):
* Potential matching order example:
*
* Our order:
* haveAssetId=[GOLD], wantAssetId=0 (QORT), amount=40 (GOLD), price=486 (QORT/GOLD)
@@ -328,129 +304,107 @@ public class Order {
* If their order only had 36 GOLD left, only 36 * 486.00074844 = 17496.02694384 QORT would be traded.
*/
BigDecimal ourPrice = this.orderData.getPrice();
long ourPrice = this.orderData.getPrice();
String pricePair = getPricePair();
for (OrderData theirOrderData : orders) {
logOrder("Considering order", false, theirOrderData);
// Not used:
// boolean isTheirOrderNewAssetPricing = theirOrderData.getTimestamp() >= BlockChain.getInstance().getNewAssetPricingTimestamp();
// Determine their order price
BigDecimal theirPrice;
if (isOurOrderNewPricing) {
// Pricing units are the same way round for both orders, so no conversion needed.
// Orders under 'old' pricing have been converted during repository update.
theirPrice = theirOrderData.getPrice();
LOGGER.trace(String.format("Their price: %s %s", theirPrice.toPlainString(), getPricePair()));
} else {
// If our order is 'old' pricing then all other existing orders must be 'old' pricing too
// Their order pricing will be inverted, so convert
theirPrice = BigDecimal.ONE.setScale(8).divide(theirOrderData.getPrice(), RoundingMode.DOWN);
LOGGER.trace(String.format("Their price: %s %s per %s", theirPrice.toPlainString(), wantAssetData.getName(), haveAssetData.getName()));
}
long theirPrice = theirOrderData.getPrice();
LOGGER.trace(() -> String.format("Their price: %s %s", prettyAmount(theirPrice), pricePair));
// If their price is worse than what we're willing to accept then we're done as prices only get worse as we iterate through list of orders
if (isOurOrderNewPricing) {
if (haveAssetId < wantAssetId && theirPrice.compareTo(ourPrice) > 0)
break;
if (haveAssetId > wantAssetId && theirPrice.compareTo(ourPrice) < 0)
break;
} else {
// 'old' pricing scheme
if (theirPrice.compareTo(ourPrice) < 0)
break;
}
if ((haveAssetId < wantAssetId && theirPrice > ourPrice) || (haveAssetId > wantAssetId && theirPrice < ourPrice))
break;
// Calculate how much we could buy at their price.
BigDecimal ourMaxAmount;
if (isOurOrderNewPricing)
// In 'new' pricing scheme, "amount" is expressed in terms of asset with highest assetID
ourMaxAmount = this.getAmountLeft();
else
// In 'old' pricing scheme, "amount" is expressed in terms of our want-asset.
ourMaxAmount = this.getAmountLeft().multiply(theirPrice).setScale(8, RoundingMode.DOWN);
LOGGER.trace("ourMaxAmount (max we could trade at their price): " + ourMaxAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
// Calculate how much we could buy at their price, "amount" is expressed in terms of asset with highest assetID.
long ourMaxAmount = this.getAmountLeft();
LOGGER.trace(() -> String.format("ourMaxAmount (max we could trade at their price): %s %s", prettyAmount(ourMaxAmount), matchingAssetData.getName()));
// How much is remaining available in their order.
BigDecimal theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace("theirAmountLeft (max amount remaining in their order): " + theirAmountLeft.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
long theirAmountLeft = Order.getAmountLeft(theirOrderData);
LOGGER.trace(() -> String.format("theirAmountLeft (max amount remaining in their order): %s %s", prettyAmount(theirAmountLeft), matchingAssetData.getName()));
// So matchable want-asset amount is the minimum of above two values
BigDecimal matchedAmount = ourMaxAmount.min(theirAmountLeft);
LOGGER.trace("matchedAmount: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
long interimMatchedAmount = Math.min(ourMaxAmount, theirAmountLeft);
LOGGER.trace(() -> String.format("matchedAmount: %s %s", prettyAmount(interimMatchedAmount), matchingAssetData.getName()));
// If we can't buy anything then try another order
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
if (interimMatchedAmount <= 0)
continue;
// Calculate amount granularity, based on price and both assets' divisibility, so that return-amount traded is a valid value (integer or to 8 d.p.)
BigDecimal granularity = calculateAmountGranularity(matchingAssetData.getIsDivisible(), returnAssetData.getIsDivisible(), theirOrderData.getPrice());
LOGGER.trace("granularity (amount granularity): " + granularity.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
long granularity = calculateAmountGranularity(matchingAssetData.isDivisible(), returnAssetData.isDivisible(), theirOrderData.getPrice());
LOGGER.trace(() -> String.format("granularity (amount granularity): %s %s", prettyAmount(granularity), matchingAssetData.getName()));
// Reduce matched amount (if need be) to fit granularity
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(granularity));
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
long matchedAmount = interimMatchedAmount - interimMatchedAmount % granularity;
LOGGER.trace(() -> String.format("matchedAmount adjusted for granularity: %s %s", prettyAmount(matchedAmount), matchingAssetData.getName()));
// If we can't buy anything then try another order
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
if (matchedAmount <= 0)
continue;
// Safety check
if (!matchingAssetData.getIsDivisible() && matchedAmount.stripTrailingZeros().scale() > 0) {
Account participant = new PublicKeyAccount(this.repository, theirOrderData.getCreatorPublicKey());
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
matchedAmount.toPlainString(), matchingAssetData.getAssetId(), participant.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
checkDivisibility(matchingAssetData, matchedAmount, theirOrderData);
// Trade can go ahead!
// Calculate the total cost to us, in return-asset, based on their price
BigDecimal returnAmountTraded = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8, RoundingMode.DOWN);
LOGGER.trace("returnAmountTraded: " + returnAmountTraded.stripTrailingZeros().toPlainString() + " " + returnAssetData.getName());
long returnAmountTraded = Amounts.roundDownScaledMultiply(matchedAmount, theirOrderData.getPrice());
LOGGER.trace(() -> String.format("returnAmountTraded: %s %s", prettyAmount(returnAmountTraded), returnAssetData.getName()));
// Safety check
if (!returnAssetData.getIsDivisible() && returnAmountTraded.stripTrailingZeros().scale() > 0) {
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for %s",
returnAmountTraded.toPlainString(), returnAssetData.getAssetId(), creator.getAddress());
LOGGER.error(message);
throw new DataException(message);
}
checkDivisibility(returnAssetData, returnAmountTraded, this.orderData);
BigDecimal tradedWantAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? returnAmountTraded : matchedAmount;
BigDecimal tradedHaveAmount = (isOurOrderNewPricing && haveAssetId > wantAssetId) ? matchedAmount : returnAmountTraded;
long tradedWantAmount = this.isAmountInWantAsset ? matchedAmount : returnAmountTraded;
long tradedHaveAmount = this.isAmountInWantAsset ? returnAmountTraded : matchedAmount;
// We also need to know how much have-asset to refund based on price improvement ('new' pricing only and only one direction applies)
BigDecimal haveAssetRefund = isOurOrderNewPricing && haveAssetId < wantAssetId ? ourPrice.subtract(theirPrice).abs().multiply(matchedAmount).setScale(8, RoundingMode.DOWN) : BigDecimal.ZERO;
// We also need to know how much have-asset to refund based on price improvement (only one direction applies)
long haveAssetRefund = this.isAmountInWantAsset ? Amounts.roundDownScaledMultiply(matchedAmount, Math.abs(ourPrice - theirPrice)) : 0;
LOGGER.trace(String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
tradedHaveAmount.toPlainString(), haveAssetData.getName(),
tradedWantAmount.toPlainString(), wantAssetData.getName(),
haveAssetRefund.toPlainString(), haveAssetData.getName()));
LOGGER.trace(() -> String.format("We traded %s %s (have-asset) for %s %s (want-asset), saving %s %s (have-asset)",
prettyAmount(tradedHaveAmount), haveAssetData.getName(),
prettyAmount(tradedWantAmount), wantAssetData.getName(),
prettyAmount(haveAssetRefund), haveAssetData.getName()));
// Construct trade
TradeData tradeData = new TradeData(this.orderData.getOrderId(), theirOrderData.getOrderId(),
tradedWantAmount, tradedHaveAmount, haveAssetRefund, this.orderData.getTimestamp());
// Process trade, updating corresponding orders in repository
Trade trade = new Trade(this.repository, tradeData);
trade.process();
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
BigDecimal amountFulfilled = isOurOrderNewPricing ? matchedAmount : returnAmountTraded;
this.orderData.setFulfilled(this.orderData.getFulfilled().add(amountFulfilled));
LOGGER.trace("Updated our order's fulfilled amount to: " + this.orderData.getFulfilled().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
LOGGER.trace("Our order's amount remaining: " + this.getAmountLeft().stripTrailingZeros().toPlainString() + " " + matchingAssetData.getName());
long amountFulfilled = matchedAmount;
this.orderData.setFulfilled(this.orderData.getFulfilled() + amountFulfilled);
LOGGER.trace(() -> String.format("Updated our order's fulfilled amount to: %s %s", prettyAmount(this.orderData.getFulfilled()), matchingAssetData.getName()));
LOGGER.trace(() -> String.format("Our order's amount remaining: %s %s", prettyAmount(this.getAmountLeft()), matchingAssetData.getName()));
// Continue on to process other open orders if we still have amount left to match
if (this.getAmountLeft().compareTo(BigDecimal.ZERO) <= 0)
if (this.getAmountLeft() <= 0)
break;
}
}
/**
* Check amount has no fractional part if asset is indivisible.
*
* @throws DataException if divisibility check fails
*/
private void checkDivisibility(AssetData assetData, long amount, OrderData orderData) throws DataException {
if (assetData.isDivisible() || amount % Amounts.MULTIPLIER == 0)
// Asset is divisible or amount has no fractional part
return;
String message = String.format("Refusing to trade fractional %s [indivisible assetID %d] for order %s",
prettyAmount(amount), assetData.getAssetId(), Base58.encode(orderData.getOrderId()));
LOGGER.error(message);
throw new DataException(message);
}
public void orphan() throws DataException {
// Orphan trades that occurred as a result of this order
for (TradeData tradeData : getTrades())
@@ -464,7 +418,7 @@ public class Order {
// Return asset to creator
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(this.calcHaveAssetCommittment()));
creator.modifyAssetBalance(haveAssetId, this.calcHaveAssetCommittment());
}
// This is called by CancelOrderTransaction so that an Order can no longer trade
@@ -474,14 +428,14 @@ public class Order {
// Update creator's balance with unfulfilled amount
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).add(calcHaveAssetRefund()));
creator.modifyAssetBalance(haveAssetId, calcHaveAssetRefund());
}
// Opposite of cancel() above for use during orphaning
public void reopen() throws DataException {
// Update creator's balance with unfulfilled amount
Account creator = new PublicKeyAccount(this.repository, this.orderData.getCreatorPublicKey());
creator.setConfirmedBalance(haveAssetId, creator.getConfirmedBalance(haveAssetId).subtract(calcHaveAssetRefund()));
creator.modifyAssetBalance(haveAssetId, - calcHaveAssetRefund());
this.orderData.setIsClosed(false);
this.repository.getAssetRepository().save(this.orderData);

View File

@@ -1,10 +1,7 @@
package org.qortal.asset;
import java.math.BigDecimal;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.BlockChain;
import org.qortal.data.asset.OrderData;
import org.qortal.data.asset.TradeData;
import org.qortal.repository.AssetRepository;
@@ -17,12 +14,11 @@ public class Trade {
private Repository repository;
private TradeData tradeData;
private boolean isNewPricing;
private AssetRepository assetRepository;
private OrderData initiatingOrder;
private OrderData targetOrder;
private BigDecimal newPricingFulfilled;
private long fulfilled;
// Constructors
@@ -30,7 +26,6 @@ public class Trade {
this.repository = repository;
this.tradeData = tradeData;
this.isNewPricing = this.tradeData.getTimestamp() > BlockChain.getInstance().getNewAssetPricingTimestamp();
this.assetRepository = this.repository.getAssetRepository();
}
@@ -43,9 +38,9 @@ public class Trade {
// Note: targetAmount is amount traded FROM target order
// Note: initiatorAmount is amount traded FROM initiating order
// Under 'new' pricing scheme, "amount" and "fulfilled" are the same asset for both orders
// "amount" and "fulfilled" are the same asset for both orders
// which is the matchedAmount in asset with highest assetID
this.newPricingFulfilled = (initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId()) ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
this.fulfilled = initiatingOrder.getHaveAssetId() < initiatingOrder.getWantAssetId() ? this.tradeData.getTargetAmount() : this.tradeData.getInitiatorAmount();
}
public void process() throws DataException {
@@ -55,16 +50,16 @@ public class Trade {
// Note: targetAmount is amount traded FROM target order
// Note: initiatorAmount is amount traded FROM initiating order
// Update corresponding Orders on both sides of trade
commonPrep();
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().add(isNewPricing ? newPricingFulfilled : tradeData.getInitiatorAmount()));
// Update corresponding Orders on both sides of trade
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled() + fulfilled);
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to true if isFulfilled now true
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
targetOrder.setFulfilled(targetOrder.getFulfilled().add(isNewPricing ? newPricingFulfilled : tradeData.getTargetAmount()));
targetOrder.setFulfilled(targetOrder.getFulfilled() + fulfilled);
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to true if isFulfilled now true
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@@ -72,33 +67,31 @@ public class Trade {
// Actually transfer asset balances
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).add(tradeData.getTargetAmount()));
initiatingCreator.modifyAssetBalance(initiatingOrder.getWantAssetId(), tradeData.getTargetAmount());
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).add(tradeData.getInitiatorAmount()));
targetCreator.modifyAssetBalance(targetOrder.getWantAssetId(), tradeData.getInitiatorAmount());
// Possible partial saving to refund to initiator
BigDecimal initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving.compareTo(BigDecimal.ZERO) > 0)
initiatingCreator.setConfirmedBalance(initiatingOrder.getHaveAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getHaveAssetId()).add(initiatorSaving));
long initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving > 0)
initiatingCreator.modifyAssetBalance(initiatingOrder.getHaveAssetId(), initiatorSaving);
}
public void orphan() throws DataException {
AssetRepository assetRepository = this.repository.getAssetRepository();
// Note: targetAmount is amount traded FROM target order
// Note: initiatorAmount is amount traded FROM initiating order
// Revert corresponding Orders on both sides of trade
commonPrep();
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled().subtract(isNewPricing ? newPricingFulfilled : tradeData.getInitiatorAmount()));
// Revert corresponding Orders on both sides of trade
initiatingOrder.setFulfilled(initiatingOrder.getFulfilled() - fulfilled);
initiatingOrder.setIsFulfilled(Order.isFulfilled(initiatingOrder));
// Set isClosed to false if isFulfilled now false
initiatingOrder.setIsClosed(initiatingOrder.getIsFulfilled());
assetRepository.save(initiatingOrder);
targetOrder.setFulfilled(targetOrder.getFulfilled().subtract(isNewPricing ? newPricingFulfilled : tradeData.getTargetAmount()));
targetOrder.setFulfilled(targetOrder.getFulfilled() - fulfilled);
targetOrder.setIsFulfilled(Order.isFulfilled(targetOrder));
// Set isClosed to false if isFulfilled now false
targetOrder.setIsClosed(targetOrder.getIsFulfilled());
@@ -106,15 +99,15 @@ public class Trade {
// Reverse asset transfers
Account initiatingCreator = new PublicKeyAccount(this.repository, initiatingOrder.getCreatorPublicKey());
initiatingCreator.setConfirmedBalance(initiatingOrder.getWantAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getWantAssetId()).subtract(tradeData.getTargetAmount()));
initiatingCreator.modifyAssetBalance(initiatingOrder.getWantAssetId(), - tradeData.getTargetAmount());
Account targetCreator = new PublicKeyAccount(this.repository, targetOrder.getCreatorPublicKey());
targetCreator.setConfirmedBalance(targetOrder.getWantAssetId(), targetCreator.getConfirmedBalance(targetOrder.getWantAssetId()).subtract(tradeData.getInitiatorAmount()));
targetCreator.modifyAssetBalance(targetOrder.getWantAssetId(), - tradeData.getInitiatorAmount());
// Possible partial saving to claw back from initiator
BigDecimal initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving.compareTo(BigDecimal.ZERO) > 0)
initiatingCreator.setConfirmedBalance(initiatingOrder.getHaveAssetId(), initiatingCreator.getConfirmedBalance(initiatingOrder.getHaveAssetId()).subtract(initiatorSaving));
long initiatorSaving = this.tradeData.getInitiatorSaving();
if (initiatorSaving > 0)
initiatingCreator.modifyAssetBalance(initiatingOrder.getHaveAssetId(), - initiatorSaving);
// Remove trade from repository
assetRepository.delete(tradeData);

View File

@@ -1,11 +1,9 @@
package org.qortal.at;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.List;
import org.ciyam.at.MachineState;
import org.qortal.asset.Asset;
import org.ciyam.at.Timestamp;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
@@ -42,66 +40,27 @@ public class AT {
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey();
long creation = deployATTransactionData.getTimestamp();
byte[] creationBytes = deployATTransactionData.getCreationBytes();
long assetId = deployATTransactionData.getAssetId();
short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
// Just enough AT data to allow API to query initial balances, etc.
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
machineState.getIsFrozen(), machineState.getFrozenBalance());
long blockTimestamp = Timestamp.toLong(height, 0);
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
} else {
// Legacy v1 AT
// We would deploy these in 'dead' state as they will never be run on Qortal
// but this breaks import from Qora1 so something else will have to mark them dead at hard-fork
byte[] codeHash = Crypto.digest(machineState.getCodeBytes());
// Extract code bytes length
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance());
// v1 AT header is: version, reserved, code-pages, data-pages, call-stack-pages, user-stack-pages (all shorts)
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
// Number of code pages
short numCodePages = byteBuffer.get(2 + 2);
// Skip header and also "minimum activation amount" (long)
byteBuffer.position(6 * 2 + 8);
int codeLen = 0;
// Extract actual code length, stored in minimal-size form (byte, short or int)
if (numCodePages * 256 < 257) {
codeLen = byteBuffer.get() & 0xff;
} else if (numCodePages * 256 < Short.MAX_VALUE + 1) {
codeLen = byteBuffer.getShort() & 0xffff;
} else if (numCodePages * 256 <= Integer.MAX_VALUE) {
codeLen = byteBuffer.getInt();
}
// Extract code bytes
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
// Create AT
boolean isSleeping = false;
Integer sleepUntilHeight = null;
boolean isFinished = false;
boolean hadFatalError = false;
boolean isFrozen = false;
Long frozenBalance = null;
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, isSleeping, sleepUntilHeight, isFinished,
hadFatalError, isFrozen, frozenBalance);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
}
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true);
}
// Getters / setters
@@ -116,9 +75,7 @@ public class AT {
ATRepository atRepository = this.repository.getATRepository();
atRepository.save(this.atData);
// For version 2+ we also store initial AT state data
if (this.atData.getVersion() >= 2)
atRepository.save(this.atStateData);
atRepository.save(this.atStateData);
}
public void undeploy() throws DataException {
@@ -126,34 +83,89 @@ public class AT {
this.repository.getATRepository().delete(this.atData.getATAddress());
}
public List<AtTransaction> run(long blockTimestamp) throws DataException {
public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalATLogger logger = new QortalATLogger();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
// Fetch latest ATStateData for this AT (if any)
// Fetch latest ATStateData for this AT
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
// There should be at least initial AT state data
// There should be at least initial deployment AT state data
if (latestAtStateData == null)
throw new IllegalStateException("No initial AT state data found");
throw new IllegalStateException("No previous AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable
MachineState state = MachineState.fromBytes(api, logger, latestAtStateData.getStateData(), codeBytes);
state.execute();
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
try {
state.execute();
} catch (Exception e) {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
}
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
long creation = this.atData.getCreation();
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
BigDecimal atFees = api.calcFinalFees(state);
long atFees = api.calcFinalFees(state);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees);
this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
return api.getTransactions();
}
public void update(int blockHeight, long blockTimestamp) throws DataException {
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, this.atStateData.getStateData(), codeBytes);
// Save latest AT state data
this.repository.getATRepository().save(this.atStateData);
// Update AT info in repository too
this.atData.setIsSleeping(state.isSleeping());
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
this.atData.setIsFinished(state.isFinished());
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
this.repository.getATRepository().save(this.atData);
}
public void revert(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
// Delete old AT state data from repository
this.repository.getATRepository().delete(atAddress, blockHeight);
if (this.atStateData.isInitial())
return;
// Load previous state data
ATStateData previousStateData = this.repository.getATRepository().getLatestATState(atAddress);
if (previousStateData == null)
throw new DataException("Can't find previous AT state data for " + atAddress);
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, previousStateData.getStateData(), codeBytes);
// Update AT info in repository
this.atData.setIsSleeping(state.isSleeping());
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
this.atData.setIsFinished(state.isFinished());
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
this.repository.getATRepository().save(this.atData);
}
}

View File

@@ -1,134 +0,0 @@
package org.qortal.at;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.block.Block;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.DataException;
import org.qortal.transaction.Transaction;
public enum BlockchainAPI {
QORTAL(0) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
QortalATAPI api = (QortalATAPI) state.getAPI();
BlockRepository blockRepository = api.repository.getBlockRepository();
try {
Account recipientAccount = new Account(api.repository, recipient);
while (height <= blockRepository.getBlockchainHeight()) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(api.repository, blockData);
List<Transaction> transactions = block.getTransactions();
// No more transactions in this block? Try next block
if (sequence >= transactions.size()) {
++height;
sequence = 0;
continue;
}
Transaction transaction = transactions.get(sequence);
// Transaction needs to be sent to specified recipient
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
// Found a transaction
api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
byte[] hash = QortalATAPI.sha192(transaction.getTransactionData().getSignature());
api.setA2(state, QortalATAPI.fromBytes(hash, 0));
api.setA3(state, QortalATAPI.fromBytes(hash, 8));
api.setA4(state, QortalATAPI.fromBytes(hash, 16));
return;
}
// Transaction wasn't for us - keep going
++sequence;
}
// No more transactions - zero A and exit
api.zeroA(state);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
QortalATAPI api = (QortalATAPI) state.getAPI();
TransactionData transactionData = api.fetchTransaction(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
case AT:
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount.unscaledValue().longValue();
else
return 0xffffffffffffffffL;
default:
return 0xffffffffffffffffL;
}
}
},
BTC(1) {
@Override
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
// TODO BTC transaction support for ATv2
}
@Override
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
// TODO BTC transaction support for ATv2
return 0;
}
};
public final int value;
private static final Map<Integer, BlockchainAPI> map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type));
BlockchainAPI(int value) {
this.value = value;
}
public static BlockchainAPI valueOf(int value) {
return map.get(value);
}
// Blockchain-specific API methods
public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state);
public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state);
}

View File

@@ -1,11 +1,11 @@
package org.qortal.at;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.API;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
@@ -14,35 +14,41 @@ import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.account.GenesisAccount;
import org.qortal.account.NullAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.CiyamAtSettings;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.data.transaction.ATTransactionData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.PaymentTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import com.google.common.primitives.Bytes;
public class QortalATAPI extends API {
// Useful constants
private static final BigDecimal FEE_PER_STEP = BigDecimal.valueOf(1.0).setScale(8); // 1 QORT per "step"
private static final int MAX_STEPS_PER_ROUND = 500;
private static final int STEPS_PER_FUNCTION_CALL = 10;
private static final int MINUTES_PER_BLOCK = 10;
private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH];
private static final Logger LOGGER = LogManager.getLogger(QortalATAPI.class);
// Properties
Repository repository;
ATData atData;
long blockTimestamp;
private Repository repository;
private ATData atData;
private long blockTimestamp;
private final CiyamAtSettings ciyamAtSettings;
/** List of generated AT transactions */
List<AtTransaction> transactions;
@@ -54,36 +60,42 @@ public class QortalATAPI extends API {
this.atData = atData;
this.transactions = new ArrayList<>();
this.blockTimestamp = blockTimestamp;
this.ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
}
// Methods specific to Qortal AT processing, not inherited
public Repository getRepository() {
return this.repository;
}
public List<AtTransaction> getTransactions() {
return this.transactions;
}
public BigDecimal calcFinalFees(MachineState state) {
return FEE_PER_STEP.multiply(BigDecimal.valueOf(state.getSteps()));
public long calcFinalFees(MachineState state) {
return state.getSteps() * this.ciyamAtSettings.feePerStep;
}
// Inherited methods from CIYAM AT API
@Override
public int getMaxStepsPerRound() {
return MAX_STEPS_PER_ROUND;
return this.ciyamAtSettings.maxStepsPerRound;
}
@Override
public int getOpCodeSteps(OpCode opcode) {
if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value)
return STEPS_PER_FUNCTION_CALL;
return this.ciyamAtSettings.stepsPerFunctionCall;
return 1;
}
@Override
public long getFeePerStep() {
return FEE_PER_STEP.unscaledValue().longValue();
return this.ciyamAtSettings.feePerStep;
}
@Override
@@ -105,31 +117,63 @@ public class QortalATAPI extends API {
}
@Override
public void putPreviousBlockHashInA(MachineState state) {
public void putPreviousBlockHashIntoA(MachineState state) {
try {
BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight());
int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1;
// We only need signature, so only request a block summary
List<BlockSummaryData> blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight);
if (blockSummaries == null || blockSummaries.size() != 1)
throw new RuntimeException("AT API unable to fetch previous block hash?");
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
byte[] blockHash = Crypto.digest(blockData.getSignature());
// To be able to use hash to look up block, save height (8 bytes) and partial signature (24 bytes)
this.setA1(state, previousBlockHeight);
this.setA(state, blockHash);
byte[] signature = blockSummaries.get(0).getSignature();
// Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature.
this.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
}
@Override
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) {
// Recipient is this AT
String recipient = this.atData.getATAddress();
String atAddress = this.atData.getATAddress();
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
ATRepository.NextTransactionInfo nextTransactionInfo;
try {
nextTransactionInfo = this.getRepository().getATRepository().findNextTransaction(atAddress, height, sequence);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch next transaction?", e);
}
if (nextTransactionInfo == null) {
// No more transactions for AT at this time - zero A and exit
this.zeroA(state);
return;
}
// Found a transaction
this.setA1(state, new Timestamp(nextTransactionInfo.height, timestamp.blockchainId, nextTransactionInfo.sequence).longValue());
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
this.setA2(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 8));
this.setA3(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 16));
this.setA4(state, BitTwiddling.longFromBEBytes(nextTransactionInfo.signature, 24));
}
@Override
public long getTypeFromTransactionInA(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
switch (transactionData.getType()) {
case PAYMENT:
@@ -151,22 +195,36 @@ public class QortalATAPI extends API {
@Override
public long getAmountFromTransactionInA(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
TransactionData transactionData = this.getTransactionFromA(state);
switch (transactionData.getType()) {
case PAYMENT:
return ((PaymentTransactionData) transactionData).getAmount();
case AT:
Long amount = ((ATTransactionData) transactionData).getAmount();
if (amount != null)
return amount;
// fall-through to default
default:
return 0xffffffffffffffffL;
}
}
@Override
public long getTimestampFromTransactionInA(MachineState state) {
// Transaction's "timestamp" already stored in A1
Timestamp timestamp = new Timestamp(state.getA1());
Timestamp timestamp = new Timestamp(this.getA1(state));
return timestamp.longValue();
}
@Override
public long generateRandomUsingTransactionInA(MachineState state) {
// The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic,
// value.
// The plan here is to sleep for a block then use next block's signature
// and this transaction's signature to generate pseudo-random, but deterministic, value.
if (!isFirstOpCodeAfterSleeping(state)) {
// First call
@@ -179,7 +237,7 @@ public class QortalATAPI extends API {
// Second call
// HASH(A and new block hash)
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
try {
BlockData blockData = this.repository.getBlockRepository().getLastBlock();
@@ -191,7 +249,7 @@ public class QortalATAPI extends API {
byte[] hash = Crypto.digest(input);
return fromBytes(hash, 0);
return BitTwiddling.longFromBEBytes(hash, 0);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch latest block from repository?", e);
}
@@ -203,50 +261,50 @@ public class QortalATAPI extends API {
// Zero B in case of issues or shorter-than-B message
this.zeroB(state);
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
byte[] messageData = null;
switch (transactionData.getType()) {
case MESSAGE:
messageData = ((MessageTransactionData) transactionData).getData();
break;
case AT:
messageData = ((ATTransactionData) transactionData).getMessage();
break;
default:
return;
}
// Check data length is appropriate, i.e. not larger than B
if (messageData.length > 4 * 8)
return;
byte[] messageData = this.getMessageFromTransaction(transactionData);
// Pad messageData to fit B
byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
if (messageData.length < 4 * 8)
messageData = Bytes.ensureCapacity(messageData, 4 * 8, 0);
// Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally
this.setB(state, paddedMessageData);
this.setB(state, messageData);
}
@Override
public void putAddressFromTransactionInAIntoB(MachineState state) {
TransactionData transactionData = this.fetchTransaction(state);
TransactionData transactionData = this.getTransactionFromA(state);
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = transactionData.getCreatorPublicKey();
String address;
if (transactionData.getType() == TransactionType.AT) {
// Use AT address from transaction data, as transaction's public key will always be fake
address = ((ATTransactionData) transactionData).getATAddress();
} else {
byte[] publicKey = transactionData.getCreatorPublicKey();
address = Crypto.toAddress(publicKey);
}
this.setB(state, bytes);
// Convert to byte form as this only takes 25 bytes,
// compared to string-form's 34 bytes,
// and we only have 32 bytes available.
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
this.setB(state, addressBytes);
}
@Override
public void putCreatorAddressIntoB(MachineState state) {
// We actually use public key as it has more potential utility (e.g. message verification) than an address
byte[] bytes = atData.getCreatorPublicKey();
byte[] publicKey = atData.getCreatorPublicKey();
String address = Crypto.toAddress(publicKey);
this.setB(state, bytes);
// Convert to byte form as this only takes 25 bytes,
// compared to string-form's 34 bytes,
// and we only have 32 bytes available.
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
this.setB(state, addressBytes);
}
@Override
@@ -254,25 +312,22 @@ public class QortalATAPI extends API {
try {
Account atAccount = this.getATAccount();
return atAccount.getConfirmedBalance(Asset.QORT).unscaledValue().longValue();
return atAccount.getConfirmedBalance(Asset.QORT);
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch AT's current balance?", e);
}
}
@Override
public void payAmountToB(long unscaledAmount, MachineState state) {
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
public void payAmountToB(long amount, MachineState state) {
Account recipient = getAccountFromB(state);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
recipient.getAddress(), amount, this.atData.getAssetId(), new byte[0]);
recipient.getAddress(), amount, this.atData.getAssetId());
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
// Add to our transactions
@@ -281,17 +336,15 @@ public class QortalATAPI extends API {
@Override
public void messageAToB(MachineState state) {
byte[] message = state.getA();
byte[] publicKey = state.getB();
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
byte[] message = this.getA(state);
Account recipient = getAccountFromB(state);
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
recipient.getAddress(), BigDecimal.ZERO, this.atData.getAssetId(), message);
recipient.getAddress(), message);
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
// Add to our transactions
@@ -303,22 +356,24 @@ public class QortalATAPI extends API {
int blockHeight = timestamp.blockHeight;
// At least one block in the future
blockHeight += (minutes / MINUTES_PER_BLOCK) + 1;
blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1);
return new Timestamp(blockHeight, 0).longValue();
}
@Override
public void onFinished(long finalBalance, MachineState state) {
if (finalBalance <= 0)
return;
// Refund remaining balance (if any) to AT's creator
Account creator = this.getCreator();
long timestamp = this.getNextTransactionTimestamp();
byte[] reference = this.getLastReference();
BigDecimal amount = BigDecimal.valueOf(finalBalance, 8);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, GenesisAccount.PUBLIC_KEY, BigDecimal.ZERO, null);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, 0L, null);
ATTransactionData atTransactionData = new ATTransactionData(baseTransactionData, this.atData.getATAddress(),
creator.getAddress(), amount, this.atData.getAssetId(), new byte[0]);
creator.getAddress(), finalBalance, this.atData.getAssetId());
AtTransaction atTransaction = new AtTransaction(this.repository, atTransactionData);
// Add to our transactions
@@ -327,7 +382,7 @@ public class QortalATAPI extends API {
@Override
public void onFatalError(MachineState state, ExecutionException e) {
state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
LOGGER.error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
}
@Override
@@ -338,47 +393,38 @@ public class QortalATAPI extends API {
if (qortalFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qortalFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode);
qortalFunctionCode.preExecuteCheck(paramCount, returnValueExpected, rawFunctionCode);
}
@Override
public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
QortalFunctionCode qortalFunctionCode = QortalFunctionCode.valueOf(rawFunctionCode);
if (qortalFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qortalFunctionCode.execute(functionData, state, rawFunctionCode);
}
// Utility methods
/** Convert part of little-endian byte[] to long */
/* package */ static long fromBytes(byte[] bytes, int start) {
return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32);
}
/** Returns SHA2-192 digest of input - used to verify transaction signatures */
public static byte[] sha192(byte[] input) {
try {
// SHA2-192
MessageDigest sha192 = MessageDigest.getInstance("SHA-192");
return sha192.digest(input);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-192 not available");
}
}
/** Verify transaction's partial signature matches A2 thru A4 */
private void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare end of transaction's signature against A2 thru A4
byte[] sig = transactionData.getSignature();
/** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */
private static void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare SHA2-192 of transaction's signature against A2 thru A4
byte[] hash = sha192(transactionData.getSignature());
if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16))
if (this.getA2(state) != BitTwiddling.longFromBEBytes(sig, 8) || this.getA3(state) != BitTwiddling.longFromBEBytes(sig, 16) || this.getA4(state) != BitTwiddling.longFromBEBytes(sig, 24))
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
}
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
/* package */ TransactionData fetchTransaction(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
/* package */ TransactionData getTransactionFromA(MachineState state) {
Timestamp timestamp = new Timestamp(this.getA1(state));
try {
TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight,
@@ -396,6 +442,20 @@ public class QortalATAPI extends API {
}
}
/** Returns message data from transaction. */
/*package*/ byte[] getMessageFromTransaction(TransactionData transactionData) {
switch (transactionData.getType()) {
case MESSAGE:
return ((MessageTransactionData) transactionData).getData();
case AT:
return ((ATTransactionData) transactionData).getMessage();
default:
return null;
}
}
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());
@@ -409,29 +469,17 @@ public class QortalATAPI extends API {
/** Returns the timestamp to use for next AT Transaction */
private long getNextTransactionTimestamp() {
/*
* Timestamp is block's timestamp + position in AT-Transactions list.
* Use block's timestamp.
*
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
*
* As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without
* issue.
*
* As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine.
* This is OK because AT transactions are always generated locally and order is preserved in Transaction.getDataComparator().
*/
// XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
// return this.blockTimestamp + this.transactions.size();
throw new RuntimeException("AT timestamp code not fixed!");
return this.blockTimestamp;
}
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */
/** Returns AT account's lastReference */
private byte[] getLastReference() {
// Use signature from last AT Transaction we generated
if (!this.transactions.isEmpty())
return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature();
try {
// No transactions yet, so look up AT's account's last reference from repository
// Look up AT's account's last reference from repository
Account atAccount = this.getATAccount();
return atAccount.getLastReference();
@@ -440,4 +488,42 @@ public class QortalATAPI extends API {
}
}
/**
* Returns Account (possibly PublicKeyAccount) based on value in B.
* <p>
* If first byte in B starts with either address version bytes,<br>
* and bytes 26 to 32 are zero, then use as an address, but only if valid.
* <p>
* Otherwise, assume B is a public key.
*/
private Account getAccountFromB(MachineState state) {
byte[] bBytes = this.getB(state);
if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
&& Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) {
// Extract only the bytes containing address
byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH);
// If address (in byte form) is valid...
if (Crypto.isValidAddress(addressBytes))
// ...then return an Account using address (converted to Base58
return new Account(this.repository, Base58.encode(addressBytes));
}
return new PublicKeyAccount(this.repository, bBytes);
}
/* Convenience methods to allow QortalFunctionCode package-visibility access to A/B-get/set methods. */
protected byte[] getB(MachineState state) {
return super.getB(state);
}
protected void setB(MachineState state, byte[] bBytes) {
super.setB(state, bBytes);
}
protected void zeroB(MachineState state) {
super.zeroB(state);
}
}

View File

@@ -1,26 +0,0 @@
package org.qortal.at;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class QortalATLogger implements org.ciyam.at.LoggerInterface {
// NOTE: We're logging on behalf of org.qortal.at.AT, not ourselves!
private static final Logger LOGGER = LogManager.getLogger(AT.class);
@Override
public void error(String message) {
LOGGER.error(message);
}
@Override
public void debug(String message) {
LOGGER.debug(message);
}
@Override
public void echo(String message) {
LOGGER.info(message);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
package org.qortal.at;
import org.ciyam.at.AtLogger;
public class QortalAtLoggerFactory implements org.ciyam.at.AtLoggerFactory {
private static QortalAtLoggerFactory instance;
private QortalAtLoggerFactory() {
}
public static synchronized QortalAtLoggerFactory getInstance() {
if (instance == null)
instance = new QortalAtLoggerFactory();
return instance;
}
@Override
public AtLogger create(final Class<?> loggerName) {
return QortalAtLogger.create(loggerName);
}
}

View File

@@ -1,15 +1,19 @@
package org.qortal.at;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.crosschain.BTC;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
import org.qortal.settings.Settings;
/**
* Qortal-specific CIYAM-AT Functions.
@@ -19,28 +23,105 @@ import org.ciyam.at.Timestamp;
*/
public enum QortalFunctionCode {
/**
* <tt>0x0500</tt><br>
* Returns current BTC block's "timestamp"
* Returns length of message data from transaction in A.<br>
* <tt>0x0501</tt><br>
* If transaction has no 'message', returns -1.
*/
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
GET_MESSAGE_LENGTH_FROM_TX_IN_A(0x0501, 0, true) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0);
QortalATAPI api = (QortalATAPI) state.getAPI();
TransactionData transactionData = api.getTransactionFromA(state);
byte[] messageData = api.getMessageFromTransaction(transactionData);
if (messageData == null)
functionData.returnValue = -1L;
else
functionData.returnValue = (long) messageData.length;
}
},
/**
* <tt>0x0501</tt><br>
* Put transaction from specific recipient after timestamp in A, or zero if none<br>
* Put offset 'message' from transaction in A into B<br>
* <tt>0x0502 start-offset</tt><br>
* Copies up to 32 bytes of message data, starting at <tt>start-offset</tt> into B.<br>
* If transaction has no 'message', or <tt>start-offset</tt> out of bounds, then zero B<br>
* Example 'message' could be 256-bit shared secret
*/
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B(0x0502, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
Timestamp timestamp = new Timestamp(functionData.value2);
QortalATAPI api = (QortalATAPI) state.getAPI();
String recipient = new String(state.getB(), StandardCharsets.UTF_8);
// In case something goes wrong, or we don't have enough message data.
api.zeroB(state);
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
if (functionData.value1 < 0 || functionData.value1 > Integer.MAX_VALUE)
return;
int startOffset = functionData.value1.intValue();
TransactionData transactionData = api.getTransactionFromA(state);
byte[] messageData = api.getMessageFromTransaction(transactionData);
if (messageData == null || startOffset > messageData.length)
return;
/*
* Copy up to 32 bytes of message data into B,
* retain order but pad with zeros in lower bytes.
*
* So a 4-byte message "a b c d" would copy thusly:
* a b c d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
*/
int byteCount = Math.min(32, messageData.length - startOffset);
byte[] bBytes = new byte[32];
System.arraycopy(messageData, startOffset, bBytes, 0, byteCount);
api.setB(state, bBytes);
}
},
/**
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
* <tt>0x0510</tt>
*/
CONVERT_B_TO_PKH(0x0510, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Needs to be 'B' sized
byte[] pkh = new byte[32];
// Copy PKH part of B to last 20 bytes
System.arraycopy(getB(state), 32 - 20 - 4, pkh, 32 - 20, 20);
setB(state, pkh);
}
},
/**
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
* <tt>0x0511</tt><br>
* P2SH stored in lower 25 bytes of B.
*/
CONVERT_B_TO_P2SH(0x0511, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
convertAddressInB(addressPrefix, state);
}
},
/**
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
* <tt>0x0512</tt><br>
* Qortal address stored in lower 25 bytes of B.
*/
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
convertAddressInB(Crypto.ADDRESS_VERSION, state);
}
};
@@ -48,7 +129,9 @@ public enum QortalFunctionCode {
public final int paramCount;
public final boolean returnsValue;
private final static Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
private static final Logger LOGGER = LogManager.getLogger(QortalFunctionCode.class);
private static final Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
private QortalFunctionCode(int value, int paramCount, boolean returnsValue) {
@@ -61,7 +144,7 @@ public enum QortalFunctionCode {
return map.get((short) value);
}
public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException {
public void preExecuteCheck(int paramCount, boolean returnValueExpected, short rawFunctionCode) throws IllegalFunctionCodeException {
if (paramCount != this.paramCount)
throw new IllegalFunctionCodeException(
"Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")");
@@ -84,7 +167,7 @@ public enum QortalFunctionCode {
*/
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Check passed functionData against requirements of this function
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode);
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, rawFunctionCode);
if (functionData.paramCount >= 1 && functionData.value1 == null)
throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")");
@@ -92,7 +175,7 @@ public enum QortalFunctionCode {
if (functionData.paramCount == 2 && functionData.value2 == null)
throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")");
state.getLogger().debug("Function \"" + this.name() + "\"");
LOGGER.debug(() -> String.format("Function \"%s\"", this.name()));
postCheckExecute(functionData, state, rawFunctionCode);
}
@@ -100,4 +183,29 @@ public enum QortalFunctionCode {
/** Actually execute function */
protected abstract void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException;
private static void convertAddressInB(byte addressPrefix, MachineState state) {
byte[] addressNoChecksum = new byte[1 + 20];
addressNoChecksum[0] = addressPrefix;
System.arraycopy(getB(state), 0, addressNoChecksum, 1, 20);
byte[] checksum = Crypto.doubleDigest(addressNoChecksum);
// Needs to be 'B' sized
byte[] address = new byte[32];
System.arraycopy(addressNoChecksum, 0, address, 32 - 1 - 20 - 4, addressNoChecksum.length);
System.arraycopy(checksum, 0, address, 32 - 4, 4);
setB(state, address);
}
private static byte[] getB(MachineState state) {
QortalATAPI api = (QortalATAPI) state.getAPI();
return api.getB(state);
}
private static void setB(MachineState state, byte[] bBytes) {
QortalATAPI api = (QortalATAPI) state.getAPI();
api.setB(state, bBytes);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,6 @@ package org.qortal.block;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.MathContext;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
@@ -56,12 +54,14 @@ public class BlockChain {
/** Transaction expiry period, starting from transaction's timestamp, in milliseconds. */
private long transactionExpiryPeriod;
private BigDecimal unitFee;
private BigDecimal maxBytesPerUnitFee;
private BigDecimal minFeePerByte;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long unitFee;
private int maxBytesPerUnitFee;
/** Maximum acceptable timestamp disagreement offset in milliseconds. */
private long blockTimestampMargin;
/** Maximum block size, in bytes. */
private int maxBlockSize;
@@ -71,15 +71,6 @@ public class BlockChain {
private GenesisBlock.GenesisInfo genesisInfo;
public enum FeatureTrigger {
messageHeight, // block height when MESSAGE transactions are enabled
atHeight, // block height when CIYAM automated transactions are enabled
assetsTimestamp, // timestamp when assets (issue/transfer/payments) are enabled
votingTimestamp, // timestamp when voting is enabled
arbitraryTimestamp, // timestamp when arbitrary transactions are enabled
powfixTimestamp, // timestamp when various legacy fixes come into effect
qortalTimestamp, // timestamp when Qortal changes come into effect
newAssetPricingTimestamp, // timestamp when new asset pricing comes into effect
groupApprovalTimestamp; // timestamp when transaction approval comes into effect
}
/** Map of which blockchain features are enabled when (height/timestamp) */
@@ -95,21 +86,28 @@ public class BlockChain {
/** Block rewards by block height */
public static class RewardByHeight {
public int height;
public BigDecimal reward;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long reward;
}
List<RewardByHeight> rewardsByHeight;
private List<RewardByHeight> rewardsByHeight;
/** Share of block reward/fees by account level */
public static class ShareByLevel {
public static class AccountLevelShareBin {
public List<Integer> levels;
public BigDecimal share;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long share;
}
List<ShareByLevel> sharesByLevel;
private List<AccountLevelShareBin> sharesByLevel;
/** Generated lookup of share-bin by account level */
private AccountLevelShareBin[] shareBinsByLevel;
/** Share of block reward/fees to legacy QORA coin holders */
BigDecimal qoraHoldersShare;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraHoldersShare;
/** How many legacy QORA per 1 QORT of block reward. */
BigDecimal qoraPerQortReward;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long qoraPerQortReward;
/**
* Number of minted blocks required to reach next level from previous.
@@ -120,7 +118,7 @@ public class BlockChain {
* Example: if <tt>blocksNeededByLevel[3]</tt> is 200,<br>
* then level 3 accounts need to mint 200 blocks to reach level 4.
*/
List<Integer> blocksNeededByLevel;
private List<Integer> blocksNeededByLevel;
/**
* Cumulative number of minted blocks required to reach next level from scratch.
@@ -134,7 +132,7 @@ public class BlockChain {
* <p>
* Should NOT be present in blockchain config file!
*/
List<Integer> cumulativeBlocksByLevel;
private List<Integer> cumulativeBlocksByLevel;
/** Block times by block height */
public static class BlockTimingByHeight {
@@ -143,7 +141,7 @@ public class BlockChain {
public long deviation; // ms
public double power;
}
List<BlockTimingByHeight> blockTimingsByHeight;
private List<BlockTimingByHeight> blockTimingsByHeight;
private int minAccountLevelToMint = 1;
private int minAccountLevelToRewardShare;
@@ -155,6 +153,19 @@ public class BlockChain {
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
/** Settings relating to CIYAM AT feature. */
public static class CiyamAtSettings {
/** Fee per step/op-code executed. */
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long feePerStep;
/** Maximum number of steps per execution round, before AT is forced to sleep until next block. */
public int maxStepsPerRound;
/** How many steps for calling a function. */
public int stepsPerFunctionCall;
/** Roughly how many minutes per block. */
public int minutesPerBlock;
}
private CiyamAtSettings ciyamAtSettings;
// Constructors, etc.
@@ -225,6 +236,19 @@ public class BlockChain {
Throwable linkedException = e.getLinkedException();
if (linkedException instanceof XMLMarshalException) {
String message = ((XMLMarshalException) linkedException).getInternalException().getLocalizedMessage();
if (message == null && linkedException.getCause() != null && linkedException.getCause().getCause() != null )
message = linkedException.getCause().getCause().getLocalizedMessage();
if (message == null && linkedException.getCause() != null)
message = linkedException.getCause().getLocalizedMessage();
if (message == null)
message = linkedException.getLocalizedMessage();
if (message == null)
message = e.getLocalizedMessage();
LOGGER.error(message);
throw new RuntimeException(message, e);
}
@@ -257,18 +281,14 @@ public class BlockChain {
return this.isTestChain;
}
public BigDecimal getUnitFee() {
public long getUnitFee() {
return this.unitFee;
}
public BigDecimal getMaxBytesPerUnitFee() {
public int getMaxBytesPerUnitFee() {
return this.maxBytesPerUnitFee;
}
public BigDecimal getMinFeePerByte() {
return this.minFeePerByte;
}
public long getTransactionExpiryPeriod() {
return this.transactionExpiryPeriod;
}
@@ -298,10 +318,14 @@ public class BlockChain {
return this.rewardsByHeight;
}
public List<ShareByLevel> getBlockSharesByLevel() {
public List<AccountLevelShareBin> getAccountLevelShareBins() {
return this.sharesByLevel;
}
public AccountLevelShareBin[] getShareBinsByAccountLevel() {
return this.shareBinsByLevel;
}
public List<Integer> getBlocksNeededByLevel() {
return this.blocksNeededByLevel;
}
@@ -310,11 +334,11 @@ public class BlockChain {
return this.cumulativeBlocksByLevel;
}
public BigDecimal getQoraHoldersShare() {
public long getQoraHoldersShare() {
return this.qoraHoldersShare;
}
public BigDecimal getQoraPerQortReward() {
public long getQoraPerQortReward() {
return this.qoraPerQortReward;
}
@@ -342,53 +366,21 @@ public class BlockChain {
return this.onlineAccountSignaturesMaxLifetime;
}
public CiyamAtSettings getCiyamAtSettings() {
return this.ciyamAtSettings;
}
// Convenience methods for specific blockchain feature triggers
public long getMessageReleaseHeight() {
return featureTriggers.get("messageHeight");
}
public long getATReleaseHeight() {
return featureTriggers.get("atHeight");
}
public long getPowFixReleaseTimestamp() {
return featureTriggers.get("powfixTimestamp");
}
public long getAssetsReleaseTimestamp() {
return featureTriggers.get("assetsTimestamp");
}
public long getVotingReleaseTimestamp() {
return featureTriggers.get("votingTimestamp");
}
public long getArbitraryReleaseTimestamp() {
return featureTriggers.get("arbitraryTimestamp");
}
public long getQortalTimestamp() {
return featureTriggers.get("qortalTimestamp");
}
public long getNewAssetPricingTimestamp() {
return featureTriggers.get("newAssetPricingTimestamp");
}
public long getGroupApprovalTimestamp() {
return featureTriggers.get("groupApprovalTimestamp");
}
// More complex getters for aspects that change by height or timestamp
public BigDecimal getRewardAtHeight(int ourHeight) {
public long getRewardAtHeight(int ourHeight) {
// Scan through for reward at our height
for (int i = rewardsByHeight.size() - 1; i >= 0; --i)
if (rewardsByHeight.get(i).height <= ourHeight)
return rewardsByHeight.get(i).reward;
return null;
return 0;
}
public BlockTimingByHeight getBlockTimingByHeight(int ourHeight) {
@@ -437,6 +429,9 @@ public class BlockChain {
if (this.founderEffectiveMintingLevel <= 0)
Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config");
if (this.ciyamAtSettings == null)
Settings.throwValidationError("No \"ciyamAtSettings\" entry found in blockchain config");
if (this.featureTriggers == null)
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
@@ -444,15 +439,20 @@ public class BlockChain {
for (FeatureTrigger featureTrigger : FeatureTrigger.values())
if (!this.featureTriggers.containsKey(featureTrigger.name()))
Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name()));
// Check block reward share bounds
long totalShare = this.qoraHoldersShare;
// Add share percents for account-level-based rewards
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
totalShare += accountLevelShareBin.share;
if (totalShare < 0 || totalShare > 1_00000000L)
Settings.throwValidationError("Total non-founder share out of bounds (0<x<1e8)");
}
/** Minor normalization, cached value generation, etc. */
private void fixUp() {
this.maxBytesPerUnitFee = this.maxBytesPerUnitFee.setScale(8);
this.unitFee = this.unitFee.setScale(8);
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
// Pre-calculate cumulative blocks required for each level
// Calculate cumulative blocks required for each level
int cumulativeBlocks = 0;
this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1);
for (int level = 0; level <= this.blocksNeededByLevel.size(); ++level) {
@@ -462,6 +462,17 @@ public class BlockChain {
cumulativeBlocks += this.blocksNeededByLevel.get(level);
}
// Generate lookup-array for account-level share bins
AccountLevelShareBin lastAccountLevelShareBin = this.sharesByLevel.get(this.sharesByLevel.size() - 1);
final int lastLevel = lastAccountLevelShareBin.levels.get(lastAccountLevelShareBin.levels.size() - 1);
this.shareBinsByLevel = new AccountLevelShareBin[lastLevel];
for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel)
for (int level : accountLevelShareBin.levels)
// level 1 stored at index 0, level 2 stored at index 1, etc.
// level 0 not allowed
this.shareBinsByLevel[level - 1] = accountLevelShareBin;
// Convert collections to unmodifiable form
this.rewardsByHeight = Collections.unmodifiableList(this.rewardsByHeight);
this.sharesByLevel = Collections.unmodifiableList(this.sharesByLevel);
@@ -499,7 +510,7 @@ public class BlockChain {
}
}
private static boolean isGenesisBlockValid() throws DataException {
private static boolean isGenesisBlockValid() {
try (final Repository repository = RepositoryManager.getRepository()) {
BlockRepository blockRepository = repository.getBlockRepository();
@@ -512,6 +523,8 @@ public class BlockChain {
return false;
return GenesisBlock.isGenesisBlock(blockData);
} catch (DataException e) {
return false;
}
}

View File

@@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@@ -12,7 +13,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.block.Block.ValidationResult;
import org.qortal.controller.Controller;
import org.qortal.data.account.MintingAccountData;
@@ -40,6 +40,8 @@ public class BlockMinter extends Thread {
// Other properties
private static final Logger LOGGER = LogManager.getLogger(BlockMinter.class);
private static Long lastLogTimestamp;
private static Long logTimeout;
// Constructors
@@ -76,20 +78,15 @@ public class BlockMinter extends Thread {
boolean isMintingPossible = false;
boolean wasMintingPossible = isMintingPossible;
while (running) {
repository.discardChanges(); // Free repository locks, if any
if (isMintingPossible != wasMintingPossible)
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
wasMintingPossible = isMintingPossible;
// Sleep for a while
try {
repository.discardChanges(); // Free repository locks, if any
if (isMintingPossible != wasMintingPossible)
Controller.getInstance().onMintingPossibleChange(isMintingPossible);
wasMintingPossible = isMintingPossible;
Thread.sleep(1000);
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
}
Thread.sleep(1000);
isMintingPossible = false;
@@ -123,7 +120,7 @@ public class BlockMinter extends Thread {
continue;
}
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
Account mintingAccount = new Account(repository, rewardShareData.getMinter());
if (!mintingAccount.canMint()) {
// Minting-account component of reward-share can no longer mint - disregard
madi.remove();
@@ -131,7 +128,7 @@ public class BlockMinter extends Thread {
}
}
List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
List<Peer> peers = Network.getInstance().getHandshakedPeers();
BlockData lastBlockData = blockRepository.getLastBlock();
// Disregard peers that have "misbehaved" recently
@@ -156,23 +153,38 @@ public class BlockMinter extends Thread {
if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) {
previousBlock = new Block(repository, lastBlockData);
newBlocks.clear();
// Reduce log timeout
logTimeout = 10 * 1000L;
}
// Discard accounts we have already built blocks with
mintingAccountsData.removeIf(mintingAccountData -> newBlocks.stream().anyMatch(newBlock -> Arrays.equals(newBlock.getBlockData().getMinterPublicKey(), mintingAccountData.getPublicKey())));
// Do we need to build any potential new blocks?
List<PrivateKeyAccount> mintingAccounts = mintingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getPrivateKey())).collect(Collectors.toList());
// Discard accounts we have blocks for
mintingAccounts.removeIf(account -> newBlocks.stream().anyMatch(newBlock -> newBlock.getMinter().getAddress().equals(account.getAddress())));
for (PrivateKeyAccount mintingAccount : mintingAccounts) {
// First block does the AT heavy-lifting
if (newBlocks.isEmpty()) {
Block newBlock = Block.mint(repository, previousBlock.getBlockData(), mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.error("Couldn't build a to-be-minted block"));
continue;
}
newBlocks.add(newBlock);
} else {
// The blocks for other minters require less effort...
Block newBlock = newBlocks.get(0);
newBlocks.add(newBlock.remint(mintingAccount));
Block newBlock = newBlocks.get(0).remint(mintingAccount);
if (newBlock == null) {
// For some reason we can't mint right now
moderatedLog(() -> LOGGER.error("Couldn't rebuild a to-be-minted block"));
continue;
}
newBlocks.add(newBlock);
}
}
@@ -182,15 +194,23 @@ public class BlockMinter extends Thread {
// Make sure we're the only thread modifying the blockchain
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
if (!blockchainLock.tryLock())
if (!blockchainLock.tryLock(30, TimeUnit.SECONDS)) {
LOGGER.warn("Couldn't acquire blockchain lock even after waiting 30 seconds");
continue;
}
boolean newBlockMinted = false;
Block newBlock = null;
try {
// Clear repository's "in transaction" state so we don't cause a repository deadlock
// Clear repository session state so we have latest view of data
repository.discardChanges();
// Now that we have blockchain lock, do final check that chain hasn't changed
BlockData latestBlockData = blockRepository.getLastBlock();
if (!Arrays.equals(lastBlockData.getSignature(), latestBlockData.getSignature()))
continue;
List<Block> goodBlocks = new ArrayList<>();
for (Block testBlock : newBlocks) {
// Is new block's timestamp valid yet?
@@ -199,8 +219,12 @@ public class BlockMinter extends Thread {
continue;
// Is new block valid yet? (Before adding unconfirmed transactions)
if (testBlock.isValid() != ValidationResult.OK)
ValidationResult result = testBlock.isValid();
if (result != ValidationResult.OK) {
moderatedLog(() -> LOGGER.error(String.format("To-be-minted block invalid '%s' before adding transactions?", result.name())));
continue;
}
goodBlocks.add(testBlock);
}
@@ -212,7 +236,6 @@ public class BlockMinter extends Thread {
final int parentHeight = previousBlock.getBlockData().getHeight();
final byte[] parentBlockSignature = previousBlock.getSignature();
Block newBlock = null;
BigInteger bestWeight = null;
for (int bi = 0; bi < goodBlocks.size(); ++bi) {
@@ -257,11 +280,10 @@ public class BlockMinter extends Thread {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(newBlock.getBlockData().getMinterPublicKey());
if (rewardShareData != null) {
PublicKeyAccount mintingAccount = new PublicKeyAccount(repository, rewardShareData.getMinterPublicKey());
LOGGER.info(String.format("Minted block %d, sig %.8s by %s on behalf of %s",
newBlock.getBlockData().getHeight(),
Base58.encode(newBlock.getBlockData().getSignature()),
mintingAccount.getAddress(),
rewardShareData.getMinter(),
rewardShareData.getRecipient()));
} else {
LOGGER.info(String.format("Minted block %d, sig %.8s by %s",
@@ -284,10 +306,13 @@ public class BlockMinter extends Thread {
}
if (newBlockMinted)
Controller.getInstance().onBlockMinted();
Controller.getInstance().onNewBlock(newBlock.getBlockData());
}
} catch (DataException e) {
LOGGER.warn("Repository issue while running block minter", e);
} catch (InterruptedException e) {
// We've been interrupted - time to exit
return;
}
}
@@ -340,19 +365,21 @@ public class BlockMinter extends Thread {
this.interrupt();
}
public static void mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
if (!BlockChain.getInstance().isTestChain()) {
LOGGER.warn("Ignoring attempt to mint testing block for non-test chain!");
return;
}
public static Block mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
if (!BlockChain.getInstance().isTestChain())
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
// Ensure mintingAccount is 'online' so blocks can be minted
Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
}
public static Block mintTestingBlockRetainingTimestamps(Repository repository, PrivateKeyAccount mintingAccount) throws DataException {
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
// Make sure we're the only thread modifying the blockchain
@@ -375,9 +402,22 @@ public class BlockMinter extends Thread {
LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight()));
repository.saveChanges();
return newBlock;
} finally {
blockchainLock.unlock();
}
}
private static void moderatedLog(Runnable logFunction) {
// We only log if logging at TRACE or previous log timeout has expired
if (!LOGGER.isTraceEnabled() && lastLogTimestamp != null && lastLogTimestamp + logTimeout > System.currentTimeMillis())
return;
lastLogTimestamp = System.currentTimeMillis();
logTimeout = 2 * 60 * 1000L; // initial timeout, can be reduced if new block appears
logFunction.run();
}
}

View File

@@ -2,32 +2,25 @@ package org.qortal.block;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.account.Account;
import org.qortal.account.GenesisAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.account.NullAccount;
import org.qortal.crypto.Crypto;
import org.qortal.data.asset.AssetData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.IssueAssetTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ApprovalStatus;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.TransactionTransformer;
@@ -39,9 +32,8 @@ public class GenesisBlock extends Block {
private static final Logger LOGGER = LogManager.getLogger(GenesisBlock.class);
private static final byte[] GENESIS_REFERENCE = new byte[] {
1, 1, 1, 1, 1, 1, 1, 1
}; // NOTE: Neither 64 nor 128 bytes!
private static final byte[] GENESIS_BLOCK_REFERENCE = new byte[128];
private static final byte[] GENESIS_TRANSACTION_REFERENCE = new byte[64];
@XmlAccessorType(XmlAccessType.FIELD)
public static class GenesisInfo {
@@ -93,46 +85,25 @@ public class GenesisBlock extends Block {
// Add default values to transactions
transactionsData.stream().forEach(transactionData -> {
if (transactionData.getFee() == null)
transactionData.setFee(BigDecimal.ZERO.setScale(8));
transactionData.setFee(0L);
if (transactionData.getCreatorPublicKey() == null)
transactionData.setCreatorPublicKey(GenesisAccount.PUBLIC_KEY);
transactionData.setCreatorPublicKey(NullAccount.PUBLIC_KEY);
if (transactionData.getTimestamp() == 0)
transactionData.setTimestamp(info.timestamp);
});
// For version 1, extract any ISSUE_ASSET transactions into initialAssets and only allow GENESIS transactions
if (info.version == 1) {
List<TransactionData> issueAssetTransactions = transactionsData.stream()
.filter(transactionData -> transactionData.getType() == TransactionType.ISSUE_ASSET).collect(Collectors.toList());
transactionsData.removeAll(issueAssetTransactions);
// There should be only GENESIS transactions left;
if (transactionsData.stream().anyMatch(transactionData -> transactionData.getType() != TransactionType.GENESIS)) {
LOGGER.error("Version 1 genesis block only allowed to contain GENESIS transctions (after issue-asset processing)");
throw new RuntimeException("Version 1 genesis block only allowed to contain GENESIS transctions (after issue-asset processing)");
}
// Convert ISSUE_ASSET transactions into initial assets
initialAssets = issueAssetTransactions.stream().map(transactionData -> {
IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData;
return new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), issueAssetTransactionData.getDescription(),
issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), "", false, Group.NO_GROUP, issueAssetTransactionData.getReference());
}).collect(Collectors.toList());
}
byte[] reference = GENESIS_REFERENCE;
byte[] reference = GENESIS_BLOCK_REFERENCE;
int transactionCount = transactionsData.size();
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
byte[] minterPublicKey = GenesisAccount.PUBLIC_KEY;
long totalFees = 0;
byte[] minterPublicKey = NullAccount.PUBLIC_KEY;
byte[] bytesForSignature = getBytesForMinterSignature(info.timestamp, reference, minterPublicKey);
byte[] minterSignature = calcGenesisMinterSignature(bytesForSignature);
byte[] transactionsSignature = calcGenesisTransactionsSignature();
int height = 1;
int atCount = 0;
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
long atFees = 0;
genesisBlockData = new BlockData(info.version, reference, transactionCount, totalFees, transactionsSignature, height, info.timestamp,
minterPublicKey, minterSignature, atCount, atFees);
@@ -172,7 +143,7 @@ public class GenesisBlock extends Block {
/**
* Refuse to calculate genesis block's minter signature!
* <p>
* This is not possible as there is no private key for the genesis account and so no way to sign data.
* This is not possible as there is no private key for the null account and so no way to sign data.
* <p>
* <b>Always throws IllegalStateException.</b>
*
@@ -180,13 +151,13 @@ public class GenesisBlock extends Block {
*/
@Override
public void calcMinterSignature() {
throw new IllegalStateException("There is no private key for genesis account");
throw new IllegalStateException("There is no private key for null account");
}
/**
* Refuse to calculate genesis block's transactions signature!
* <p>
* This is not possible as there is no private key for the genesis account and so no way to sign data.
* This is not possible as there is no private key for the null account and so no way to sign data.
* <p>
* <b>Always throws IllegalStateException.</b>
*
@@ -194,13 +165,13 @@ public class GenesisBlock extends Block {
*/
@Override
public void calcTransactionsSignature() {
throw new IllegalStateException("There is no private key for genesis account");
throw new IllegalStateException("There is no private key for null account");
}
/**
* Generate genesis block minter signature.
* <p>
* This is handled differently as there is no private key for the genesis account and so no way to sign data.
* This is handled differently as there is no private key for the null account and so no way to sign data.
*
* @return byte[]
*/
@@ -212,24 +183,16 @@ public class GenesisBlock extends Block {
try {
// Passing expected size to ByteArrayOutputStream avoids reallocation when adding more bytes than default 32.
// See below for explanation of some of the values used to calculated expected size.
ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 64 + 8 + 32);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(8 + 128 + 32);
/*
* NOTE: Historic code had genesis block using Longs.toByteArray(version) compared to standard block's Ints.toByteArray. The subsequent
* Bytes.ensureCapacity(versionBytes, 0, 4) did not truncate versionBytes back to 4 bytes either. This means 8 bytes were used even though
* VERSION_LENGTH is set to 4. Correcting this historic bug will break genesis block signatures!
*/
// For Qortal, we use genesis timestamp instead
// Genesis block timestamp
bytes.write(Longs.toByteArray(timestamp));
/*
* NOTE: Historic code had the reference expanded to only 64 bytes whereas standard block references are 128 bytes. Correcting this historic bug
* will break genesis block signatures!
*/
bytes.write(Bytes.ensureCapacity(reference, 64, 0));
// Block's reference
bytes.write(reference);
// NOTE: Genesis account's public key is only 8 bytes, not the usual 32, so we have to pad.
bytes.write(Bytes.ensureCapacity(minterPublicKey, 32, 0));
// Minting account's public key (typically NullAccount)
bytes.write(minterPublicKey);
return bytes.toByteArray();
} catch (IOException e) {
@@ -295,26 +258,18 @@ public class GenesisBlock extends Block {
public void process() throws DataException {
LOGGER.info(String.format("Using genesis block timestamp of %d", this.blockData.getTimestamp()));
// If we're a version 1 genesis block, create assets now
if (this.blockData.getVersion() == 1)
for (AssetData assetData : initialAssets)
repository.getAssetRepository().save(assetData);
/*
* Some transactions will be missing references and signatures,
* so we generate them by trial-processing transactions and using
* account's last-reference to fill in the gaps for reference,
* so we generate them by using <tt>GENESIS_TRANSACTION_REFERENCE</tt>
* and a duplicated SHA256 digest for signature
*/
this.repository.setSavepoint();
try {
for (Transaction transaction : this.getTransactions()) {
TransactionData transactionData = transaction.getTransactionData();
Account creator = new PublicKeyAccount(this.repository, transactionData.getCreatorPublicKey());
// Missing reference?
if (transactionData.getReference() == null)
transactionData.setReference(creator.getLastReference());
transactionData.setReference(GENESIS_TRANSACTION_REFERENCE);
// Missing signature?
if (transactionData.getSignature() == null) {
@@ -324,24 +279,21 @@ public class GenesisBlock extends Block {
transactionData.setSignature(signature);
}
// Missing approval status (not used in V1)
// Approval status
transactionData.setApprovalStatus(ApprovalStatus.NOT_REQUIRED);
// Ask transaction to update references, etc.
transaction.processReferencesAndFees();
creator.setLastReference(transactionData.getSignature());
}
} catch (TransformationException e) {
throw new RuntimeException("Can't process genesis block transaction", e);
} finally {
this.repository.rollbackToSavepoint();
}
// Save transactions into repository ready for processing
for (Transaction transaction : this.getTransactions())
this.repository.getTransactionRepository().save(transaction.getTransactionData());
// No ATs in genesis block
this.ourAtStates = Collections.emptyList();
this.ourAtFees = 0;
super.process();
}

View File

@@ -21,6 +21,7 @@ import org.qortal.ApplyUpdate;
import org.qortal.api.ApiRequest;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.globalization.Translator;
import org.qortal.gui.SysTray;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -40,7 +41,7 @@ public class AutoUpdate extends Thread {
public static final String NEW_JAR_FILENAME = "new-" + JAR_FILENAME;
private static final Logger LOGGER = LogManager.getLogger(AutoUpdate.class);
private static final long CHECK_INTERVAL = 5 * 60 * 1000L; // ms
private static final long CHECK_INTERVAL = 20 * 60 * 1000L; // ms
private static final int DEV_GROUP_ID = 1;
private static final int UPDATE_SERVICE = 1;
@@ -209,7 +210,7 @@ public class AutoUpdate extends Thread {
return false; // failed - try another repo
}
} catch (IOException e) {
LOGGER.warn(String.format("Failed to fetch update from %s", repoUri));
LOGGER.warn(String.format("Failed to fetch update from %s: %s", repoUri, e.getMessage()));
return false; // failed - try another repo
}
@@ -245,7 +246,9 @@ public class AutoUpdate extends Thread {
LOGGER.info(String.format("Applying update with: %s", String.join(" ", javaCmd)));
SysTray.getInstance().showMessage("Auto Update", "Applying automatic update and restarting...", MessageType.INFO);
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"),
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"),
MessageType.INFO);
new ProcessBuilder(javaCmd).start();

View File

@@ -0,0 +1,61 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.qortal.api.model.BlockInfo;
import org.qortal.data.block.BlockData;
public class BlockNotifier {
private static BlockNotifier instance;
@FunctionalInterface
public interface Listener {
void notify(BlockInfo blockInfo);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
private BlockNotifier() {
}
public static synchronized BlockNotifier getInstance() {
if (instance == null)
instance = new BlockNotifier();
return instance;
}
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public void onNewBlock(BlockData blockData) {
// Convert BlockData to BlockInfo
BlockInfo blockInfo = new BlockInfo(blockData);
for (Listener listener : getAllListeners())
listener.notify(blockInfo);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -0,0 +1,62 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.qortal.data.transaction.ChatTransactionData;
public class ChatNotifier {
private static ChatNotifier instance;
@FunctionalInterface
public interface Listener {
void notify(ChatTransactionData chatTransactionData);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
private ChatNotifier() {
}
public static synchronized ChatNotifier getInstance() {
if (instance == null)
instance = new ChatNotifier();
return instance;
}
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
for (Listener listener : getAllListeners())
listener.notify(chatTransactionData);
}
public void onGroupMembershipChange() {
for (Listener listener : getAllListeners())
listener.notify(null);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
public class StatusNotifier {
private static StatusNotifier instance;
@FunctionalInterface
public interface Listener {
void notify(long timestamp);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
private StatusNotifier() {
}
public static synchronized StatusNotifier getInstance() {
if (instance == null)
instance = new StatusNotifier();
return instance;
}
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public void onStatusChange(long now) {
for (Listener listener : getAllListeners())
listener.notify(now);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -22,7 +22,6 @@ import org.qortal.network.message.BlockMessage;
import org.qortal.network.message.BlockSummariesMessage;
import org.qortal.network.message.GetBlockMessage;
import org.qortal.network.message.GetBlockSummariesMessage;
import org.qortal.network.message.GetSignaturesMessage;
import org.qortal.network.message.GetSignaturesV2Message;
import org.qortal.network.message.Message;
import org.qortal.network.message.SignaturesMessage;
@@ -372,12 +371,7 @@ public class Synchronizer {
return SynchronizationResult.TOO_DIVERGENT;
}
if (peer.getVersion() >= 2) {
step <<= 1;
} else {
// Old v1 peers are hard-coded to return 500 signatures so we might as well go backward by 500 too
step = 500;
}
step <<= 1;
step = Math.min(step, MAXIMUM_BLOCK_STEP);
testHeight = Math.max(testHeight - step, 1);
@@ -415,8 +409,7 @@ public class Synchronizer {
}
private List<byte[]> getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException {
// numberRequested is v2+ feature
Message getSignaturesMessage = peer.getVersion() >= 2 ? new GetSignaturesV2Message(parentSignature, numberRequested) : new GetSignaturesMessage(parentSignature);
Message getSignaturesMessage = new GetSignaturesV2Message(parentSignature, numberRequested);
Message message = peer.getResponse(getSignaturesMessage);
if (message == null || message.getType() != MessageType.SIGNATURES)

File diff suppressed because it is too large Load Diff

View File

@@ -1,199 +1,93 @@
package org.qortal.crosschain;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.util.Date;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.CheckpointManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.core.UTXO;
import org.bitcoinj.core.UTXOProvider;
import org.bitcoinj.core.UTXOProviderException;
import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.SPVBlockStore;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.KeyChainGroup;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.qortal.crosschain.ElectrumX.UnspentOutput;
import org.qortal.crypto.Crypto;
import org.qortal.settings.Settings;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
public class BTC {
private static class RollbackBlockChain extends BlockChain {
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20;
public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
super(params, blockStore);
}
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
@Override
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
super.setChainHead(chainHead);
}
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
private static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
}
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
private static final int checkpointInterval = 500;
private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
public UpdateableCheckpointManager(NetworkParameters params) throws IOException {
super(params, getMinimalTextFileStream(params));
}
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
super(params, inputStream);
}
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
if (params == MainNetParams.get())
return new ByteArrayInputStream(minimalMainNetTextFile.getBytes());
if (params == TestNet3Params.get())
return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes());
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
}
@Override
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
int height = block.getHeight();
if (height % checkpointInterval == 0)
checkpoints.put(block.getHeader().getTimeSeconds(), block);
}
public void saveAsText(File textFile) throws FileNotFoundException {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
writer.println("TXT CHECKPOINTS 1");
writer.println("0"); // Number of signatures to read. Do this later.
writer.println(checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpoints.values()) {
block.serializeCompact(buffer);
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
buffer.position(0);
}
public enum BitcoinNet {
MAIN {
@Override
public NetworkParameters getParams() {
return MainNetParams.get();
}
}
@SuppressWarnings("unused")
public void saveAsBinary(File file) throws IOException {
try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
MessageDigest digest = Sha256Hash.newDigest();
try (final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest)) {
digestOutputStream.on(false);
try (final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream)) {
dataOutputStream.writeBytes("CHECKPOINTS 1");
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
digestOutputStream.on(true);
dataOutputStream.writeInt(checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpoints.values()) {
block.serializeCompact(buffer);
dataOutputStream.write(buffer.array());
buffer.position(0);
}
}
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return TestNet3Params.get();
}
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return RegTestParams.get();
}
};
public abstract NetworkParameters getParams();
}
private static BTC instance;
private static final Object instanceLock = new Object();
private final NetworkParameters params;
private final ElectrumX electrumX;
private static File directory;
private static String chainFileName;
private static String checkpointsFileName;
// Let ECKey.equals() do the hard work
private final Set<ECKey> spentKeys = new HashSet<>();
private static NetworkParameters params;
private static PeerGroup peerGroup;
private static BlockStore blockStore;
private static RollbackBlockChain chain;
private static UpdateableCheckpointManager manager;
// Constructors and instance
private BTC() {
// Start wallet
if (Settings.getInstance().useBitcoinTestNet()) {
params = TestNet3Params.get();
chainFileName = "bitcoinj-testnet.spvchain";
checkpointsFileName = "checkpoints-testnet.txt";
} else {
params = MainNetParams.get();
chainFileName = "bitcoinj.spvchain";
checkpointsFileName = "checkpoints.txt";
}
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
this.params = bitcoinNet.getParams();
directory = new File("Qortal-BTC");
if (!directory.exists())
directory.mkdirs();
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
File chainFile = new File(directory, chainFileName);
try {
blockStore = new SPVBlockStore(params, chainFile);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e);
}
File checkpointsFile = new File(directory, checkpointsFileName);
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
manager = new UpdateableCheckpointManager(params, checkpointsStream);
} catch (FileNotFoundException e) {
// Construct with no checkpoints then
try {
manager = new UpdateableCheckpointManager(params);
} catch (IOException e2) {
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
}
} catch (IOException e) {
throw new RuntimeException("Failed to load BTC checkpoints", e);
}
try {
chain = new RollbackBlockChain(params, blockStore);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to construct BTC blockchain", e);
}
peerGroup = new PeerGroup(params, chain);
peerGroup.setUserAgent("qortal", "1.0");
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
peerGroup.start();
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
}
public static synchronized BTC getInstance() {
@@ -203,109 +97,285 @@ public class BTC {
return instance;
}
public void shutdown() {
synchronized (instanceLock) {
if (instance == null)
return;
// Getters & setters
instance = null;
public NetworkParameters getNetworkParameters() {
return this.params;
}
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
public static String format(Coin amount) {
return BTC.FORMAT.format(amount).toString();
}
public static String format(long amount) {
return format(Coin.valueOf(amount));
}
public boolean isValidXprv(String xprv58) {
try {
DeterministicKey.deserializeB58(null, xprv58, this.params);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
/** Returns P2PKH Bitcoin address using passed public key hash. */
public String pkhToAddress(byte[] publicKeyHash) {
return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
}
public String deriveP2shAddress(byte[] redeemScriptBytes) {
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
return p2shAddress.toString();
}
/** Returns median timestamp from latest 11 blocks, in seconds. */
public Integer getMedianBlockTime() {
Integer height = this.electrumX.getCurrentHeight();
if (height == null)
return null;
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height - 11, 11);
if (blockHeaders == null || blockHeaders.size() < 11)
return null;
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
// Descending order
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
// Pick median
return blockTimestamps.get(5);
}
public Long getBalance(String base58Address) {
return this.electrumX.getBalance(addressToScript(base58Address));
}
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
List<UnspentOutput> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
if (unspentOutputs == null)
return null;
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
return null;
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
}
peerGroup.stop();
return unspentTransactionOutputs;
}
public List<TransactionOutput> getOutputs(byte[] txHash) {
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
if (rawTransactionBytes == null)
return null;
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
return transaction.getOutputs();
}
/** Returns list of raw transactions spending passed address. */
public List<byte[]> getAddressTransactions(String base58Address) {
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
}
public boolean broadcastTransaction(Transaction transaction) {
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
}
/**
* Returns bitcoinj transaction sending <tt>amount</tt> to <tt>recipient</tt>.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @param recipient P2PKH address
* @param amount unscaled amount
* @return transaction, or null if insufficient funds
*/
public Transaction buildSpend(String xprv58, String recipient, long amount) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ALL_SPENT));
Address destination = Address.fromString(this.params, recipient);
SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
if (this.params == TestNet3Params.get())
// Much smaller fee for TestNet3
sendRequest.feePerKb = Coin.valueOf(2000L);
try {
blockStore.close();
} catch (BlockStoreException e) {
// What can we do?
wallet.completeTx(sendRequest);
return sendRequest.tx;
} catch (InsufficientMoneyException e) {
return null;
}
}
protected Wallet createEmptyWallet() {
ECKey dummyKey = new ECKey();
/**
* Returns unspent Bitcoin balance given 'm' BIP32 key.
*
* @param xprv58 BIP32 extended Bitcoin private key
* @return unspent BTC balance, or null if unable to determine balance
*/
public Long getWalletBalance(String xprv58) {
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet, WalletAwareUTXOProvider.KeySearchMode.REQUEST_MORE_IF_ANY_SPENT));
KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params);
keyChainGroup.importKeys(dummyKey);
Coin balance = wallet.getBalance();
if (balance == null)
return null;
Wallet wallet = new Wallet(params, keyChainGroup);
wallet.removeKey(dummyKey);
return wallet;
return balance.value;
}
public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
Wallet wallet = createEmptyWallet();
// UTXOProvider support
WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
System.out.println("Coins received via transaction " + tx.getTxId().toString());
}
};
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
static class WalletAwareUTXOProvider implements UTXOProvider {
private static final int LOOKAHEAD_INCREMENT = 3;
Address address = Address.fromString(params, base58Address);
wallet.addWatchedAddress(address, startTime);
private final BTC btc;
private final Wallet wallet;
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
enum KeySearchMode {
REQUEST_MORE_IF_ALL_SPENT, REQUEST_MORE_IF_ANY_SPENT;
}
private final KeySearchMode keySearchMode;
private final DeterministicKeyChain keyChain;
chain.addWallet(wallet);
peerGroup.addWallet(wallet);
peerGroup.setFastCatchupTimeSecs(startTime);
public WalletAwareUTXOProvider(BTC btc, Wallet wallet, KeySearchMode keySearchMode) {
this.btc = btc;
this.wallet = wallet;
this.keySearchMode = keySearchMode;
this.keyChain = this.wallet.getActiveKeyChain();
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
if (blocksLeft % 1000 == 0)
System.out.println("Blocks left: " + blocksLeft);
});
System.out.println("Starting download...");
peerGroup.downloadBlockChain();
List<TransactionOutput> outputs = wallet.getWatchedOutputs(true);
peerGroup.removeWallet(wallet);
chain.removeWallet(wallet);
for (TransactionOutput output : outputs)
System.out.println(output.toString());
}
public void watch(Script script) {
// wallet.addWatchedScripts(scripts);
}
public void updateCheckpoints() {
final long now = new Date().getTime() / 1000 - 86400;
try {
StoredBlock checkpoint = manager.getCheckpointBefore(now);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to update BTC checkpoints", e);
// Set up wallet's key chain
this.keyChain.setLookaheadSize(LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
}
peerGroup.setFastCatchupTimeSecs(now);
public List<UTXO> getOpenTransactionOutputs(List<ECKey> keys) throws UTXOProviderException {
List<UTXO> allUnspentOutputs = new ArrayList<>();
final boolean coinbase = false;
chain.addNewBestBlockListener(Threading.SAME_THREAD, manager);
int ki = 0;
do {
boolean areAllKeysUnspent = true;
boolean areAllKeysSpent = true;
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
if (blocksLeft % 1000 == 0)
System.out.println("Blocks left: " + blocksLeft);
});
for (; ki < keys.size(); ++ki) {
ECKey key = keys.get(ki);
System.out.println("Starting download...");
peerGroup.downloadBlockChain();
Address address = Address.fromKey(btc.params, key, ScriptType.P2PKH);
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
try {
manager.saveAsText(new File(directory, checkpointsFileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
List<UnspentOutput> unspentOutputs = btc.electrumX.getUnspentOutputs(script);
if (unspentOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
/*
* If there are no unspent outputs then either:
* a) all the outputs have been spent
* b) address has never been used
*
* For case (a) we want to remember not to check this address (key) again.
*/
if (unspentOutputs.isEmpty()) {
// If this is a known key that has been spent before, then we can skip asking for transaction history
if (btc.spentKeys.contains(key)) {
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
continue;
}
// Ask for transaction history - if it's empty then key has never been used
List<byte[]> historicTransactionHashes = btc.electrumX.getAddressTransactions(script);
if (historicTransactionHashes == null)
throw new UTXOProviderException(
String.format("Unable to fetch transaction history for %s", address));
if (!historicTransactionHashes.isEmpty()) {
// Fully spent key - case (a)
btc.spentKeys.add(key);
wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
areAllKeysUnspent = false;
} else {
// Key never been used - case (b)
areAllKeysSpent = false;
}
continue;
}
// If we reach here, then there's definitely at least one unspent key
areAllKeysSpent = false;
for (UnspentOutput unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = btc.getOutputs(unspentOutput.hash);
if (transactionOutputs == null)
throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
HashCode.fromBytes(unspentOutput.hash)));
TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
transactionOutput.getScriptPubKey());
allUnspentOutputs.add(utxo);
}
}
if ((this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ALL_SPENT && areAllKeysSpent)
|| (this.keySearchMode == KeySearchMode.REQUEST_MORE_IF_ANY_SPENT && !areAllKeysUnspent)) {
// Generate some more keys
this.keyChain.setLookaheadSize(this.keyChain.getLookaheadSize() + LOOKAHEAD_INCREMENT);
this.keyChain.maybeLookAhead();
// This returns all keys, including those already in 'keys'
List<DeterministicKey> allLeafKeys = this.keyChain.getLeafKeys();
// Add only new keys onto our list of keys to search
List<DeterministicKey> newKeys = allLeafKeys.subList(ki, allLeafKeys.size());
keys.addAll(newKeys);
// Fall-through to checking more keys as now 'ki' is smaller than 'keys.size()' again
}
// If we have processed all keys, then we're done
} while (ki < keys.size());
return allUnspentOutputs;
}
public int getChainHeadHeight() throws UTXOProviderException {
Integer height = btc.electrumX.getCurrentHeight();
if (height == null)
throw new UTXOProviderException("Unable to determine Bitcoin chain height");
return height.intValue();
}
public NetworkParameters getParams() {
return btc.params;
}
}
// Utility methods for us
private byte[] addressToScript(String base58Address) {
Address address = Address.fromString(this.params, base58Address);
return ScriptBuilder.createOutputScript(address).getProgram();
}
}

View File

@@ -0,0 +1,918 @@
package org.qortal.crosschain;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import static org.ciyam.at.OpCode.calcOffset;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.ciyam.at.API;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/**
* Cross-chain trade AT
*
* <p>
* <ul>
* <li>Bob generates Bitcoin & Qortal 'trade' keys, and secret-b
* <ul>
* <li>private key required to sign P2SH redeem tx</li>
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
* </ul>
* </li>
* <li>Bob deploys Qortal AT
* <ul>
* </ul>
* </li>
* <li>Alice finds Qortal AT and wants to trade
* <ul>
* <li>Alice generates Bitcoin & Qortal 'trade' keys</li>
* <li>Alice funds Bitcoin P2SH-A</li>
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
* <ul>
* <li>hash-of-secret-A</li>
* <li>her 'trade' Bitcoin PKH</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>Bob receives "offer" MESSAGE
* <ul>
* <li>Checks Alice's P2SH-A</li>
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
* <ul>
* <li>Alice's trade Qortal address</li>
* <li>Alice's trade Bitcoin PKH</li>
* <li>hash-of-secret-A</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>Alice checks Qortal AT to confirm it's locked to her
* <ul>
* <li>Alice creates/funds Bitcoin P2SH-B</li>
* </ul>
* </li>
* <li>Bob checks P2SH-B is funded
* <ul>
* <li>Bob redeems P2SH-B using his Bitcoin trade key and secret-B</li>
* </ul>
* </li>
* <li>Alice scans P2SH-B redeem transaction to extract secret-B
* <ul>
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
* <ul>
* <li>secret-A</li>
* <li>secret-B</li>
* <li>Qortal receiving address of her chosing</li>
* </ul>
* </li>
* <li>AT's QORT funds are sent to Qortal receiving address</li>
* </ul>
* </li>
* <li>Bob checks AT, extracts secret-A
* <ul>
* <li>Bob redeems P2SH-A using his Bitcoin trade key and secret-A</li>
* <li>P2SH-A BTC funds end up at Bitcoin address determined by redeem transaction output(s)</li>
* </ul>
* </li>
* </ul>
*/
public class BTCACCT {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("f7f419522a9aaa3c671149878f8c1374dfc59d4fd86ca43ff2a4d913cfbc9e89").asBytes(); // SHA256 of AT code bytes
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
private static final int MODE_VALUE_OFFSET = 68;
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
public static class OfferMessageData {
public byte[] partnerBitcoinPKH;
public byte[] hashOfSecretA;
public long lockTimeA;
}
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerBitcoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ 24 /*partner's Bitcoin PKH (padded from 20 to 24)*/
+ 8 /*lockTimeB*/
+ 24 /*hash of secret-A (padded from 20 to 24)*/
+ 8 /*lockTimeA*/;
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret*/ + 32 /*secret*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
public enum Mode {
OFFERING(0), TRADING(1), CANCELLED(2), REFUNDED(3), REDEEMED(4);
public final int value;
private static final Map<Integer, Mode> map = stream(Mode.values()).collect(toMap(mode -> mode.value, mode -> mode));
Mode(int value) {
this.value = value;
}
public static Mode valueOf(int value) {
return map.get(value);
}
}
private BTCACCT() {
}
/**
* Returns Qortal AT creation bytes for cross-chain trading AT.
* <p>
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
*
* @param creatorTradeAddress AT creator's trade Qortal address, also used for refunds
* @param bitcoinPublicKeyHash 20-byte HASH160 of creator's trade Bitcoin public key
* @param hashOfSecretB 20-byte HASH160 of 32-byte secret-B
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
* @param bitcoinAmount how much BTC the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire trade
* @return
*/
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) {
// Labels for data segment addresses
int addrCounter = 0;
// Constants (with corresponding dataByteBuffer.put*() calls below)
final int addrCreatorTradeAddress1 = addrCounter++;
final int addrCreatorTradeAddress2 = addrCounter++;
final int addrCreatorTradeAddress3 = addrCounter++;
final int addrCreatorTradeAddress4 = addrCounter++;
final int addrBitcoinPublicKeyHash = addrCounter;
addrCounter += 4;
final int addrHashOfSecretB = addrCounter;
addrCounter += 4;
final int addrQortAmount = addrCounter++;
final int addrBitcoinAmount = addrCounter++;
final int addrTradeTimeout = addrCounter++;
final int addrMessageTxnType = addrCounter++;
final int addrExpectedTradeMessageLength = addrCounter++;
final int addrExpectedRedeemMessageLength = addrCounter++;
final int addrCreatorAddressPointer = addrCounter++;
final int addrHashOfSecretBPointer = addrCounter++;
final int addrQortalPartnerAddressPointer = addrCounter++;
final int addrMessageSenderPointer = addrCounter++;
final int addrTradeMessagePartnerBitcoinPKHOffset = addrCounter++;
final int addrPartnerBitcoinPKHPointer = addrCounter++;
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
final int addrHashOfSecretAPointer = addrCounter++;
final int addrRedeemMessageSecretBOffset = addrCounter++;
final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
final int addrMessageDataPointer = addrCounter++;
final int addrMessageDataLength = addrCounter++;
final int addrPartnerReceivingAddressPointer = addrCounter++;
final int addrEndOfConstants = addrCounter;
// Variables
final int addrCreatorAddress1 = addrCounter++;
final int addrCreatorAddress2 = addrCounter++;
final int addrCreatorAddress3 = addrCounter++;
final int addrCreatorAddress4 = addrCounter++;
final int addrQortalPartnerAddress1 = addrCounter++;
final int addrQortalPartnerAddress2 = addrCounter++;
final int addrQortalPartnerAddress3 = addrCounter++;
final int addrQortalPartnerAddress4 = addrCounter++;
final int addrLockTimeA = addrCounter++;
final int addrLockTimeB = addrCounter++;
final int addrRefundTimeout = addrCounter++;
final int addrRefundTimestamp = addrCounter++;
final int addrLastTxnTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++;
final int addrTxnType = addrCounter++;
final int addrResult = addrCounter++;
final int addrMessageSender1 = addrCounter++;
final int addrMessageSender2 = addrCounter++;
final int addrMessageSender3 = addrCounter++;
final int addrMessageSender4 = addrCounter++;
final int addrMessageLength = addrCounter++;
final int addrMessageData = addrCounter;
addrCounter += 4;
final int addrHashOfSecretA = addrCounter;
addrCounter += 4;
final int addrPartnerBitcoinPKH = addrCounter;
addrCounter += 4;
final int addrPartnerReceivingAddress = addrCounter;
addrCounter += 4;
final int addrMode = addrCounter++;
assert addrMode == MODE_VALUE_OFFSET : "MODE_VALUE_OFFSET does not match addrMode";
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// AT creator's trade Qortal address, decoded from Base58
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
// Bitcoin public key hash
assert dataByteBuffer.position() == addrBitcoinPublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinPublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(bitcoinPublicKeyHash, 32, 0));
// Hash of secret-B
assert dataByteBuffer.position() == addrHashOfSecretB * MachineState.VALUE_SIZE : "addrHashOfSecretB incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(hashOfSecretB, 32, 0));
// Redeem Qort amount
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
dataByteBuffer.putLong(qortAmount);
// Expected Bitcoin amount
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
dataByteBuffer.putLong(bitcoinAmount);
// Suggested trade timeout (minutes)
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
dataByteBuffer.putLong(tradeTimeout);
// We're only interested in MESSAGE transactions
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
// Expected length of 'trade' MESSAGE data from AT creator
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
// Expected length of 'redeem' MESSAGE data from trade partner
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
// Index into data segment of AT creator's address, used by GET_B_IND
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
dataByteBuffer.putLong(addrCreatorAddress1);
// Index into data segment of hash of secret B, used by GET_B_IND
assert dataByteBuffer.position() == addrHashOfSecretBPointer * MachineState.VALUE_SIZE : "addrHashOfSecretBPointer incorrect";
dataByteBuffer.putLong(addrHashOfSecretB);
// Index into data segment of partner's Qortal address, used by SET_B_IND
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
dataByteBuffer.putLong(addrQortalPartnerAddress1);
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
dataByteBuffer.putLong(addrMessageSender1);
// Offset into 'trade' MESSAGE data payload for extracting partner's Bitcoin PKH
assert dataByteBuffer.position() == addrTradeMessagePartnerBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerBitcoinPKHOffset incorrect";
dataByteBuffer.putLong(32L);
// Index into data segment of partner's Bitcoin PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerBitcoinPKHPointer incorrect";
dataByteBuffer.putLong(addrPartnerBitcoinPKH);
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
dataByteBuffer.putLong(64L);
// Index into data segment to hash of secret A, used by GET_B_IND
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
dataByteBuffer.putLong(addrHashOfSecretA);
// Offset into 'redeem' MESSAGE data payload for extracting secret-B
assert dataByteBuffer.position() == addrRedeemMessageSecretBOffset * MachineState.VALUE_SIZE : "addrRedeemMessageSecretBOffset incorrect";
dataByteBuffer.putLong(32L);
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
dataByteBuffer.putLong(64L);
// Source location and length for hashing any passed secret
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
dataByteBuffer.putLong(addrMessageData);
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
dataByteBuffer.putLong(32L);
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
dataByteBuffer.putLong(addrPartnerReceivingAddress);
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
// Code labels
Integer labelRefund = null;
Integer labelTradeTxnLoop = null;
Integer labelCheckTradeTxn = null;
Integer labelCheckCancelTxn = null;
Integer labelNotTradeNorCancelTxn = null;
Integer labelCheckNonRefundTradeTxn = null;
Integer labelTradeTxnExtract = null;
Integer labelRedeemTxnLoop = null;
Integer labelCheckRedeemTxn = null;
Integer labelCheckRedeemTxnSender = null;
Integer labelCheckSecretB = null;
Integer labelPayout = null;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
/* Transaction processing loop */
labelTradeTxnLoop = codeByteBuffer.position();
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckTradeTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
// Message sender's address matches AT creator's trade address so go process 'trade' message
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
/* Checking message sender for possible cancel message */
labelCheckCancelTxn = codeByteBuffer.position();
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
// Partner address is AT creator's address, so cancel offer and finish.
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.CANCELLED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
/* Not trade nor cancel message */
labelNotTradeNorCancelTxn = codeByteBuffer.position();
// Loop to find another transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
/* Possible switch-to-trade-mode message */
labelCheckNonRefundTradeTxn = codeByteBuffer.position();
// Check 'trade' message we received has expected number of message bytes
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
// If message length matches, branch to info extraction code
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
/* Extracting info from 'trade' MESSAGE transaction */
labelTradeTxnExtract = codeByteBuffer.position();
// Extract message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
// Extract trade partner's Bitcoin public key hash (PKH)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerBitcoinPKHOffset));
// Extract partner's Bitcoin PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerBitcoinPKHPointer));
// Also extract lockTimeB
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeB));
// Grab next 32 bytes
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
// Extract hash-of-secret-a (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
// Extract lockTimeA (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
// Calculate trade refund timeout: (lockTimeA - lockTimeB) / 2 / 60
codeByteBuffer.put(OpCode.SET_DAT.compile(addrRefundTimeout, addrLockTimeA)); // refundTimeout = lockTimeA
codeByteBuffer.put(OpCode.SUB_DAT.compile(addrRefundTimeout, addrLockTimeB)); // refundTimeout -= lockTimeB
codeByteBuffer.put(OpCode.DIV_VAL.compile(addrRefundTimeout, 2L * 60L)); // refundTimeout /= 2 * 60
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
/* We are in 'trade mode' */
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.TRADING.value));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
// Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
// If we're not past refund 'timestamp' then look for next transaction
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
// We're past refund 'timestamp' so go refund everything back to AT creator
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
/* Transaction processing loop */
labelRedeemTxnLoop = codeByteBuffer.position();
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckRedeemTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check message payload length */
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
// If message length matches, branch to sender checking code
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Check transaction's sender */
labelCheckRedeemTxnSender = codeByteBuffer.position();
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check 'secret-A' in transaction's message */
// Extract secret-A from first 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
// If hashes don't match, addrResult will be zero so go find another transaction
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckSecretB)));
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Check 'secret-B' in transaction's message */
labelCheckSecretB = codeByteBuffer.position();
// Extract secret-B from next 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageSecretBOffset));
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
// Load B register with expected hash result (as pointed to by addrHashOfSecretBPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretBPointer));
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
// If hashes don't match, addrResult will be zero so go find another transaction
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Success! Pay arranged amount to receiving address */
labelPayout = codeByteBuffer.position();
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
// Pay AT's balance to receiving address
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
// Set redeemed mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.REDEEMED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
// Fall-through to refunding any remaining balance back to AT creator
/* Refund balance back to AT creator */
labelRefund = codeByteBuffer.position();
// Set refunded mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, Mode.REFUNDED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), BTCACCT.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*
* @param repository
* @param atAddress
* @throws DataException
*/
public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*
* @param repository
* @param atAddress
* @throws DataException
*/
public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress());
return populateTradeData(repository, creatorPublicKey, atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*
* @param repository
* @param atAddress
* @throws DataException
*/
public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException {
byte[] addressBytes = new byte[25]; // for general use
String atAddress = atStateData.getATAddress();
CrossChainTradeData tradeData = new CrossChainTradeData();
tradeData.qortalAtAddress = atAddress;
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
tradeData.creationTimestamp = atStateData.getCreation();
Account atAccount = new Account(repository, atAddress);
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
byte[] stateData = atStateData.getStateData();
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
dataByteBuffer.position(MachineState.HEADER_LENGTH);
/* Constants */
// Skip creator's trade address
dataByteBuffer.get(addressBytes);
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Creator's Bitcoin/foreign public key hash
tradeData.creatorBitcoinPKH = new byte[20];
dataByteBuffer.get(tradeData.creatorBitcoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorBitcoinPKH.length); // skip to 32 bytes
// Hash of secret B
tradeData.hashOfSecretB = new byte[20];
dataByteBuffer.get(tradeData.hashOfSecretB);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.hashOfSecretB.length); // skip to 32 bytes
// Redeem payout
tradeData.qortAmount = dataByteBuffer.getLong();
// Expected BTC amount
tradeData.expectedBitcoin = dataByteBuffer.getLong();
// Trade timeout
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
// Skip MESSAGE transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected 'trade' message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected 'redeem' message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to hash-of-secret-B
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Qortal trade address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message sender
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for partner's bitcoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's bitcoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'redeem' message data offset for secret-B
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'redeem' message data offset for partner's Qortal receiving address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message data
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip message data length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's receiving address
dataByteBuffer.position(dataByteBuffer.position() + 8);
/* End of constants / begin variables */
// Skip AT creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Partner's trade address (if present)
dataByteBuffer.get(addressBytes);
String qortalRecipient = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Potential lockTimeA (if in trade mode)
int lockTimeA = (int) dataByteBuffer.getLong();
// Potential lockTimeB (if in trade mode)
int lockTimeB = (int) dataByteBuffer.getLong();
// AT refund timeout (probably only useful for debugging)
int refundTimeout = (int) dataByteBuffer.getLong();
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
long tradeRefundTimestamp = dataByteBuffer.getLong();
// Skip last transaction timestamp
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip block timestamp
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary result
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary message sender
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Skip message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary message data
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Potential hash160 of secret A
byte[] hashOfSecretA = new byte[20];
dataByteBuffer.get(hashOfSecretA);
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
// Potential partner's Bitcoin PKH
byte[] partnerBitcoinPKH = new byte[20];
dataByteBuffer.get(partnerBitcoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerBitcoinPKH.length); // skip to 32 bytes
// Partner's receiving address (if present)
byte[] partnerReceivingAddress = new byte[25];
dataByteBuffer.get(partnerReceivingAddress);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
// Trade AT's 'mode'
long modeValue = dataByteBuffer.getLong();
Mode mode = Mode.valueOf((int) (modeValue & 0xffL));
/* End of variables */
if (mode != null && mode != Mode.OFFERING) {
tradeData.mode = mode;
tradeData.refundTimeout = refundTimeout;
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
tradeData.qortalPartnerAddress = qortalRecipient;
tradeData.hashOfSecretA = hashOfSecretA;
tradeData.partnerBitcoinPKH = partnerBitcoinPKH;
tradeData.lockTimeA = lockTimeA;
tradeData.lockTimeB = lockTimeB;
if (mode == Mode.REDEEMED)
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
} else {
tradeData.mode = Mode.OFFERING;
}
return tradeData;
}
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
}
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
public static OfferMessageData extractOfferMessageData(byte[] messageData) {
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
return null;
OfferMessageData offerMessageData = new OfferMessageData();
offerMessageData.partnerBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
return offerMessageData;
}
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int lockTimeB) {
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
byte[] lockTimeBBytes = BitTwiddling.toBEByteArray((long) lockTimeB);
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
System.arraycopy(lockTimeBBytes, 0, data, 56, lockTimeBBytes.length);
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
return data;
}
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
public static byte[] buildCancelMessage(String creatorQortalAddress) {
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
return data;
}
/** Returns 'redeem' MESSAGE payload for trade partner/ to send to AT. */
public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB, String qortalReceivingAddress) {
byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
System.arraycopy(secretA, 0, data, 0, secretA.length);
System.arraycopy(secretB, 0, data, 32, secretB.length);
System.arraycopy(qortalReceivingAddressBytes, 0, data, 64, qortalReceivingAddressBytes.length);
return data;
}
/** Returns P2SH-B lockTime (epoch seconds) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
public static int calcLockTimeB(long offerMessageTimestamp, int lockTimeA) {
// lockTimeB is halfway between offerMessageTimesamp and lockTimeA
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
}
public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(atAddress, null, null, null);
if (messageTransactionsData == null)
return null;
// Find 'redeem' message
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
// Check message payload type/encryption
if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
continue;
// Check message payload size
byte[] messageData = messageTransactionData.getData();
if (messageData.length != REDEEM_MESSAGE_LENGTH)
// Wrong payload length
continue;
// Check sender
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
// Wrong sender;
continue;
// Extract both secretA & secretB
byte[] secretA = new byte[32];
System.arraycopy(messageData, 0, secretA, 0, secretA.length);
byte[] secretB = new byte[32];
System.arraycopy(messageData, 32, secretB, 0, secretB.length);
byte[] hashOfSecretA = Crypto.hash160(secretA);
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
continue;
byte[] hashOfSecretB = Crypto.hash160(secretB);
if (!Arrays.equals(hashOfSecretB, crossChainTradeData.hashOfSecretB))
continue;
return secretA;
}
return null;
}
}

View File

@@ -0,0 +1,236 @@
package org.qortal.crosschain;
import java.util.List;
import java.util.function.Function;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.qortal.crypto.Crypto;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class BTCP2SH {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
/*
* OP_TUCK (to copy public key to before signature)
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
* OP_HASH160 (convert public key to PKH)
* OP_DUP (duplicate PKH)
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
* OP_IF
* OP_DROP (no need for duplicate PKH)
* <push 4 bytes> <locktime>
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
* OP_ELSE
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
* OP_HASH160 (hash secret)
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
* OP_ENDIF
*/
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
/**
* Returns Bitcoin redeemScript used for cross-chain trading.
* <p>
* See comments in {@link BTCP2SH} for more details.
*
* @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
* @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
* @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
* @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
* @return
*/
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
}
/**
* Builds a custom transaction to spend P2SH.
*
* @param amount output amount, should be total of input amounts, less miner fees
* @param spendKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime (optional) transaction nLockTime, used in refund scenario
* @param scriptSigBuilder function for building scriptSig using transaction input signature
* @param outputPublicKeyHash PKH used to create P2PKH output
* @return Signed Bitcoin transaction for spending P2SH
*/
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes,
Long lockTime, Function<byte[], Script> scriptSigBuilder, byte[] outputPublicKeyHash) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction transaction = new Transaction(params);
transaction.setVersion(2);
// Output is back to P2SH funder
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(outputPublicKeyHash));
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
// Input (without scriptSig prior to signing)
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
if (lockTime != null)
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
else
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
transaction.addInput(input);
}
// Set locktime after inputs added but before input signatures are generated
if (lockTime != null)
transaction.setLockTime(lockTime);
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
// Generate transaction signature for input
final boolean anyoneCanPay = false;
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
// Calculate transaction signature
byte[] txSigBytes = txSig.encodeToBitcoin();
// Build scriptSig using lambda and tx signature
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
// Set input scriptSig
transaction.getInput(inputIndex).setScriptSig(scriptSig);
}
return transaction;
}
/**
* Returns signed Bitcoin transaction claiming refund from P2SH address.
*
* @param refundAmount refund amount, should be total of input amounts, less miner fees
* @param refundKey key for signing transaction, and also where refund is 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @return Signed Bitcoin transaction for refunding P2SH
*/
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// transaction signature
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// redeem public key
byte[] refundPubKey = refundKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
return scriptBuilder.build();
};
// Send funds back to funding address
return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, refundKey.getPubKeyHash());
}
/**
* Returns signed Bitcoin transaction redeeming funds from P2SH address.
*
* @param redeemAmount redeem amount, should be total of input amounts, less miner fees
* @param redeemKey key for signing transaction, and also where funds are 'sent' (output)
* @param fundingOutput output from transaction that funded P2SH address
* @param redeemScriptBytes the redeemScript itself, in byte[] form
* @param secret actual 32-byte secret used when building redeemScript
* @param receivingAccountInfo Bitcoin PKH used for output
* @return Signed Bitcoin transaction for redeeming P2SH
*/
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) {
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder();
// secret
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
// transaction signature
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
// redeem public key
byte[] redeemPubKey = redeemKey.getPubKey();
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
// redeem script
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
return scriptBuilder.build();
};
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
}
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our P2SH
for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
// Expected number of script chunks for redeem. Refund might not have the same number.
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
if (scriptChunks.size() != expectedChunkCount)
continue;
// We're expecting last chunk to contain the actual redeemScript
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
byte[] redeemScriptBytes = lastChunk.data;
// If non-push scripts, redeemScript will be null
if (redeemScriptBytes == null)
continue;
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our P2SH
continue;
byte[] secret = scriptChunks.get(0).data;
if (secret.length != BTCP2SH.SECRET_LENGTH)
continue;
return secret;
}
}
return null;
}
}

View File

@@ -0,0 +1,448 @@
package org.qortal.crosschain;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.TrustlessSSLSocketFactory;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */
public class ElectrumX {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
private static final int DEFAULT_TCP_PORT = 50001;
private static final int DEFAULT_SSL_PORT = 50002;
private static final int BLOCK_HEADER_LENGTH = 80;
private static final Map<String, ElectrumX> instances = new HashMap<>();
static class Server {
String hostname;
enum ConnectionType { TCP, SSL };
ConnectionType connectionType;
int port;
public Server(String hostname, ConnectionType connectionType, int port) {
this.hostname = hostname;
this.connectionType = connectionType;
this.port = port;
}
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof Server))
return false;
Server otherServer = (Server) other;
return this.connectionType == otherServer.connectionType
&& this.port == otherServer.port
&& this.hostname.equals(otherServer.hostname);
}
@Override
public int hashCode() {
return this.hostname.hashCode() ^ this.port;
}
@Override
public String toString() {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
private Set<Server> servers = new HashSet<>();
private Server currentServer;
private Socket socket;
private Scanner scanner;
private int nextId = 1;
// Constructors
private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) {
case "MAIN":
servers.addAll(Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002),
new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002),
new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002),
new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002),
new Server("electrumx.ml", Server.ConnectionType.SSL, 50002),
new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002),
new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512),
new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002),
new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002),
new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001),
new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002),
new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001)));
break;
case "TEST3":
servers.addAll(Arrays.asList(
new Server("tn.not.fyi", Server.ConnectionType.TCP, 55001),
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
break;
case "REGTEST":
servers.addAll(Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
break;
default:
throw new IllegalArgumentException(String.format("Bitcoin network '%s' unknown", bitcoinNetwork));
}
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
rpc("server.banner");
}
/** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
if (!instances.containsKey(bitcoinNetwork))
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
return instances.get(bitcoinNetwork);
}
// Methods for use by other classes
public Integer getCurrentHeight() {
Object blockObj = this.rpc("blockchain.headers.subscribe");
if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("height"))
return null;
return ((Long) blockJson.get("height")).intValue();
}
public List<byte[]> getBlockHeaders(int startHeight, long count) {
Object blockObj = this.rpc("blockchain.block.headers", startHeight, count);
if (!(blockObj instanceof JSONObject))
return null;
JSONObject blockJson = (JSONObject) blockObj;
if (!blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null;
Long returnedCount = (Long) blockJson.get("count");
String hex = (String) blockJson.get("hex");
byte[] raw = HashCode.fromString(hex).asBytes();
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
return null;
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
for (int i = 0; i < returnedCount; ++i)
rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
return rawBlockHeaders;
}
/** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */
public Long getBalance(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (!(balanceObj instanceof JSONObject))
return null;
JSONObject balanceJson = (JSONObject) balanceObj;
if (!balanceJson.containsKey("confirmed"))
return null;
return (Long) balanceJson.get("confirmed");
}
/** Unspent output info as returned by ElectrumX network. */
public static class UnspentOutput {
public final byte[] hash;
public final int index;
public final int height;
public final long value;
public UnspentOutput(byte[] hash, int index, int height, long value) {
this.hash = hash;
this.index = index;
this.height = height;
this.value = value;
}
}
/** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */
public List<UnspentOutput> getUnspentOutputs(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (!(unspentJson instanceof JSONArray))
return null;
List<UnspentOutput> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : (JSONArray) unspentJson) {
JSONObject unspent = (JSONObject) rawUnspent;
int height = ((Long) unspent.get("height")).intValue();
// We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0)
if (height <= 0)
continue;
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
int outputIndex = ((Long) unspent.get("tx_pos")).intValue();
long value = (Long) unspent.get("value");
unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value));
}
return unspentOutputs;
}
/** Returns raw transaction for passed transaction hash, or null if not found. */
public byte[] getRawTransaction(byte[] txHash) {
Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (!(rawTransactionHex instanceof String))
return null;
return HashCode.fromString((String) rawTransactionHex).asBytes();
}
/** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */
public List<byte[]> getAddressTransactions(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (!(transactionsJson instanceof JSONArray))
return null;
List<byte[]> rawTransactions = new ArrayList<>();
for (Object rawTransactionInfo : (JSONArray) transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions
if (!transactionInfo.containsKey("height"))
continue;
String txHash = (String) transactionInfo.get("tx_hash");
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", txHash);
if (rawTransactionHex == null)
return null;
rawTransactions.add(HashCode.fromString(rawTransactionHex).asBytes());
}
return rawTransactions;
}
/** Returns true if raw transaction successfully broadcast. */
public boolean broadcastTransaction(byte[] transactionBytes) {
Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
if (rawBroadcastResult == null)
return false;
// If result is a String, then it is simply transaction hash.
// Otherwise result is JSON and probably contains error info instead.
return rawBroadcastResult instanceof String;
}
// Class-private utility methods
/** Query current server for its list of peer servers, and return those we can parse. */
private Set<Server> serverPeersSubscribe() {
Set<Server> newServers = new HashSet<>();
Object peers = this.connectedRpc("server.peers.subscribe");
if (!(peers instanceof JSONArray))
return newServers;
for (Object rawPeer : (JSONArray) peers) {
JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3)
continue;
String hostname = (String) peer.get(1);
JSONArray features = (JSONArray) peer.get(2);
for (Object rawFeature : features) {
String feature = (String) rawFeature;
Server.ConnectionType connectionType = null;
int port = -1;
switch (feature.charAt(0)) {
case 's':
connectionType = Server.ConnectionType.SSL;
port = DEFAULT_SSL_PORT;
break;
case 't':
connectionType = Server.ConnectionType.TCP;
port = DEFAULT_TCP_PORT;
break;
}
if (connectionType == null)
continue;
// Possible non-default port?
if (feature.length() > 1)
try {
port = Integer.parseInt(feature.substring(1));
} catch (NumberFormatException e) {
// no good
continue; // for-loop above
}
Server newServer = new Server(hostname, connectionType, port);
newServers.add(newServer);
}
}
return newServers;
}
/** Return output from RPC call, with automatic reconnection to different server if needed. */
private synchronized Object rpc(String method, Object...params) {
while (haveConnection()) {
Object response = connectedRpc(method, params);
if (response != null)
return response;
this.currentServer = null;
try {
this.socket.close();
} catch (IOException e) {
/* ignore */
}
this.scanner = null;
}
return null;
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() {
if (this.currentServer != null)
return true;
List<Server> remainingServers = new ArrayList<>(this.servers);
while (!remainingServers.isEmpty()) {
Server server = remainingServers.remove(RANDOM.nextInt(remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port);
int timeout = 5000; // ms
this.socket = new Socket();
this.socket.connect(endpoint, timeout);
this.socket.setTcpNoDelay(true);
if (server.connectionType == Server.ConnectionType.SSL) {
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
this.socket = (SSLSocket) factory.createSocket(this.socket, server.hostname, server.port, true);
}
this.scanner = new Scanner(this.socket.getInputStream());
this.scanner.useDelimiter("\n");
// Check connection works by asking for more servers
Set<Server> moreServers = serverPeersSubscribe();
moreServers.removeAll(this.servers);
remainingServers.addAll(moreServers);
this.servers.addAll(moreServers);
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (IOException e) {
// Try another server...
this.socket = null;
this.scanner = null;
}
}
return false;
}
@SuppressWarnings("unchecked")
private Object connectedRpc(String method, Object...params) {
JSONObject requestJson = new JSONObject();
requestJson.put("id", this.nextId++);
requestJson.put("method", method);
JSONArray requestParams = new JSONArray();
requestParams.addAll(Arrays.asList(params));
requestJson.put("params", requestParams);
String request = requestJson.toJSONString() + "\n";
LOGGER.trace(() -> String.format("Request: %s", request));
final String response;
try {
this.socket.getOutputStream().write(request.getBytes());
response = scanner.next();
} catch (IOException | NoSuchElementException e) {
return null;
}
LOGGER.trace(() -> String.format("Response: %s", response));
if (response.isEmpty())
return null;
Object responseObj = JSONValue.parse(response);
if (!(responseObj instanceof JSONObject))
return null;
JSONObject responseJson = (JSONObject) responseObj;
return responseJson.get("result");
}
}

View File

@@ -4,16 +4,23 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.qortal.account.Account;
import org.qortal.block.BlockChain;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
public class Crypto {
public abstract class Crypto {
public static final byte ADDRESS_VERSION = 58;
public static final byte AT_ADDRESS_VERSION = 23;
public static final int SIGNATURE_LENGTH = 64;
public static final int SHARED_SECRET_LENGTH = 32;
public static final byte ADDRESS_VERSION = 58; // Q
public static final byte AT_ADDRESS_VERSION = 23; // A
public static final byte NODE_ADDRESS_VERSION = 53; // N
/**
* Returns 32-byte SHA-256 digest of message passed in input.
@@ -59,24 +66,29 @@ public class Crypto {
return Bytes.concat(digest, digest);
}
@SuppressWarnings("deprecation")
/** Returns RMD160(SHA256(data)) */
public static byte[] hash160(byte[] data) {
byte[] interim = digest(data);
try {
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
return md160.digest(interim);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RIPEMD160 message digest not available");
}
}
private static String toAddress(byte addressVersion, byte[] input) {
// SHA2-256 input to create new data and of known size
byte[] inputHash = digest(input);
// Use RIPEMD160 to create shorter address
if (BlockChain.getInstance().getUseBrokenMD160ForAddresses()) {
// Legacy BROKEN MD160
BrokenMD160 brokenMD160 = new BrokenMD160();
inputHash = brokenMD160.digest(inputHash);
} else {
// Use legit MD160
try {
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
inputHash = md160.digest(inputHash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RIPEMD160 message digest not available");
}
// Use legit MD160
try {
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
inputHash = md160.digest(inputHash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RIPEMD160 message digest not available");
}
// Create address data using above hash and addressVersion (prepended)
@@ -104,18 +116,23 @@ public class Crypto {
return toAddress(AT_ADDRESS_VERSION, signature);
}
public static String toNodeAddress(byte[] publicKey) {
return toAddress(NODE_ADDRESS_VERSION, publicKey);
}
public static boolean isValidAddress(String address) {
return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION);
}
public static boolean isValidAddress(byte[] addressBytes) {
return areValidTypedAddressBytes(addressBytes, ADDRESS_VERSION, AT_ADDRESS_VERSION);
}
public static boolean isValidAtAddress(String address) {
return isValidTypedAddress(address, AT_ADDRESS_VERSION);
}
private static boolean isValidTypedAddress(String address, byte...addressVersions) {
if (addressVersions == null || addressVersions.length == 0)
return false;
byte[] addressBytes;
try {
@@ -125,6 +142,13 @@ public class Crypto {
return false;
}
return areValidTypedAddressBytes(addressBytes, addressVersions);
}
private static boolean areValidTypedAddressBytes(byte[] addressBytes, byte...addressVersions) {
if (addressVersions == null || addressVersions.length == 0)
return false;
// Check address length
if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH)
return false;
@@ -142,4 +166,33 @@ public class Crypto {
return false;
}
public static boolean verify(byte[] publicKey, byte[] signature, byte[] message) {
try {
return Ed25519.verify(signature, 0, publicKey, 0, message, 0, message.length);
} catch (Exception e) {
return false;
}
}
public static byte[] sign(Ed25519PrivateKeyParameters edPrivateKeyParams, byte[] message) {
byte[] signature = new byte[SIGNATURE_LENGTH];
edPrivateKeyParams.sign(Ed25519.Algorithm.Ed25519, edPrivateKeyParams.generatePublicKey(), null, message, 0, message.length, signature, 0);
return signature;
}
public static byte[] getSharedSecret(byte[] privateKey, byte[] publicKey) {
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(privateKey);
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
return sharedSecret;
}
}

View File

@@ -1,83 +1,112 @@
package org.qortal.crypto;
import com.google.common.primitives.Bytes;
import java.nio.ByteBuffer;
public class MemoryPoW {
public static final int WORK_BUFFER_LENGTH = 4 * 1024 * 1024;
private static final int WORK_BUFFER_LENGTH_MASK = WORK_BUFFER_LENGTH - 1;
private static final int HASH_LENGTH = 32;
private static final int HASH_LENGTH_MASK = HASH_LENGTH - 1;
public static Integer compute(byte[] data, int start, int range, int difficulty) {
if (range < 1)
throw new IllegalArgumentException("range must be at least 1");
if (difficulty < 1)
throw new IllegalArgumentException("difficulty must be at least 1");
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
assert hash.length == HASH_LENGTH;
long[] longHash = new long[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
longHash[0] = byteBuffer.getLong();
longHash[1] = byteBuffer.getLong();
longHash[2] = byteBuffer.getLong();
longHash[3] = byteBuffer.getLong();
byteBuffer = null;
byte[] perturbedHash = new byte[HASH_LENGTH];
byte[] workBuffer = new byte[WORK_BUFFER_LENGTH];
byte[] bufferHash = new byte[HASH_LENGTH];
int longBufferLength = workBufferLength / 8;
long[] workBuffer = new long[longBufferLength];
long[] state = new long[4];
long seed = 8682522807148012L;
long seedMultiplier = 1181783497276652981L;
// For each nonce...
for (int nonce = start; nonce < start + range; ++nonce) {
// Perturb hash using nonce
int temp = nonce;
for (int hi = 0; hi < HASH_LENGTH; ++hi) {
perturbedHash[hi] = (byte) (hash[hi] ^ (temp & 0xff));
temp >>>= 1;
int nonce = -1;
long result = 0;
do {
++nonce;
seed *= seedMultiplier; // per nonce
state[0] = longHash[0] ^ seed;
state[1] = longHash[1] ^ seed;
state[2] = longHash[2] ^ seed;
state[3] = longHash[3] ^ seed;
// Fill work buffer with random
for (int i = 0; i < workBuffer.length; ++i)
workBuffer[i] = xoshiro256p(state);
// Random bounce through whole buffer
result = workBuffer[0];
for (int i = 0; i < 1024; ++i) {
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
result ^= workBuffer[index];
}
// Fill large working memory buffer using hash, further perturbing as we go
int wanderingBufferOffset = 0;
byte ch = 0;
// Return if final value > difficulty
} while (Long.numberOfLeadingZeros(result) < difficulty);
int hashOffset = 0;
return nonce;
}
for (int workBufferOffset = 0; workBufferOffset < WORK_BUFFER_LENGTH; workBufferOffset += HASH_LENGTH) {
System.arraycopy(perturbedHash, 0, workBuffer, workBufferOffset, HASH_LENGTH);
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
hashOffset = ++hashOffset & HASH_LENGTH_MASK;
long[] longHash = new long[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
longHash[0] = byteBuffer.getLong();
longHash[1] = byteBuffer.getLong();
longHash[2] = byteBuffer.getLong();
longHash[3] = byteBuffer.getLong();
byteBuffer = null;
ch += perturbedHash[hashOffset];
int longBufferLength = workBufferLength / 8;
long[] workBuffer = new long[longBufferLength];
long[] state = new long[4];
for (byte hi = 0; hi < HASH_LENGTH; ++hi) {
byte hashByte = perturbedHash[hi];
wanderingBufferOffset = (wanderingBufferOffset << 3) ^ (hashByte & 0xff);
long seed = 8682522807148012L;
long seedMultiplier = 1181783497276652981L;
perturbedHash[hi] = (byte) (hashByte ^ (ch + hi));
}
for (int i = 0; i <= nonce; ++i)
seed *= seedMultiplier;
workBuffer[wanderingBufferOffset & WORK_BUFFER_LENGTH_MASK] ^= 0xAA;
state[0] = longHash[0] ^ seed;
state[1] = longHash[1] ^ seed;
state[2] = longHash[2] ^ seed;
state[3] = longHash[3] ^ seed;
// final int finalWanderingBufferOffset = wanderingBufferOffset & WORK_BUFFER_LENGTH_MASK;
// System.out.println(String.format("wanderingBufferOffset: 0x%08x / 0x%08x - %02d%%", finalWanderingBufferOffset, WORK_BUFFER_LENGTH, finalWanderingBufferOffset * 100 / WORK_BUFFER_LENGTH));
}
// Fill work buffer with random
for (int i = 0; i < workBuffer.length; ++i)
workBuffer[i] = xoshiro256p(state);
Bytes.reverse(workBuffer);
// bufferHash = Crypto.digest(workBuffer);
System.arraycopy(workBuffer, 0, bufferHash, 0, HASH_LENGTH);
int hi = 0;
for (hi = 0; hi < difficulty; ++hi)
if (bufferHash[hi] != 0)
break;
if (hi == difficulty)
return nonce;
Thread.yield();
// Random bounce through whole buffer
long result = workBuffer[0];
for (int i = 0; i < 1024; ++i) {
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
result ^= workBuffer[index];
}
return null;
return Long.numberOfLeadingZeros(result) >= difficulty;
}
private static final long xoshiro256p(long[] state) {
final long result = state[0] + state[3];
final long temp = state[1] << 17;
state[2] ^= state[0];
state[3] ^= state[1];
state[1] ^= state[2];
state[0] ^= state[3];
state[2] ^= temp;
state[3] = (state[3] << 45) | (state[3] >>> (64 - 45)); // rol64(s[3], 45);
return result;
}
}

View File

@@ -0,0 +1,42 @@
package org.qortal.crypto;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public abstract class TrustlessSSLSocketFactory {
// Create a trust manager that does not validate certificate chains
private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
}
};
// Install the all-trusting trust manager
private static final SSLContext sc;
static {
try {
sc = SSLContext.getInstance("SSL");
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static SSLSocketFactory getSocketFactory() {
return sc.getSocketFactory();
}
}

View File

@@ -1,18 +1,21 @@
package org.qortal.data;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class PaymentData {
// Properties
private String recipient;
private long assetId;
private BigDecimal amount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long amount;
// Constructors
@@ -20,7 +23,7 @@ public class PaymentData {
protected PaymentData() {
}
public PaymentData(String recipient, long assetId, BigDecimal amount) {
public PaymentData(String recipient, long assetId, long amount) {
this.recipient = recipient;
this.assetId = assetId;
this.amount = amount;
@@ -36,7 +39,7 @@ public class PaymentData {
return this.assetId;
}
public BigDecimal getAmount() {
public long getAmount() {
return this.amount;
}

View File

@@ -1,9 +1,11 @@
package org.qortal.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.utils.Amounts;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@@ -12,7 +14,9 @@ public class AccountBalanceData {
// Properties
private String address;
private long assetId;
private BigDecimal balance;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long balance;
// Not always present:
private Integer height;
@@ -24,19 +28,19 @@ public class AccountBalanceData {
protected AccountBalanceData() {
}
public AccountBalanceData(String address, long assetId, BigDecimal balance) {
public AccountBalanceData(String address, long assetId, long balance) {
this.address = address;
this.assetId = assetId;
this.balance = balance;
}
public AccountBalanceData(String address, long assetId, BigDecimal balance, int height) {
public AccountBalanceData(String address, long assetId, long balance, int height) {
this(address, assetId, balance);
this.height = height;
}
public AccountBalanceData(String address, long assetId, BigDecimal balance, String assetName) {
public AccountBalanceData(String address, long assetId, long balance, String assetName) {
this(address, assetId, balance);
this.assetName = assetName;
@@ -52,11 +56,11 @@ public class AccountBalanceData {
return this.assetId;
}
public BigDecimal getBalance() {
public long getBalance() {
return this.balance;
}
public void setBalance(BigDecimal balance) {
public void setBalance(long balance) {
this.balance = balance;
}
@@ -68,4 +72,8 @@ public class AccountBalanceData {
return this.assetName;
}
public String toString() {
return String.format("%s has %s %s [assetId %d]", this.address, Amounts.prettyAmount(this.balance), (assetName != null ? assetName : ""), assetId);
}
}

View File

@@ -2,10 +2,8 @@ package org.qortal.data.account;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.crypto.Crypto;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -16,14 +14,17 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
public class MintingAccountData {
// Properties
// Never actually displayed by API
@Schema(hidden = true)
@XmlTransient
protected byte[] privateKey;
// Not always present - used by API if not null
@XmlTransient
@Schema(hidden = true)
// Read-only by API, we never ask for it as input
@Schema(accessMode = AccessMode.READ_ONLY)
protected byte[] publicKey;
// Not always present - used by API if not null
protected String mintingAccount;
protected String recipientAccount;
protected String address;
@@ -34,17 +35,17 @@ public class MintingAccountData {
protected MintingAccountData() {
}
public MintingAccountData(byte[] privateKey) {
public MintingAccountData(byte[] privateKey, byte[] publicKey) {
this.privateKey = privateKey;
this.publicKey = PrivateKeyAccount.toPublicKey(privateKey);
this.publicKey = publicKey;
}
public MintingAccountData(byte[] privateKey, RewardShareData rewardShareData) {
this(privateKey);
public MintingAccountData(MintingAccountData srcMintingAccountData, RewardShareData rewardShareData) {
this(srcMintingAccountData.privateKey, srcMintingAccountData.publicKey);
if (rewardShareData != null) {
this.recipientAccount = rewardShareData.getRecipient();
this.mintingAccount = Crypto.toAddress(rewardShareData.getMinterPublicKey());
this.mintingAccount = rewardShareData.getMinter();
} else {
this.address = Crypto.toAddress(this.publicKey);
}
@@ -56,8 +57,6 @@ public class MintingAccountData {
return this.privateKey;
}
@XmlElement(name = "publicKey")
@Schema(accessMode = AccessMode.READ_ONLY)
public byte[] getPublicKey() {
return this.publicKey;
}

View File

@@ -1,18 +1,23 @@
package org.qortal.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class QortFromQoraData {
// Properties
private String address;
// Not always present:
private BigDecimal finalQortFromQora;
// Not always present
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long finalQortFromQora;
// Not always present
private Integer finalBlockHeight;
// Constructors
@@ -21,7 +26,7 @@ public class QortFromQoraData {
protected QortFromQoraData() {
}
public QortFromQoraData(String address, BigDecimal finalQortFromQora, Integer finalBlockHeight) {
public QortFromQoraData(String address, Long finalQortFromQora, Integer finalBlockHeight) {
this.address = address;
this.finalQortFromQora = finalQortFromQora;
this.finalBlockHeight = finalBlockHeight;
@@ -33,11 +38,11 @@ public class QortFromQoraData {
return this.address;
}
public BigDecimal getFinalQortFromQora() {
public Long getFinalQortFromQora() {
return this.finalQortFromQora;
}
public void setFinalQortFromQora(BigDecimal finalQortFromQora) {
public void setFinalQortFromQora(Long finalQortFromQora) {
this.finalQortFromQora = finalQortFromQora;
}

View File

@@ -5,8 +5,12 @@ import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crypto.Crypto;
import org.qortal.utils.Base58;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@@ -14,9 +18,17 @@ public class RewardShareData {
// Properties
private byte[] minterPublicKey;
// "minter" is called "mintingAccount" instead
@XmlTransient
@Schema(hidden = true)
private String minter;
private String recipient;
private byte[] rewardSharePublicKey;
private BigDecimal sharePercent;
@XmlJavaTypeAdapter(value = org.qortal.api.RewardSharePercentTypeAdapter.class)
private int sharePercent;
// Constructors
@@ -25,8 +37,9 @@ public class RewardShareData {
}
// Used when fetching from repository
public RewardShareData(byte[] minterPublicKey, String recipient, byte[] rewardSharePublicKey, BigDecimal sharePercent) {
public RewardShareData(byte[] minterPublicKey, String minter, String recipient, byte[] rewardSharePublicKey, int sharePercent) {
this.minterPublicKey = minterPublicKey;
this.minter = minter;
this.recipient = recipient;
this.rewardSharePublicKey = rewardSharePublicKey;
this.sharePercent = sharePercent;
@@ -38,6 +51,10 @@ public class RewardShareData {
return this.minterPublicKey;
}
public String getMinter() {
return this.minter;
}
public String getRecipient() {
return this.recipient;
}
@@ -46,13 +63,25 @@ public class RewardShareData {
return this.rewardSharePublicKey;
}
public BigDecimal getSharePercent() {
/** Returns share percent scaled by 100. i.e. 12.34% is represented by 1234 */
public int getSharePercent() {
return this.sharePercent;
}
// Some JAXB/API-related getters
@XmlElement(name = "mintingAccount")
public String getMintingAccount() {
return Crypto.toAddress(this.minterPublicKey);
return this.minter;
}
// For debugging
public String toString() {
if (this.minter.equals(this.recipient))
return String.format("Minter/recipient: %s, reward-share public key: %s", this.minter, Base58.encode(this.rewardSharePublicKey));
else
return String.format("Minter: %s, recipient: %s (%s %%), reward-share public key: %s", this.minter, this.recipient, BigDecimal.valueOf(this.sharePercent, 2), Base58.encode(this.rewardSharePublicKey));
}
}

View File

@@ -20,19 +20,26 @@ public class AssetData {
private String data;
private boolean isUnspendable;
private int creationGroupId;
// No need to expose this via API
@XmlTransient
@Schema(hidden = true)
private byte[] reference;
// For internal use only
@XmlTransient
@Schema(hidden = true)
private String reducedAssetName;
// Constructors
// necessary for JAX-RS serialization
// necessary for JAXB serialization
protected AssetData() {
}
// NOTE: key is Long, not long, because it can be null if asset ID/key not yet assigned.
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable, int creationGroupId, byte[] reference) {
public AssetData(Long assetId, String owner, String name, String description, long quantity, boolean isDivisible,
String data, boolean isUnspendable, int creationGroupId, byte[] reference, String reducedAssetName) {
this.assetId = assetId;
this.owner = owner;
this.name = name;
@@ -43,11 +50,13 @@ public class AssetData {
this.isUnspendable = isUnspendable;
this.creationGroupId = creationGroupId;
this.reference = reference;
this.reducedAssetName = reducedAssetName;
}
// New asset with unassigned assetId
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data, boolean isUnspendable, int creationGroupId, byte[] reference) {
this(null, owner, name, description, quantity, isDivisible, data, isUnspendable, creationGroupId, reference);
public AssetData(String owner, String name, String description, long quantity, boolean isDivisible, String data,
boolean isUnspendable, int creationGroupId, byte[] reference, String reducedAssetName) {
this(null, owner, name, description, quantity, isDivisible, data, isUnspendable, creationGroupId, reference, reducedAssetName);
}
// Getters/Setters
@@ -84,7 +93,7 @@ public class AssetData {
return this.quantity;
}
public boolean getIsDivisible() {
public boolean isDivisible() {
return this.isDivisible;
}
@@ -96,7 +105,7 @@ public class AssetData {
this.data = data;
}
public boolean getIsUnspendable() {
public boolean isUnspendable() {
return this.isUnspendable;
}
@@ -112,4 +121,12 @@ public class AssetData {
this.reference = reference;
}
public String getReducedAssetName() {
return this.reducedAssetName;
}
public void setReducedAssetName(String reducedAssetName) {
this.reducedAssetName = reducedAssetName;
}
}

Some files were not shown because too many files have changed in this diff Show More