Java tutorial
/* * SparkBit * * Copyright 2014 Coin Sciences Ltd * * Licensed under the MIT license (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://opensource.org/licenses/mit-license.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.sparkbit.jsonrpc; import org.multibit.controller.bitcoin.BitcoinController; import org.sparkbit.jsonrpc.autogen.*; import java.util.List; import java.util.ArrayList; import org.multibit.model.bitcoin.WalletData; import com.google.bitcoin.core.*; //import java.util.HashMap; import java.util.Iterator; //import org.apache.commons.codec.digest.DigestUtils; import org.coinspark.protocol.CoinSparkAddress; import org.coinspark.wallet.CSAsset; import org.coinspark.wallet.CSAssetDatabase; import org.coinspark.wallet.CSEventBus; import org.coinspark.wallet.CSEventType; import org.multibit.utils.CSMiscUtils; import org.coinspark.protocol.CoinSparkAssetRef; import org.multibit.model.bitcoin.BitcoinModel; import org.multibit.model.bitcoin.WalletAddressBookData; import org.multibit.model.bitcoin.WalletInfoData; import org.multibit.file.FileHandler; import java.io.*; import org.multibit.file.BackupManager; import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.script.Script; import com.google.bitcoin.wallet.DefaultCoinSelector; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Map; import org.coinspark.protocol.CoinSparkGenesis; import org.joda.time.DateTime; import org.joda.time.LocalDateTime; import org.sparkbit.SBEvent; import org.sparkbit.SBEventType; import org.multibit.network.MultiBitService; import org.multibit.store.MultiBitWalletVersion; import java.util.Date; import org.multibit.file.WalletSaveException; import static org.multibit.model.bitcoin.WalletAssetComboBoxModel.NUMBER_OF_CONFIRMATIONS_TO_SEND_ASSET_THRESHOLD; import java.text.SimpleDateFormat; import static org.multibit.network.MultiBitService.WALLET_SUFFIX; import org.sparkbit.utils.FileNameCleaner; import org.apache.commons.io.FilenameUtils; import java.util.TimeZone; import org.multibit.viewsystem.swing.action.ExitAction; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.Executors; import org.sparkbit.SparkBitMapDB; import org.apache.commons.io.FileDeleteStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; import java.util.Arrays; import java.util.Set; import org.coinspark.wallet.CSBalance; import org.coinspark.wallet.CSTransactionOutput; import java.util.HashSet; import org.coinspark.core.CSExceptions; import org.coinspark.protocol.*; /** * For now, synchronized access to commands which mutate */ public class SparkBitJSONRPCServiceImpl implements sparkbit { private static final Logger log = LoggerFactory.getLogger(SparkBitJSONRPCServiceImpl.class); // Limit on the number of addresses you can create in one call public static final int CREATE_ADDRESSES_LIMIT = 100; // How many milliseconds before we shutdown via ExitAction public static final int SHUTDOWN_DELAY = JettyEmbeddedServer.GRACEFUL_SHUTDOWN_PERIOD + 1000; private BitcoinController controller; // private MultiBitFrame mainFrame; // private ConcurrentHashMap<String,String> walletFilenameMap; private Timer stopTimer; public SparkBitJSONRPCServiceImpl() { this.controller = JSONRPCController.INSTANCE.getBitcoinController(); // this.mainFrame = JSONRPCController.INSTANCE.getMultiBitFrame(); // walletFilenameMap = new ConcurrentHashMap<>(); // updateWalletFilenameMap(); } // Quit after a delay so we can return true to JSON-RPC client. @Override public Boolean stop() throws com.bitmechanic.barrister.RpcException { log.info("STOP"); log.info("Shutting down JSON-RPC server, will wait " + SHUTDOWN_DELAY + " milliseconds before instructing SparkBit to exit."); final BitcoinController myController = this.controller; stopTimer = new Timer(); stopTimer.schedule(new TimerTask() { @Override public void run() { ExitAction exitAction = new ExitAction(myController, null); exitAction.setBitcoinController(myController); exitAction.actionPerformed(null); } }, SHUTDOWN_DELAY); // Signal the server to stop Executors.newSingleThreadExecutor().execute(new Runnable() { @Override public void run() { JSONRPCController.INSTANCE.stopServer(); } }); return true; } /* Get status on a per wallet basis. */ @Override public JSONRPCStatusResponse getstatus() throws com.bitmechanic.barrister.RpcException { log.info("GET STATUS"); // boolean replayTaskRunning = ReplayManager.INSTANCE.getCurrentReplayTask()!=null ; // boolean regularDownloadRunning = ReplayManager.isRegularDownloadRunning(); // boolean synced = !regularDownloadRunning && !replayTaskRunning; // A regular download can be running in the background, because there is a new block, // and thus we are behind by one block, but the UI will still show that we synced. // We will consider ourselves to be out of sync if we are two or more blocks behind our peers. // PeerGroup pg = controller.getMultiBitService().getPeerGroup(); // if (pg!=null) { // Peer peer = pg.getDownloadPeer(); // if (peer != null) { // int n = peer.getPeerBlockHeightDifference(); // if (synced && n>=2) { // synced = false; // } // } // } int mostCommonChainHeight = controller.getMultiBitService().getPeerGroup().getMostCommonChainHeight(); //int bestChainHeight = controller.getMultiBitService().getChain().getBestChainHeight(); List<WalletData> perWalletModelDataList = controller.getModel().getPerWalletModelDataList(); List<JSONRPCWalletStatus> wallets = new ArrayList<JSONRPCWalletStatus>(); if (perWalletModelDataList != null) { for (WalletData wd : perWalletModelDataList) { Wallet w = wd.getWallet(); long lastSeenBlock = w.getLastBlockSeenHeight(); // getLastBlockSeenHeight() returns -1 if the wallet doesn't have this data yet if (lastSeenBlock == -1) lastSeenBlock = mostCommonChainHeight; boolean synced = (lastSeenBlock == mostCommonChainHeight); // log.debug(">>>> *** description = " + wd.getWalletDescription()); // log.debug(">>>> last block = " + lastSeenBlock); // log.debug(">>>> mostCommonChainHeight = " + mostCommonChainHeight); // log.debug(">>>> replay UUID = " + wd.getReplayTaskUUID()); // log.debug(">>>> busy key = " + wd.getBusyTaskKey()); if (wd.getReplayTaskUUID() != null) { synced = false; } else if (wd.isBusy()) { String key = wd.getBusyTaskKey(); if (key.equals("multiBitDownloadListener.downloadingText") || key.equals("singleWalletPanel.waiting.text")) { synced = false; } } String filename = wd.getWalletFilename(); String base = FilenameUtils.getBaseName(filename); JSONRPCWalletStatus ws = new JSONRPCWalletStatus(base, synced, lastSeenBlock); wallets.add(ws); } } String versionNumber = controller.getLocaliser().getVersionNumber(); int numConnectedPeers = controller.getMultiBitService().getPeerGroup().numConnectedPeers(); boolean isTestNet = controller.getMultiBitService().isTestNet3(); JSONRPCStatusResponse resp = new JSONRPCStatusResponse(); resp.setVersion(versionNumber); resp.setConnections((long) numConnectedPeers); resp.setTestnet(isTestNet); JSONRPCWalletStatus[] x = wallets.toArray(new JSONRPCWalletStatus[0]); resp.setWallets(x); return resp; } @Override public String[] listwallets() throws com.bitmechanic.barrister.RpcException { log.info("LIST WALLETS"); List<WalletData> perWalletModelDataList = controller.getModel().getPerWalletModelDataList(); List<String> names = new ArrayList<String>(); if (perWalletModelDataList != null) { for (WalletData loopPerWalletModelData : perWalletModelDataList) { String filename = loopPerWalletModelData.getWalletFilename(); String base = FilenameUtils.getBaseName(filename); names.add(base); // store/update local cache //walletFilenameMap.put(digest, filename); } } String[] resultArray = names.toArray(new String[0]); return resultArray; } @Override public synchronized Boolean createwallet(String name) throws com.bitmechanic.barrister.RpcException { log.info("CREATE WALLET"); log.info("wallet name = " + name); boolean isNameSane = sanityCheckName(name); if (!isNameSane) { JSONRPCError.WALLET_NAME_BAD_CHARS.raiseRpcException(); } if (name.startsWith("-") || name.startsWith("_")) { JSONRPCError.WALLET_NAME_BEGINS_WITH_SYMBOL.raiseRpcException(); } String newWalletFilename = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory() + File.separator + name + MultiBitService.WALLET_SUFFIX; File newWalletFile = new File(newWalletFilename); if (newWalletFile.exists()) { JSONRPCError.WALLET_ID_ALREADY_EXISTS.raiseRpcException(); } LocalDateTime dt = new DateTime().toLocalDateTime(); String description = name + " (jsonrpc " + dt.toString("YYYY-MM-DD HH:mm" + ")"); //.SSS"); // Create a new wallet - protobuf.2 initially for backwards compatibility. try { Wallet newWallet = new Wallet(this.controller.getModel().getNetworkParameters()); ECKey newKey = new ECKey(); newWallet.addKey(newKey); WalletData perWalletModelData = new WalletData(); /* CoinSpark START */ // set default address label for first key WalletInfoData walletInfo = new WalletInfoData(newWalletFilename, newWallet, MultiBitWalletVersion.PROTOBUF); NetworkParameters networkParams = this.controller.getModel().getNetworkParameters(); Address defaultReceivingAddress = newKey.toAddress(networkParams); walletInfo.getReceivingAddresses() .add(new WalletAddressBookData("Default Address", defaultReceivingAddress.toString())); perWalletModelData.setWalletInfo(walletInfo); /* CoinSpark END */ perWalletModelData.setWallet(newWallet); perWalletModelData.setWalletFilename(newWalletFilename); perWalletModelData.setWalletDescription(description); this.controller.getFileHandler().savePerWalletModelData(perWalletModelData, true); // Start using the new file as the wallet. this.controller.addWalletFromFilename(newWalletFile.getAbsolutePath()); // We could select the new wallet if we wanted to. //this.controller.getModel().setActiveWalletByFilename(newWalletFilename); //controller.getModel().setUserPreference(BitcoinModel.GRAB_FOCUS_FOR_ACTIVE_WALLET, "true"); // Save the user properties to disk. FileHandler.writeUserPreferences(this.controller); //log.debug("User preferences with new wallet written successfully"); // Backup the wallet and wallet info. BackupManager.INSTANCE.backupPerWalletModelData(controller.getFileHandler(), perWalletModelData); controller.fireRecreateAllViews(true); controller.fireDataChangedUpdateNow(); } catch (Exception e) { JSONRPCError.CREATE_WALLET_FAILED.raiseRpcException(); //JSONRPCError.throwAsRpcException("Could not create wallet", e); } // updateWalletFilenameMap(); return true; } @Override public synchronized Boolean deletewallet(String walletID) throws com.bitmechanic.barrister.RpcException { log.info("DELETE WALLET"); log.info("wallet name = " + walletID); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } // Are there any asset or btc balances? Do not allow deleting wallet if any balance exists? Map<Integer, Wallet.CoinSpark.AssetBalance> map = w.CS.getAllAssetBalances(); for (Wallet.CoinSpark.AssetBalance ab : map.values()) { if (ab.total.compareTo(BigInteger.ZERO) > 0) { JSONRPCError.DELETE_WALLET_NOT_EMPTY.raiseRpcException(); } } String filename = getFullPathForWalletName(walletID); final WalletData wd = this.controller.getModel().getPerWalletModelDataByWalletFilename(filename); WalletInfoData winfo = wd.getWalletInfo(); if (wd.isBusy()) { JSONRPCError.WALLEY_IS_BUSY.raiseRpcException(); } // Deleting a wallet even if not currently active, causes list of wallets to be unselected // so we need to keep track of what was active befor eremoval String activeFilename = this.controller.getModel().getActiveWalletFilename(); if (wd.getWalletFilename().equals(activeFilename)) { activeFilename = null; } // Unhook it from the PeerGroup. this.controller.getMultiBitService().getPeerGroup().removeWallet(w); // Remove it from the model. this.controller.getModel().remove(wd); // Perform delete if possible etc. FileHandler fileHandler = this.controller.getFileHandler(); try { fileHandler.deleteWalletAndWalletInfo(wd); // Delete .cs String csassets = filename + ".csassets"; String csbalances = filename + ".csbalances"; File f = new File(csassets); if (f.exists()) { if (!f.delete()) { //log.error(">>>> Asset DB: Cannot delete"); } } f = new File(csbalances); if (f.exists()) { if (!f.delete()) { //log.error(">>>> Balances DB: Cannot delete"); } } String cslog = filename + ".cslog"; f = new File(cslog); if (f.exists()) { if (!f.delete()) { // log.error(">>>> CS Log File: Cannot delete"); } } // Delete cached contracts String csfiles = filename + ".csfiles"; f = new File(csfiles); if (f.exists()) { FileDeleteStrategy.FORCE.delete(f); } // Delete the backup folder and cached contracts String backupFolderPath = BackupManager.INSTANCE .calculateTopLevelBackupDirectoryName(new File(filename)); f = new File(backupFolderPath); if (f.exists()) { FileDeleteStrategy.FORCE.delete(f); } } catch (Exception e) { JSONRPCError.throwAsRpcException("Error deleting wallet files", e); } if (!winfo.isDeleted()) { JSONRPCError.throwAsRpcException("Wallet was not deleted. Reason unknown."); } // Set the new Wallet to be the old active wallet, or the first wallet if (activeFilename != null) { this.controller.getModel().setActiveWalletByFilename(activeFilename); } else if (!this.controller.getModel().getPerWalletModelDataList().isEmpty()) { WalletData firstPerWalletModelData = this.controller.getModel().getPerWalletModelDataList().get(0); this.controller.getModel().setActiveWalletByFilename(firstPerWalletModelData.getWalletFilename()); } controller.fireRecreateAllViews(true); // updateWalletFilenameMap(); return true; } /* Synchronized access: clear and recreate the wallet filename map. */ // private synchronized void updateWalletFilenameMap() { // walletFilenameMap.clear(); // List<WalletData> perWalletModelDataList = controller.getModel().getPerWalletModelDataList(); // if (perWalletModelDataList != null) { // for (WalletData loopPerWalletModelData : perWalletModelDataList) { // String filename = loopPerWalletModelData.getWalletFilename(); // String id = loopPerWalletModelData.getWalletDescription(); // walletFilenameMap. put(id, filename); //// //// String filename = loopPerWalletModelData.getWalletFilename(); //// String digest = DigestUtils.md5Hex(filename); //// walletFilenameMap. put(digest, filename); // } // } // // } /* Get full path for wallet given it's name */ private String getFullPathForWalletName(String name) { String filename = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory() + File.separator + name + WALLET_SUFFIX; return filename; } /* Check to see if a name gets cleaned or not because it contains characters bad for a filename */ private boolean sanityCheckName(String name) { String cleaned = FileNameCleaner.cleanFileNameForWallet(name); return (cleaned.equals(name)); } private Wallet getWalletForWalletName(String walletID) { Wallet w = null; String filename = getFullPathForWalletName(walletID); if (filename != null) { WalletData wd = controller.getModel().getPerWalletModelDataByWalletFilename(filename); if (wd != null) { w = wd.getWallet(); } } return w; } // Helper function private boolean isAssetRefValid(String s) { if (s != null) { s = s.trim(); CoinSparkAssetRef assetRef = new CoinSparkAssetRef(); if (assetRef.decode(s)) { return true; } } return false; } private CSAsset getAssetForAssetRefString(Wallet w, String assetRef) { CSAssetDatabase db = w.CS.getAssetDB(); int[] assetIDs = w.CS.getAssetIDs(); if (assetIDs != null) { for (int id : assetIDs) { CSAsset asset = db.getAsset(id); if (asset != null) { //CoinSparkAssetRef ref = asset.getAssetReference(); String s = CSMiscUtils.getHumanReadableAssetRef(asset); if (s.equals(assetRef)) { return asset; } } } } return null; } @Override public synchronized Boolean setassetvisible(String walletID, String assetRef, Boolean visibility) throws com.bitmechanic.barrister.RpcException { log.info("SET ASSET VISIBILITY"); log.info("wallet name = " + walletID); log.info("asset ref = " + assetRef); log.info("visibility = " + visibility); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } CSAsset asset = getAssetForAssetRefString(w, assetRef); if (asset == null) { if (isAssetRefValid(assetRef)) { JSONRPCError.ASSETREF_NOT_FOUND.raiseRpcException(); } else { JSONRPCError.ASSETREF_INVALID.raiseRpcException(); } } else { asset.setVisibility(visibility); CSEventBus.INSTANCE.postAsyncEvent(CSEventType.ASSET_VISIBILITY_CHANGED, asset.getAssetID()); } return true; } @Override public synchronized Boolean addasset(String walletID, String assetRefString) throws com.bitmechanic.barrister.RpcException { log.info("ADD ASSET"); log.info("wallet name = " + walletID); log.info("asset ref = " + assetRefString); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } String s = assetRefString; if ((s != null) && (s.length() > 0)) { s = s.trim(); // Does the asset already exist? If so, return true. if (getAssetForAssetRefString(w, s) != null) { return true; } //System.out.println("asset ref detected! " + s); CoinSparkAssetRef assetRef = new CoinSparkAssetRef(); if (assetRef.decode(s)) { // Wallet wallet = this.controller.getModel().getActiveWallet(); CSAssetDatabase assetDB = w.CS.getAssetDB(); if (assetDB != null) { CSAsset asset = new CSAsset(assetRef, CSAsset.CSAssetSource.MANUAL); if (assetDB.insertAsset(asset) != null) { //System.out.println("Inserted new asset manually: " + asset); } else { JSONRPCError.throwAsRpcException( "Internal error, assetDB.insertAsset() failed for an unknown reason"); } } } else { JSONRPCError.ASSETREF_INVALID.raiseRpcException(); } } return true; } @Override public Boolean deleteasset(String walletname, String assetRef) throws com.bitmechanic.barrister.RpcException { log.info("DELETE ASSET"); log.info("wallet name = " + walletname); log.info("asset ref = " + assetRef); Wallet w = getWalletForWalletName(walletname); boolean success = false; CSAsset asset = getAssetForAssetRefString(w, assetRef); if (asset != null) { int assetID = asset.getAssetID(); BigInteger x = w.CS.getAssetBalance(assetID).total; boolean canDelete = x.equals(BigInteger.ZERO); // Delete invalid asset if property allows String s = controller.getModel().getUserPreference(BitcoinModel.CAN_DELETE_INVALID_ASSETS); boolean isAssetInvalid = asset.getAssetState() != CSAsset.CSAssetState.VALID; boolean deleteInvalidAsset = false; if (Boolean.TRUE.toString().equals(s) && isAssetInvalid) { deleteInvalidAsset = true; } if (canDelete || deleteInvalidAsset) { success = w.CS.deleteAsset(asset); if (success) { // Note: the event can be fired, but the listener can do nothing if in headless mode. // We want main asset panel to refresh, since there isn't an event fired on manual reset. CSEventBus.INSTANCE.postAsyncEvent(CSEventType.ASSET_DELETED, assetID); } else { JSONRPCError.DELETE_ASSET_FAILED.raiseRpcException(); } } else { if (isAssetInvalid) { JSONRPCError.DELETE_INVALID_ASSET_FAILED.raiseRpcException(); } else { JSONRPCError.DELETE_ASSET_NONZERO_BALANCE.raiseRpcException(); } } } else { if (isAssetRefValid(assetRef)) { JSONRPCError.ASSETREF_NOT_FOUND.raiseRpcException(); } else { JSONRPCError.ASSETREF_INVALID.raiseRpcException(); } } return success; } @Override public synchronized Boolean refreshasset(String walletID, String assetRef) throws com.bitmechanic.barrister.RpcException { log.info("REFRESH ASSET"); log.info("wallet name = " + walletID); log.info("asset ref = " + assetRef); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } CSAsset asset = getAssetForAssetRefString(w, assetRef); if (asset != null) { asset.setRefreshState(); // Note: the event can be fired, but the listener can do nothing if in headless mode. // We want main asset panel to refresh, since there isn't an event fired on manual reset. CSEventBus.INSTANCE.postAsyncEvent(CSEventType.ASSET_UPDATED, asset.getAssetID()); } else { if (isAssetRefValid(assetRef)) { JSONRPCError.ASSETREF_NOT_FOUND.raiseRpcException(); } else { JSONRPCError.ASSETREF_INVALID.raiseRpcException(); } } return true; } @Override public JSONRPCAddressBookEntry[] listaddresses(String walletID) throws com.bitmechanic.barrister.RpcException { log.info("LIST ADDRESSES"); log.info("wallet name = " + walletID); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } List<JSONRPCAddressBookEntry> addresses = new ArrayList<JSONRPCAddressBookEntry>(); String address, sparkAddress, label; String filename = getFullPathForWalletName(walletID); final WalletData wd = this.controller.getModel().getPerWalletModelDataByWalletFilename(filename); final WalletInfoData addressBook = wd.getWalletInfo(); if (addressBook != null) { ArrayList<WalletAddressBookData> receivingAddresses = addressBook.getReceivingAddresses(); if (receivingAddresses != null) { Iterator<WalletAddressBookData> iter = receivingAddresses.iterator(); while (iter.hasNext()) { WalletAddressBookData addressBookData = iter.next(); if (addressBookData != null) { address = addressBookData.getAddress(); label = addressBookData.getLabel(); sparkAddress = CSMiscUtils.convertBitcoinAddressToCoinSparkAddress(address); if (sparkAddress != null) { JSONRPCAddressBookEntry entry = new JSONRPCAddressBookEntry(label, address, sparkAddress); addresses.add(entry); } } } } } JSONRPCAddressBookEntry[] resultArray = addresses.toArray(new JSONRPCAddressBookEntry[0]); return resultArray; } // TODO: Should we remove limit of 100 addresses? @Override public synchronized JSONRPCAddressBookEntry[] createaddresses(String walletID, Long quantity) throws com.bitmechanic.barrister.RpcException { log.info("CREATE ADDRESSES"); log.info("wallet name = " + walletID); log.info("quantity = " + quantity); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } int qty = quantity.intValue(); if (qty <= 0) { JSONRPCError.CREATE_ADDRESS_TOO_FEW.raiseRpcException(); } if (qty > CREATE_ADDRESSES_LIMIT) { JSONRPCError.CREATE_ADDRESS_TOO_MANY.raiseRpcException(); } String filename = getFullPathForWalletName(walletID); final WalletData wd = this.controller.getModel().getPerWalletModelDataByWalletFilename(filename); if (wd.isBusy()) { JSONRPCError.WALLEY_IS_BUSY.raiseRpcException(); } else { wd.setBusy(true); wd.setBusyTaskKey("jsonrpc.busy.createaddress"); this.controller.fireWalletBusyChange(true); } List<JSONRPCAddressBookEntry> addresses = new ArrayList<JSONRPCAddressBookEntry>(); try { List<ECKey> newKeys = new ArrayList<ECKey>(); for (int i = 0; i < qty; i++) { ECKey newKey = new ECKey(); newKeys.add(newKey); } FileHandler fileHandler = this.controller.getFileHandler(); synchronized (wd.getWallet()) { wd.getWallet().addKeys(newKeys); } // Recalculate the bloom filter. if (this.controller.getMultiBitService() != null) { this.controller.getMultiBitService().recalculateFastCatchupAndFilter(); } // Add keys to address book. int n = 1, count = newKeys.size(); for (ECKey newKey : newKeys) { String lastAddressString = newKey.toAddress(this.controller.getModel().getNetworkParameters()) .toString(); LocalDateTime dt = new DateTime().toLocalDateTime(); String label = "Created on " + dt.toString("d MMM y, HH:mm:ss z") + " (" + n++ + " of " + count + ")"; // unlikely address is already present, we don't want to update the label wd.getWalletInfo().addReceivingAddress(new WalletAddressBookData(label, lastAddressString), false); // Create structure for JSON response String sparkAddress = CSMiscUtils.convertBitcoinAddressToCoinSparkAddress(lastAddressString); if (sparkAddress == null) sparkAddress = "Internal error creating CoinSparkAddress from this Bitcoin address"; JSONRPCAddressBookEntry entry = new JSONRPCAddressBookEntry(label, lastAddressString, sparkAddress); addresses.add(entry); } // Backup the wallet and wallet info. BackupManager.INSTANCE.backupPerWalletModelData(fileHandler, wd); } catch (KeyCrypterException e) { JSONRPCError.throwAsRpcException("Create addresses failed with KeyCrypterException", e); } catch (Exception e) { JSONRPCError.throwAsRpcException("Create addresses failed", e); } finally { // Declare that wallet is no longer busy with the task. wd.setBusyTaskKey(null); wd.setBusy(false); this.controller.fireWalletBusyChange(false); } CSEventBus.INSTANCE.postAsync(new SBEvent(SBEventType.ADDRESS_CREATED)); wd.setDirty(true); JSONRPCAddressBookEntry[] resultArray = addresses.toArray(new JSONRPCAddressBookEntry[0]); return resultArray; } @Override public synchronized Boolean setaddresslabel(String walletID, String address, String label) throws com.bitmechanic.barrister.RpcException { log.info("SET ADDRESS LABEL"); log.info("wallet name = " + walletID); log.info("address = " + address); log.info("label = " + label); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } if (address.startsWith("s")) { address = CSMiscUtils.getBitcoinAddressFromCoinSparkAddress(address); if (address == null) { JSONRPCError.COINSPARK_ADDRESS_INVALID.raiseRpcException(); } } if (label == null) label = ""; // this shouldn't happen when invoked via barrister boolean success = false; String filename = getFullPathForWalletName(walletID); final WalletData wd = this.controller.getModel().getPerWalletModelDataByWalletFilename(filename); final WalletInfoData addressBook = wd.getWalletInfo(); if (addressBook != null) { ArrayList<WalletAddressBookData> receivingAddresses = addressBook.getReceivingAddresses(); if (receivingAddresses != null) { Iterator<WalletAddressBookData> iter = receivingAddresses.iterator(); while (iter.hasNext()) { WalletAddressBookData addressBookData = iter.next(); if (addressBookData != null) { String btcAddress = addressBookData.getAddress(); if (btcAddress.equals(address)) { addressBookData.setLabel(label); success = true; break; } } } } } if (success) { CSEventBus.INSTANCE.postAsync(new SBEvent(SBEventType.ADDRESS_UPDATED)); wd.setDirty(true); } else { JSONRPCError.ADDRESS_NOT_FOUND.raiseRpcException(); } return success; } @Override public JSONRPCTransaction[] listtransactions(String walletID, Long limit) throws com.bitmechanic.barrister.RpcException { log.info("LIST TRANSACTIONS"); log.info("wallet name = " + walletID); log.info("limit # = " + limit); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } // if (limit>100) { // JSONRPCError.LIST_TRANSACTIONS_TOO_MANY.raiseRpcException(); // } else if (limit < 0) { JSONRPCError.LIST_TRANSACTIONS_TOO_FEW.raiseRpcException(); } // if limit is 0 get them all List<JSONRPCTransaction> resultList = new ArrayList<JSONRPCTransaction>(); int lastSeenBlock = controller.getMultiBitService().getChain().getBestChainHeight(); List<Transaction> transactions = null; if (limit > 0) { transactions = w.getRecentTransactions(limit.intValue(), false); } else { transactions = w.getTransactionsByTime(); } for (Transaction tx : transactions) { Date txDate = controller.getModel().getDateOfTransaction(controller, tx); long unixtime = txDate.getTime() / 1000L; // unix epoch in seconds long confirmations = 0; try { confirmations = lastSeenBlock - tx.getConfidence().getAppearedAtChainHeight(); } catch (IllegalStateException e) { } boolean incoming = !tx.sent(w); BigInteger feeSatoshis = tx.calculateFee(w); Double fee = null; if (!incoming) { BigDecimal feeBTC = new BigDecimal(feeSatoshis).divide(new BigDecimal(Utils.COIN)); fee = -1.0 * feeBTC.doubleValue(); } String txid = tx.getHashAsString(); ArrayList<JSONRPCTransactionAmount> amounts = getAssetTransactionAmounts(w, tx, true, false); JSONRPCTransactionAmount[] amountsArray = amounts.toArray(new JSONRPCTransactionAmount[0]); String message = CSMiscUtils.getShortTextMessage(w, txid); // int size = amounts.size(); // AssetTransactionAmountEntry[] entries = new AssetTransactionAmountEntry[size]; // int index = 0; // for (JSONRPCTransactionAmount ata : amounts) { // entries[index++] = new AssetTransactionAmountEntry(ata.getAssetRef(), ata); // } /* Get send and receive address */ TransactionOutput myOutput = null; TransactionOutput theirOutput = null; List<TransactionOutput> transactionOutputs = tx.getOutputs(); if (transactionOutputs != null) { for (TransactionOutput transactionOutput : transactionOutputs) { if (transactionOutput != null && transactionOutput.isMine(w)) { myOutput = transactionOutput; } if (transactionOutput != null && !transactionOutput.isMine(w)) { // We have to skip the OP_RETURN output as there is no address and it result sin an exception when trying to get the destination address Script script = transactionOutput.getScriptPubKey(); if (script != null) { if (script.isSentToAddress() || script.isSentToP2SH()) { theirOutput = transactionOutput; } } } } } // If this transaction was sent from addresses belonging to the same wallet boolean toMyself = myOutput != null && theirOutput == null; boolean hasAssets = amounts.size() > 1; boolean hasMessage = (message != null); boolean showCoinSparkAddress = hasAssets || hasMessage; String myReceiveAddress = null; String theirAddress = null; if (incoming || toMyself) { try { if (myOutput != null) { Address toAddress = new Address(this.controller.getModel().getNetworkParameters(), myOutput.getScriptPubKey().getPubKeyHash()); myReceiveAddress = toAddress.toString(); } if (myReceiveAddress != null && showCoinSparkAddress) { String s = CSMiscUtils.convertBitcoinAddressToCoinSparkAddress(myReceiveAddress); if (s != null) { myReceiveAddress = s; } } // If to myself, lets see if CoinSpark or not if (myReceiveAddress != null && toMyself) { String myCoinSparkAddress = SparkBitMapDB.INSTANCE .getSendCoinSparkAddressForTxid(tx.getHashAsString()); if (myCoinSparkAddress != null) { myReceiveAddress = myCoinSparkAddress; } } } catch (ScriptException e) { } } else { // outgoing transaction if (theirOutput != null) { // First let's see if we have stored the recipient in our map try { theirAddress = SparkBitMapDB.INSTANCE.getSendCoinSparkAddressForTxid(tx.getHashAsString()); } catch (Exception e) { } if (theirAddress == null) { try { theirAddress = theirOutput.getScriptPubKey() .getToAddress(controller.getModel().getNetworkParameters()).toString(); } catch (ScriptException se) { } if (theirAddress != null & showCoinSparkAddress) { String s = CSMiscUtils.convertBitcoinAddressToCoinSparkAddress(theirAddress); if (s != null) { theirAddress = s; } } } } } String address = ""; if ((incoming || toMyself) && myReceiveAddress != null) { address = myReceiveAddress; } else if (!incoming && theirAddress != null) { address = theirAddress; } String category = (incoming) ? "receive" : "send"; long paymentRef = CSMiscUtils.getPaymentRefFromTx(w, txid); JSONRPCTransaction atx = new JSONRPCTransaction(unixtime, confirmations, category, amountsArray, fee, txid, address, paymentRef, message); resultList.add(atx); } JSONRPCTransaction[] resultArray = resultList.toArray(new JSONRPCTransaction[0]); return resultArray; } /* * Return array of asset transaction objects. * Original method is in CSMiscUtils, so any changes there, must be reflected here. */ private ArrayList<JSONRPCTransactionAmount> getAssetTransactionAmounts(Wallet wallet, Transaction tx, boolean excludeBTCFee, boolean absoluteBTCFee) { if (wallet == null || tx == null) return null; Map<Integer, BigInteger> receiveMap = wallet.CS.getAssetsSentToMe(tx); Map<Integer, BigInteger> sendMap = wallet.CS.getAssetsSentFromMe(tx); // System.out.println(">>>> tx = " + tx.getHashAsString()); // System.out.println(">>>> receive map = " + receiveMap); // System.out.println(">>>> send map = " + sendMap); //Map<String, String> nameAmountMap = new TreeMap<>(); ArrayList<JSONRPCTransactionAmount> resultList = new ArrayList<>(); boolean isSentByMe = tx.sent(wallet); Map<Integer, BigInteger> loopMap = (isSentByMe) ? sendMap : receiveMap; // Integer assetID = null; BigInteger netAmount = null; // for (Map.Entry<Integer, BigInteger> entry : loopMap.entrySet()) { for (Integer assetID : loopMap.keySet()) { // assetID = entry.getKey(); if (assetID == null || assetID == 0) continue; // skip bitcoin BigInteger receivedAmount = receiveMap.get(assetID); // should be number of raw units BigInteger sentAmount = sendMap.get(assetID); boolean isReceivedAmountMissing = (receivedAmount == null); boolean isSentAmountMissing = (sentAmount == null); netAmount = BigInteger.ZERO; if (!isReceivedAmountMissing) netAmount = netAmount.add(receivedAmount); if (!isSentAmountMissing) netAmount = netAmount.subtract(sentAmount); if (isSentByMe && !isSentAmountMissing && sentAmount.equals(BigInteger.ZERO)) { // Catch a case where for a send transaction, the send amount for an asset is 0, // but the receive cmount is not 0. Also the asset was not valid. continue; } CSAsset asset = wallet.CS.getAsset(assetID); if (asset == null) { // something went wrong, we have asset id but no asset, probably deleted. // For now, we carry on, and we display what we know. } if (netAmount.equals(BigInteger.ZERO) && isSentByMe) { // If net asset is 0 and this is our send transaction, // we don't need to show anything, as this probably due to implicit transfer. // So continue the loop. continue; } if (netAmount.equals(BigInteger.ZERO) && !isSentByMe) { // Receiving an asset, where the value is 0 because its not confirmed yet, // or not known because asset files not uploaded so we dont know display format. // Anyway, we don't do anything here as we do want to display this incoming // transaction the best we can. } // System.out.println(">>>> isSentAmountMissing = " + isSentAmountMissing); // System.out.println(">>>> asset reference = " + asset.getAssetReference()); // System.out.println(">>>> asset name = " + asset.getName()); String name = null; CoinSparkGenesis genesis = null; boolean isUnknown = false; if (asset != null) { genesis = asset.getGenesis(); name = asset.getNameShort(); // could return null? } if (name == null) { isUnknown = true; if (genesis != null) { name = "Asset from " + genesis.getDomainName(); } else { // No genesis block found yet name = "Other Asset"; } } String s1 = null; if (asset == null || isUnknown == true || (netAmount.equals(BigInteger.ZERO) && !isSentByMe)) { // We don't have formatting details since asset is unknown or deleted // If there is a quantity, we don't display it since we don't have display format info // Of if incoming asset transfer, unconfirmed, it will be zero, so show ... instead s1 = "..."; } else { BigDecimal displayUnits = CSMiscUtils.getDisplayUnitsForRawUnits(asset, netAmount); s1 = CSMiscUtils.getFormattedDisplayString(asset, displayUnits); } // // Create JSONRPCTransactionAmount and add it to list // String fullName = ""; String assetRef = ""; if (asset != null) { fullName = asset.getName(); if (fullName == null) fullName = name; // use short name assetRef = CSMiscUtils.getHumanReadableAssetRef(asset); } BigDecimal displayQty = CSMiscUtils.getDisplayUnitsForRawUnits(asset, netAmount); JSONRPCTransactionAmount amount = new JSONRPCTransactionAmount(); amount.setAsset_ref(assetRef); amount.setDisplay(s1); amount.setName(fullName); amount.setName_short(name); amount.setQty(displayQty.doubleValue()); amount.setRaw(netAmount.longValue()); resultList.add(amount); } BigInteger satoshiAmount = receiveMap.get(0); satoshiAmount = satoshiAmount.subtract(sendMap.get(0)); // We will show the fee separately so no need to include here. if (excludeBTCFee && isSentByMe) { BigInteger feeSatoshis = tx.calculateFee(wallet); // returns positive if (absoluteBTCFee) { satoshiAmount = satoshiAmount.abs().subtract(feeSatoshis); } else { satoshiAmount = satoshiAmount.add(feeSatoshis); } } String btcAmount = Utils.bitcoinValueToFriendlyString(satoshiAmount) + " BTC"; BigDecimal satoshiAmountBTC = new BigDecimal(satoshiAmount).divide(new BigDecimal(Utils.COIN)); JSONRPCTransactionAmount amount = new JSONRPCTransactionAmount(); amount.setAsset_ref("bitcoin"); amount.setDisplay(btcAmount); amount.setName("Bitcoin"); amount.setName_short("Bitcoin"); amount.setQty(satoshiAmountBTC.doubleValue()); amount.setRaw(satoshiAmount.longValue()); resultList.add(amount); return resultList; } /** * Returns an array of unspent transaction outputs, detailing the Bitcoin and * asset balances tied to that UTXO. * * Behavior is modeled from bitcoin: https://bitcoin.org/en/developer-reference#listunspent * minconf "Default is 1; use 0 to count unconfirmed transactions." * maxconf "Default is 9,999,999; use 0 to count unconfirmed transactions." * As minconf and maxconf are not optional, the default values above are ignored. * * @param walletname * @param minconf * @param maxconf * @param addresses List of CoinSpark or Bitcoin addresses * @return * @throws com.bitmechanic.barrister.RpcException */ @Override public JSONRPCUnspentTransactionOutput[] listunspent(String walletname, Long minconf, Long maxconf, String[] addresses) throws com.bitmechanic.barrister.RpcException { log.info("LIST UNSPENT TRANSACTION OUTPUTS"); log.info("wallet name = " + walletname); log.info("min number of confirmations = " + minconf); log.info("max number of confirmations = " + maxconf); log.info("addresses = " + Arrays.toString(addresses)); Wallet w = getWalletForWalletName(walletname); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } if (minconf < 0 || maxconf < 0) { JSONRPCError.CONFIRMATIONS_TOO_LOW.raiseRpcException(); } NetworkParameters networkParams = this.controller.getModel().getNetworkParameters(); int mostCommonChainHeight = this.controller.getMultiBitService().getPeerGroup().getMostCommonChainHeight(); boolean skipAddresses = addresses.length == 0; // Parse the list of addresses // If CoinSpark, convert to BTC. Set<String> setOfAddresses = new HashSet<String>(); for (String address : addresses) { address = address.trim(); if (address.length() == 0) { continue; // ignore empty string } String btcAddress = null; String csAddress = null; if (address.startsWith("s")) { csAddress = address; btcAddress = CSMiscUtils.getBitcoinAddressFromCoinSparkAddress(address); if (btcAddress == null) { // Invalid CoinSpark address, so throw exception JSONRPCError.COINSPARK_ADDRESS_INVALID.raiseRpcException(csAddress); } } else { btcAddress = address; } boolean b = CSMiscUtils.validateBitcoinAddress(btcAddress, this.controller); if (b) { setOfAddresses.add(btcAddress); // convenience to add coinspark address for lookup // if (csAddress != null) setOfAddresses.add(csAddress); } else { if (csAddress != null) { // Invalid Bitcoin address from decoded CoinSpark address so throw exception. JSONRPCError.COINSPARK_ADDRESS_INVALID .raiseRpcException(csAddress + " --> decoded btc address " + btcAddress); } else { // Invalid Bitcoin address JSONRPCError.BITCOIN_ADDRESS_INVALID.raiseRpcException(btcAddress); } } } // If the set of addresses is empty, list of addresses probably whitespace etc. // Let's treat as skipping addresses. if (setOfAddresses.size() == 0) { skipAddresses = true; } ArrayList<JSONRPCUnspentTransactionOutput> resultList = new ArrayList<>(); Map<CSTransactionOutput, Map<Integer, CSBalance>> map = w.CS.getAllAssetTxOuts(); // Set<CSTransactionOutput> keys = map.keySet(); Iterator it = map.entrySet().iterator(); while (it.hasNext()) { Map.Entry entry = (Map.Entry) it.next(); CSTransactionOutput cstxout = (CSTransactionOutput) entry.getKey(); // Only process if txout belongs to our wallet and is unspent Transaction tx = cstxout.getParentTransaction(); TransactionOutput txout = tx.getOutput(cstxout.getIndex()); if (!txout.isAvailableForSpending() || !txout.isMine(w)) { continue; } // If confidence is not building, don't list it as it is pending, dead or unknown. TransactionConfidence.ConfidenceType confidenceType = tx.getConfidence().getConfidenceType(); if (confidenceType == TransactionConfidence.ConfidenceType.UNKNOWN || confidenceType == TransactionConfidence.ConfidenceType.DEAD) { continue; } int numConfirmations = 0; // If confidence is PENDING, this will be zero if (confidenceType == TransactionConfidence.ConfidenceType.BUILDING) { // getAppearedAtChainHeight() will throw illegalstate exception if confidence is not building int txAppearedAtChainHeight = tx.getConfidence().getAppearedAtChainHeight(); numConfirmations = mostCommonChainHeight - txAppearedAtChainHeight + 1; // if same, then it means 1 confirmation } // Only process if number of confirmations is within range. if (minconf == 0 || maxconf == 0) { // Unconfirmed transactions are okay, so do nothing. } else if (numConfirmations < minconf || numConfirmations > maxconf) { // Skip if outside of range continue; } // Only process if the BTC address for this tx is in the list of addresses String btcAddress = txout.getScriptPubKey().getToAddress(networkParams).toString(); if (!skipAddresses && !setOfAddresses.contains(btcAddress)) { continue; } // Get the bitcoin and asset balances on this utxo Map<Integer, CSBalance> balances = (Map<Integer, CSBalance>) entry.getValue(); boolean hasAssets = false; ArrayList<JSONRPCBalance> balancesList = new ArrayList<>(); for (Map.Entry<Integer, CSBalance> balanceEntry : balances.entrySet()) { Integer assetID = balanceEntry.getKey(); CSBalance balance = balanceEntry.getValue(); BigInteger qty = balance.getQty(); boolean isSelectable = DefaultCoinSelector.isSelectable(tx); // log.info(">>>> assetID = " + assetID + " , qty = " + qty); // Handle Bitcoin specially if (assetID == 0) { JSONRPCBalance bal = null; // if (isSelectable) { // bal = createBitcoinBalance(w, qty, null); // } else { bal = createBitcoinBalance(w, qty, null); // } balancesList.add(bal); continue; } // Other assets hasAssets = true; if (qty.compareTo(BigInteger.ZERO) > 0) { JSONRPCBalance bal = null; bal = createAssetBalance(w, assetID, qty, null); // if (isSelectable) { // bal = createAssetBalance(w, assetID, qty, qty); // } else { // bal = createAssetBalance(w, assetID, qty, BigInteger.ZERO); // } balancesList.add(bal); } } JSONRPCBalance[] balancesArray = balancesList.toArray(new JSONRPCBalance[0]); String scriptPubKeyHexString = Utils.bytesToHexString(txout.getScriptBytes()); // Build the object to return JSONRPCUnspentTransactionOutput utxo = new JSONRPCUnspentTransactionOutput(); utxo.setTxid(tx.getHashAsString()); utxo.setVout((long) cstxout.getIndex()); utxo.setScriptPubKey(scriptPubKeyHexString); utxo.setAmounts(balancesArray); //new JSONRPCBalance[0]); utxo.setConfirmations((long) numConfirmations); utxo.setBitcoin_address(btcAddress); if (hasAssets) { String sparkAddress = null; // First let's see if we have stored the recipient in our map and use it instead // of generating a new one from bitcoin address try { String spk = SparkBitMapDB.INSTANCE.getSendCoinSparkAddressForTxid(tx.getHashAsString()); String btc = CSMiscUtils.getBitcoinAddressFromCoinSparkAddress(spk); if (btc.equals(btcAddress)) { sparkAddress = spk; } } catch (Exception e) { } if (sparkAddress == null) { sparkAddress = CSMiscUtils.convertBitcoinAddressToCoinSparkAddress(btcAddress); } utxo.setCoinspark_address(sparkAddress); } utxo.setAmounts(balancesArray); resultList.add(utxo); } JSONRPCUnspentTransactionOutput[] resultArray = resultList.toArray(new JSONRPCUnspentTransactionOutput[0]); return resultArray; } /** * Create a JSONRPCBalance object for Bitcoin asset * @param w * @return */ private JSONRPCBalance createBitcoinBalance(Wallet w) { BigInteger rawBalanceSatoshi = w.getBalance(Wallet.BalanceType.ESTIMATED); BigInteger rawSpendableSatoshi = w.getBalance(Wallet.BalanceType.AVAILABLE); return createBitcoinBalance(w, rawBalanceSatoshi, rawSpendableSatoshi); } /** * Create a JSONRPCBalance object for Bitcoin asset * @param w * @param rawBalanceSatoshi In BitcoinJ terms, this is the estimated total balance * @param rawSpendableSatoshi In BitcoinJ terms, this is the available amount to spend * If null, will set amount instead of total and spendable. * @return */ private JSONRPCBalance createBitcoinBalance(Wallet w, BigInteger rawBalanceSatoshi, BigInteger rawSpendableSatoshi) { JSONRPCBalance balance = new JSONRPCBalance(); balance.setAsset_ref("bitcoin"); BigDecimal rawBalanceBTC = new BigDecimal(rawBalanceSatoshi).divide(new BigDecimal(Utils.COIN)); String rawBalanceDisplay = Utils.bitcoinValueToFriendlyString(rawBalanceSatoshi) + " BTC"; JSONRPCBalanceAmount bitcoinBalanceAmount = new JSONRPCBalanceAmount(rawBalanceSatoshi.longValue(), rawBalanceBTC.doubleValue(), rawBalanceDisplay); if (rawSpendableSatoshi != null) { BigDecimal rawSpendableBTC = new BigDecimal(rawSpendableSatoshi).divide(new BigDecimal(Utils.COIN)); String rawSpendableDisplay = Utils.bitcoinValueToFriendlyString(rawSpendableSatoshi) + " BTC"; JSONRPCBalanceAmount bitcoinSpendableAmount = new JSONRPCBalanceAmount(rawSpendableSatoshi.longValue(), rawSpendableBTC.doubleValue(), rawSpendableDisplay); balance.setTotal(bitcoinBalanceAmount); balance.setSpendable(bitcoinSpendableAmount); } else { balance.setAmount(bitcoinBalanceAmount); } balance.setVisible(true); balance.setValid(true); balance.setRefreshing(false); return balance; } /** * Create and populate a JSONRPCBalance object given an assetID and balance * @param w * @param assetID * @param totalRaw * @param spendableRaw If null, we set the amount field, instead of total and spendable. * @return */ private JSONRPCBalance createAssetBalance(Wallet w, int assetID, BigInteger totalRaw, BigInteger spendableRaw) { //Wallet.CoinSpark.AssetBalance assetBalance; CSAsset asset = w.CS.getAsset(assetID); String name = asset.getName(); String nameShort = asset.getNameShort(); if (name == null) { CoinSparkGenesis genesis = asset.getGenesis(); if (genesis != null) { name = "Asset from " + genesis.getDomainName(); nameShort = name; } else { // No genesis block found yet name = "Other Asset"; nameShort = "Other Asset"; } } String assetRef = CSMiscUtils.getHumanReadableAssetRef(asset); if (assetRef == null) { assetRef = "Awaiting new asset confirmation..."; } JSONRPCBalance ab = new JSONRPCBalance(); ab.setAsset_ref(assetRef); // Compute total balance Double balanceQty = CSMiscUtils.getDisplayUnitsForRawUnits(asset, totalRaw).doubleValue(); String balanceDisplay = CSMiscUtils.getFormattedDisplayStringForRawUnits(asset, totalRaw); JSONRPCBalanceAmount balanceAmount = new JSONRPCBalanceAmount(totalRaw.longValue(), balanceQty, balanceDisplay); if (spendableRaw != null) { Double spendableQty = CSMiscUtils.getDisplayUnitsForRawUnits(asset, spendableRaw).doubleValue(); String spendableDisplay = CSMiscUtils.getFormattedDisplayStringForRawUnits(asset, spendableRaw); JSONRPCBalanceAmount spendableAmount = new JSONRPCBalanceAmount(spendableRaw.longValue(), spendableQty, spendableDisplay); ab.setTotal(balanceAmount); ab.setSpendable(spendableAmount); } else { ab.setAmount(balanceAmount); } ab.setName(name); ab.setName_short(nameShort); String domain = CSMiscUtils.getDomainHost(asset.getDomainURL()); ab.setDomain(domain); ab.setUrl(asset.getAssetWebPageURL()); ab.setIssuer(asset.getIssuer()); ab.setDescription(asset.getDescription()); ab.setUnits(asset.getUnits()); ab.setMultiple(asset.getMultiple()); boolean isValid = (asset.getAssetState() == CSAsset.CSAssetState.VALID); if (asset.getAssetState() == CSAsset.CSAssetState.REFRESH) { ab.setRefreshing(true); ab.setStatus(CSMiscUtils.getHumanReadableAssetState(asset.getAssetStateBeforeRefresh())); } else { ab.setRefreshing(false); ab.setStatus(CSMiscUtils.getHumanReadableAssetState(asset.getAssetState())); } ab.setValid(isValid); Date validCheckedDate = asset.getValidChecked(); if (validCheckedDate != null) { ab.setChecked_unixtime(validCheckedDate.getTime() / 1000L); } ab.setContract_url(asset.getContractUrl()); String contractPath = asset.getContractPath(); if (contractPath != null) { String appDirPath = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory(); File file = new File(contractPath); File dir = new File(appDirPath); try { URI absolute = file.toURI(); URI base = dir.toURI(); URI relative = base.relativize(absolute); contractPath = relative.getPath(); } catch (Exception e) { // do nothing, if error, just use full contractPath } } ab.setContract_file(contractPath); ab.setGenesis_txid(asset.getGenTxID()); Date creationDate = asset.getDateCreation(); if (creationDate != null) { ab.setAdded_unixtime(creationDate.getTime() / 1000L); } // 3 October 2014, 1:47 am SimpleDateFormat sdf = new SimpleDateFormat("d MMMM yyyy, h:mm"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // CoinSpark asset web page shows GMT/UTC. SimpleDateFormat ampmdf = new SimpleDateFormat(" a"); // by default is uppercase and we need lower to match website Date issueDate = asset.getIssueDate(); if (issueDate != null) { ab.setIssue_date(sdf.format(issueDate) + ampmdf.format(issueDate).toLowerCase()); ab.setIssue_unixtime(issueDate.getTime() / 1000L); } // Never expires Date expiryDate = asset.getExpiryDate(); if (expiryDate != null) { ab.setExpiry_date(sdf.format(expiryDate) + ampmdf.format(expiryDate).toLowerCase()); ab.setExpiry_unixtime(expiryDate.getTime() / 1000L); } ab.setTracker_urls(asset.getCoinsparkTrackerUrls()); ab.setIcon_url(asset.getIconUrl()); ab.setImage_url(asset.getImageUrl()); ab.setFeed_url(asset.getFeedUrl()); ab.setRedemption_url(asset.getRedemptionUrl()); ab.setVisible(asset.isVisible()); return ab; } @Override public JSONRPCBalance[] listbalances(String walletID, Boolean onlyVisible) throws com.bitmechanic.barrister.RpcException { log.info("LIST BALANCES"); log.info("wallet name = " + walletID); log.info("only visible = " + onlyVisible); Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } ArrayList<JSONRPCBalance> resultList = new ArrayList<>(); // Add entry for BTC balances JSONRPCBalance btcAssetBalance = createBitcoinBalance(w); resultList.add(btcAssetBalance); int[] assetIDs = w.CS.getAssetIDs(); int n = 0; if (assetIDs != null) { n = assetIDs.length; } Wallet.CoinSpark.AssetBalance assetBalance; for (int i = 0; i < n; i++) { int id = assetIDs[i]; if (id == 0) continue; CSAsset asset = w.CS.getAsset(id); if (asset == null) continue; if (onlyVisible && !asset.isVisible()) continue; String name = asset.getName(); String nameShort = asset.getNameShort(); if (name == null) { CoinSparkGenesis genesis = asset.getGenesis(); if (genesis != null) { name = "Asset from " + genesis.getDomainName(); nameShort = name; } else { // No genesis block found yet name = "Other Asset"; nameShort = "Other Asset"; } } String assetRef = CSMiscUtils.getHumanReadableAssetRef(asset); if (assetRef == null) assetRef = "Awaiting new asset confirmation..."; assetBalance = w.CS.getAssetBalance(id); JSONRPCBalance ab = createAssetBalance(w, id, assetBalance.total, assetBalance.spendable); /* Long spendableRaw = assetBalance.spendable.longValue(); Double spendableQty = CSMiscUtils.getDisplayUnitsForRawUnits(asset, assetBalance.spendable).doubleValue(); String spendableDisplay = CSMiscUtils.getFormattedDisplayStringForRawUnits(asset, assetBalance.spendable); JSONRPCBalanceAmount spendableAmount = new JSONRPCBalanceAmount(spendableRaw, spendableQty, spendableDisplay); Long balanceRaw = assetBalance.total.longValue(); Double balanceQty = CSMiscUtils.getDisplayUnitsForRawUnits(asset, assetBalance.total).doubleValue(); String balanceDisplay = CSMiscUtils.getFormattedDisplayStringForRawUnits(asset, assetBalance.total); JSONRPCBalanceAmount balanceAmount = new JSONRPCBalanceAmount(balanceRaw, balanceQty, balanceDisplay); JSONRPCBalance ab = new JSONRPCBalance(); ab.setAsset_ref(assetRef); ab.setBalance(balanceAmount); ab.setSpendable(spendableAmount); ab.setName(name); ab.setName_short(nameShort); String domain = CSMiscUtils.getDomainHost(asset.getDomainURL()); ab.setDomain(domain); ab.setUrl(asset.getAssetWebPageURL()); ab.setIssuer(asset.getIssuer()); ab.setDescription(asset.getDescription()); ab.setUnits(asset.getUnits()); ab.setMultiple(asset.getMultiple()); ab.setStatus(CSMiscUtils.getHumanReadableAssetState(asset.getAssetState())); boolean isValid = (asset.getAssetState()==CSAsset.CSAssetState.VALID); // FIXME: Check num confirms too? ab.setValid(isValid); Date validCheckedDate = asset.getValidChecked(); if (validCheckedDate!=null) { ab.setChecked_unixtime(validCheckedDate.getTime()/1000L); } ab.setContract_url(asset.getContractUrl()); String contractPath = asset.getContractPath(); if (contractPath!=null) { String appDirPath = controller.getApplicationDataDirectoryLocator().getApplicationDataDirectory(); File file = new File(contractPath); File dir = new File(appDirPath); try { URI absolute = file.toURI(); URI base = dir.toURI(); URI relative = base.relativize(absolute); contractPath = relative.getPath(); } catch (Exception e) { // do nothing, if error, just use full contractPath } } ab.setContract_file(contractPath); ab.setGenesis_txid(asset.getGenTxID()); Date creationDate = asset.getDateCreation(); if (creationDate != null) { ab.setAdded_unixtime(creationDate.getTime()/1000L); } // 3 October 2014, 1:47 am SimpleDateFormat sdf = new SimpleDateFormat("d MMMM yyyy, h:mm"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); // CoinSpark asset web page shows GMT/UTC. SimpleDateFormat ampmdf = new SimpleDateFormat(" a"); // by default is uppercase and we need lower to match website Date issueDate = asset.getIssueDate(); if (issueDate != null) { ab.setIssue_date(sdf.format(issueDate) + ampmdf.format(issueDate).toLowerCase()); ab.setIssue_unixtime(issueDate.getTime()/1000L); } // Never expires Date expiryDate = asset.getExpiryDate(); if (expiryDate != null) { ab.setExpiry_date(sdf.format(expiryDate) + ampmdf.format(expiryDate).toLowerCase() ); ab.setExpiry_unixtime(expiryDate.getTime()/1000L); } ab.setTracker_urls(asset.getCoinsparkTrackerUrls()); ab.setIcon_url(asset.getIconUrl()); ab.setImage_url(asset.getImageUrl()); ab.setFeed_url(asset.getFeedUrl()); ab.setRedemption_url(asset.getRedemptionUrl()); ab.setVisible(asset.isVisible()); */ resultList.add(ab); } JSONRPCBalance[] resultArray = resultList.toArray(new JSONRPCBalance[0]); return resultArray; } /** * Returns true if transaction output can be spent and belongs to the wallet. * * @param w * @param hash * @param vout * @return * @throws com.bitmechanic.barrister.RpcException */ private synchronized boolean isTxOutSpendable(Wallet w, Sha256Hash hash, int vout) throws com.bitmechanic.barrister.RpcException { Transaction tx = w.getTransaction(hash); if (tx == null) { // Transaction is not in wallet JSONRPCError.TX_NOT_FOUND_IN_WALLET.raiseRpcException(); } TransactionOutput txout = null; try { txout = tx.getOutput(vout); } catch (IndexOutOfBoundsException e) { // Output not found on transaction JSONRPCError.TXOUT_INDEX_INVALID.raiseRpcException(); } if (!txout.isMine(w)) { // not my wallet - this should not happen if transaction is found in wallet. JSONRPCError.TXOUT_NOT_MINE.raiseRpcException(); } if (!txout.isAvailableForSpending()) { // not available for spending JSONRPCError.TXOUT_IS_NOT_AVAILABLE_FOR_SPENDING.raiseRpcException(); } return true; } @Override public synchronized String sendbitcoin(String walletID, String address, Double amount) throws com.bitmechanic.barrister.RpcException { log.info("SEND BITCOIN"); log.info("wallet name = " + walletID); log.info("address = " + address); log.info("amount = " + amount); return sendbitcoinusing_impl(walletID, null, 0L, address, amount, null); } @Deprecated @Override public synchronized String sendbitcoinwith(String walletID, String txid, Long vout, String address, Double amount) throws com.bitmechanic.barrister.RpcException { log.info("SEND BITCOIN WITH"); log.info("wallet name = " + walletID); log.info("txid = " + txid); log.info("vout = " + vout); log.info("address = " + address); log.info("amount = " + amount); return sendbitcoinusing_impl(walletID, txid, vout, address, amount, null); } @Override public synchronized String sendbitcoinusing(String walletID, String txid, Long vout, String address, Double amount) throws com.bitmechanic.barrister.RpcException { log.info("SEND BITCOIN USING"); log.info("wallet name = " + walletID); log.info("txid = " + txid); log.info("vout = " + vout); log.info("address = " + address); log.info("amount = " + amount); return sendbitcoinusing_impl(walletID, txid, vout, address, amount, null); } private synchronized String sendbitcoinusing_impl(String walletID, String txid, Long vout, String address, Double amount, String message) throws com.bitmechanic.barrister.RpcException { Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } if (amount <= 0.0) { JSONRPCError.SEND_BITCOIN_AMOUNT_TOO_LOW.raiseRpcException(); } BigInteger bitcoinAmountSatoshis = Utils.toNanoCoins(amount.toString()); // Is the BTC amount more than what is in the wallet? BigInteger totalSpend = bitcoinAmountSatoshis.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); BigInteger availableBalance = w.getBalance(Wallet.BalanceType.AVAILABLE); if (totalSpend.compareTo(availableBalance) > 0) { JSONRPCError.SEND_BITCOIN_INSUFFICIENT_MONEY.raiseRpcException(); } // Does the BTC amount respect the migration fees of any assets? boolean walletHasAnyAssets = CSMiscUtils.walletHasAnyAssets(w); if (walletHasAnyAssets) { boolean migrationSafe = CSMiscUtils.canSafelySpendWhileRespectingMigrationFee(this.controller, w, bitcoinAmountSatoshis); if (!migrationSafe) { BigInteger migrationFee = CSMiscUtils.calcMigrationFeeSatoshis(controller, w); JSONRPCError.SEND_INSUFFICIENT_MONEY_MIGRATION.raiseRpcException( "Need to keep at least " + Utils.bitcoinValueToFriendlyString(migrationFee) + " BTC."); } } // Check send with txid and vout Sha256Hash sendWithTxidHash = null; boolean canSpendSendWithTxOut = false; if (txid != null) { try { sendWithTxidHash = new Sha256Hash(txid); } catch (IllegalArgumentException e) { // Not a valid tx hash string JSONRPCError.INVALID_TXID_HASH.raiseRpcException(); } canSpendSendWithTxOut = isTxOutSpendable(w, sendWithTxidHash, vout.intValue()); } CoinSparkPaymentRef paymentRef = null; String bitcoinAddress = address; if (address.startsWith("s")) { bitcoinAddress = CSMiscUtils.getBitcoinAddressFromCoinSparkAddress(address); if (bitcoinAddress == null) { JSONRPCError.COINSPARK_ADDRESS_INVALID.raiseRpcException(); } else { CoinSparkAddress csa = new CoinSparkAddress(); csa.decode(address); int flags = csa.getAddressFlags(); if ((flags & CoinSparkAddress.COINSPARK_ADDRESS_FLAG_PAYMENT_REFS) > 0) { paymentRef = csa.getPaymentRef(); // log.debug(">>>> CoinSpark address has payment refs flag set: " + paymentRef.toString()); } if (message != null && !CSMiscUtils.canSendTextMessageToCoinSparkAddress(csa)) { JSONRPCError.COINSPARK_ADDRESS_MISSING_TEXT_MESSAGE_FLAG.raiseRpcException(); } } } else { // Cannot send message to a bitcoin address, must be a coinspark address if (message != null) { JSONRPCError.SEND_MESSAGE_MUST_BE_COINSPARK_ADDRESS.raiseRpcException(); } } boolean isValid = CSMiscUtils.validateBitcoinAddress(bitcoinAddress, controller); if (!isValid) { JSONRPCError.BITCOIN_ADDRESS_INVALID.raiseRpcException(); } String filename = getFullPathForWalletName(walletID); final WalletData wd = this.controller.getModel().getPerWalletModelDataByWalletFilename(filename); if (wd.isBusy()) { JSONRPCError.WALLEY_IS_BUSY.raiseRpcException(); } else { wd.setBusy(true); wd.setBusyTaskKey("jsonrpc.busy.sendbitcoin"); this.controller.fireWalletBusyChange(true); } Transaction sendTransaction = null; boolean sendValidated = false; boolean sendSuccessful = false; String sendTxHash = null; try { String sendAmount = amount.toString(); // Create a SendRequest. Address sendAddressObject; sendAddressObject = new Address(controller.getModel().getNetworkParameters(), bitcoinAddress); Wallet.SendRequest sendRequest = Wallet.SendRequest.to(sendAddressObject, Utils.toNanoCoins(sendAmount)); // SendRequest sendRequest = SendRequest.to(sendAddressObject, Utils.toNanoCoins(sendAmount), 6, new BigInteger("10000"),1); sendRequest.ensureMinRequiredFee = true; sendRequest.fee = BigInteger.ZERO; sendRequest.feePerKb = BitcoinModel.SEND_FEE_PER_KB_DEFAULT; // Send with txout vout if (canSpendSendWithTxOut) { boolean addedInput = sendRequest.addInput(w, new CSTransactionOutput(sendWithTxidHash, vout.intValue())); if (!addedInput) { // Failed to add input, so throw exception JSONRPCError.SEND_WITH_TXID_VOUT_FAILED.raiseRpcException(); } } // Send with payment ref - if it exists and is not 0 which SparkBit treats semantically as null if (paymentRef != null && paymentRef.getRef() != 0) { sendRequest.setPaymentRef(paymentRef); } // Set up message if one exists boolean isEmptyMessage = false; if (message == null || message.isEmpty() || message.trim().length() == 0) { isEmptyMessage = true; } if (!isEmptyMessage) { CoinSparkMessagePart[] parts = { CSMiscUtils.createPlainTextCoinSparkMessagePart(message) }; String[] serverURLs = CSMiscUtils.getMessageDeliveryServersArray(this.controller); sendRequest.setMessage(parts, serverURLs); // log.debug(">>>> Messaging servers = " + ArrayUtils.toString(serverURLs)); // log.debug(">>>> parts[0] = " + parts[0]); // log.debug(">>>> parts[0].fileName = " + parts[0].fileName); // log.debug(">>>> parts[0].mimeType = " + parts[0].mimeType); // log.debug(">>>> parts[0].content = " + new String(parts[0].content, "UTF-8")) } // Note - Request is populated with the AES key in the SendBitcoinNowAction after the user has entered it on the SendBitcoinConfirm form. // Complete it (which works out the fee) but do not sign it yet. log.info("Just about to complete the tx (and calculate the fee)..."); w.completeTx(sendRequest, false); sendValidated = true; log.info("The fee after completing the transaction was " + sendRequest.fee); // Let's do it for real now. sendTransaction = this.controller.getMultiBitService().sendCoins(wd, sendRequest, null); if (sendTransaction == null) { // a null transaction returned indicates there was not // enough money (in spite of our validation) JSONRPCError.SEND_BITCOIN_INSUFFICIENT_MONEY.raiseRpcException(); } else { sendSuccessful = true; sendTxHash = sendTransaction.getHashAsString(); log.info("Sent transaction was:\n" + sendTransaction.toString()); } if (sendSuccessful) { // There is enough money. /* If sending assets or BTC to a coinspark address, record transaction id --> coinspark address, into hashmap so we can use when displaying transactions */ if (address.startsWith("s")) { SparkBitMapDB.INSTANCE.putSendCoinSparkAddressForTxid(sendTxHash, address); } } else { // There is not enough money } // } catch (WrongNetworkException e1) { // } catch (AddressFormatException e1) { // } catch (KeyCrypterException e1) { } catch (InsufficientMoneyException e) { JSONRPCError.SEND_BITCOIN_INSUFFICIENT_MONEY.raiseRpcException(); } catch (CSExceptions.CannotEncode e) { JSONRPCError.SEND_MESSAGE_CANNOT_ENCODE.raiseRpcException(e.getMessage()); } catch (Exception e) { JSONRPCError.throwAsRpcException("Could not send bitcoin due to error", e); } finally { // Save the wallet. try { this.controller.getFileHandler().savePerWalletModelData(wd, false); } catch (WalletSaveException e) { // log.error(e.getMessage(), e); } if (sendSuccessful) { // This returns immediately if rpcsendassettimeout is 0. JSONRPCController.INSTANCE.waitForTxSelectable(sendTransaction); // JSONRPCController.INSTANCE.waitForTxBroadcast(sendTxHash); } // Declare that wallet is no longer busy with the task. wd.setBusyTaskKey(null); wd.setBusy(false); this.controller.fireWalletBusyChange(false); } if (sendSuccessful) { controller.fireRecreateAllViews(false); } return sendTxHash; } @Override public synchronized String sendasset(String walletID, String address, String assetRef, Double quantity, Boolean senderPays) throws com.bitmechanic.barrister.RpcException { log.info("SEND ASSET"); log.info("wallet name = " + walletID); log.info("address = " + address); log.info("asset ref = " + assetRef); log.info("quantity = " + quantity); log.info("sender pays = " + senderPays); return sendassetusing_impl(walletID, null, 0L, address, assetRef, quantity, senderPays, null, null); } @Deprecated @Override public synchronized String sendassetwith(String walletID, String txid, Long vout, String address, String assetRef, Double quantity, Boolean senderPays) throws com.bitmechanic.barrister.RpcException { log.info("SEND ASSET WITH"); log.info("wallet name = " + walletID); log.info("txid = " + txid); log.info("vout = " + vout); log.info("address = " + address); log.info("asset ref = " + assetRef); log.info("quantity = " + quantity); log.info("sender pays = " + senderPays); return sendassetusing_impl(walletID, txid, vout, address, assetRef, quantity, senderPays, null, null); } @Override public synchronized String sendassetusing(String walletID, String txid, Long vout, String address, String assetRef, Double quantity, Boolean senderPays) throws com.bitmechanic.barrister.RpcException { log.info("SEND ASSET USING"); log.info("wallet name = " + walletID); log.info("txid = " + txid); log.info("vout = " + vout); log.info("address = " + address); log.info("asset ref = " + assetRef); log.info("quantity = " + quantity); log.info("sender pays = " + senderPays); return sendassetusing_impl(walletID, txid, vout, address, assetRef, quantity, senderPays, null, null); } private synchronized String sendassetusing_impl(String walletID, String txid, Long vout, String address, String assetRef, Double quantity, Boolean senderPays, String message, Double btcAmount) throws com.bitmechanic.barrister.RpcException { String sendTxHash = null; boolean sendValidated = false; boolean sendSuccessful = false; Wallet w = getWalletForWalletName(walletID); if (w == null) { JSONRPCError.WALLET_NOT_FOUND.raiseRpcException(); } // Check send with txid and vout Sha256Hash sendWithTxidHash = null; boolean canSpendSendWithTxOut = false; if (txid != null) { try { sendWithTxidHash = new Sha256Hash(txid); } catch (IllegalArgumentException e) { // Not a valid tx hash string JSONRPCError.INVALID_TXID_HASH.raiseRpcException(); } canSpendSendWithTxOut = isTxOutSpendable(w, sendWithTxidHash, vout.intValue()); } if (quantity <= 0.0) { JSONRPCError.SEND_ASSET_AMOUNT_TOO_LOW.raiseRpcException(); } // BTC send amount, if null, use default amount of 10,000 satoshis. String sendAmount; if (btcAmount == null) { sendAmount = Utils.bitcoinValueToPlainString(BitcoinModel.COINSPARK_SEND_MINIMUM_AMOUNT); } else { double d = btcAmount.doubleValue(); if (d <= 0.0) { JSONRPCError.SEND_BITCOIN_AMOUNT_TOO_LOW.raiseRpcException(); } sendAmount = btcAmount.toString(); } BigInteger bitcoinAmountSatoshis = Utils.toNanoCoins(sendAmount); // Is the BTC amount more than what is in the wallet? BigInteger totalSpend = bitcoinAmountSatoshis.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); BigInteger availableBalance = w.getBalance(Wallet.BalanceType.AVAILABLE); if (totalSpend.compareTo(availableBalance) > 0) { JSONRPCError.SEND_BITCOIN_INSUFFICIENT_MONEY.raiseRpcException(); } // Does the BTC amount respect the migration fees of any assets? boolean migrationSafe = CSMiscUtils.canSafelySpendWhileRespectingMigrationFee(this.controller, w, bitcoinAmountSatoshis); if (!migrationSafe) { BigInteger migrationFee = CSMiscUtils.calcMigrationFeeSatoshis(controller, w); JSONRPCError.SEND_INSUFFICIENT_MONEY_MIGRATION.raiseRpcException( "Need to keep at least " + Utils.bitcoinValueToFriendlyString(migrationFee) + " BTC."); } CoinSparkPaymentRef paymentRef = null; String bitcoinAddress = address; if (!address.startsWith("s")) { JSONRPCError.ADDRESS_NOT_COINSPARK_ADDRESS.raiseRpcException(); } else { bitcoinAddress = CSMiscUtils.getBitcoinAddressFromCoinSparkAddress(address); if (bitcoinAddress == null) { JSONRPCError.COINSPARK_ADDRESS_INVALID.raiseRpcException(); } CoinSparkAddress csa = CSMiscUtils.decodeCoinSparkAddress(address); if (!CSMiscUtils.canSendAssetsToCoinSparkAddress(csa)) { JSONRPCError.COINSPARK_ADDRESS_MISSING_ASSET_FLAG.raiseRpcException(); } if (message != null && !CSMiscUtils.canSendTextMessageToCoinSparkAddress(csa)) { JSONRPCError.COINSPARK_ADDRESS_MISSING_TEXT_MESSAGE_FLAG.raiseRpcException(); } // payment ref? int flags = csa.getAddressFlags(); if ((flags & CoinSparkAddress.COINSPARK_ADDRESS_FLAG_PAYMENT_REFS) > 0) { paymentRef = csa.getPaymentRef(); // log.debug(">>>> CoinSpark address has payment refs flag set: " + paymentRef.toString()); } } boolean isValid = CSMiscUtils.validateBitcoinAddress(bitcoinAddress, controller); if (!isValid) { JSONRPCError.BITCOIN_ADDRESS_INVALID.raiseRpcException(); } String filename = getFullPathForWalletName(walletID); final WalletData wd = this.controller.getModel().getPerWalletModelDataByWalletFilename(filename); if (wd.isBusy()) { JSONRPCError.WALLEY_IS_BUSY.raiseRpcException(); } else { wd.setBusy(true); wd.setBusyTaskKey("jsonrpc.busy.sendasset"); this.controller.fireWalletBusyChange(true); } Transaction sendTransaction = null; try { // -- boilerplate ends here.... CSAsset asset = getAssetForAssetRefString(w, assetRef); if (asset == null) { if (isAssetRefValid(assetRef)) { JSONRPCError.ASSETREF_NOT_FOUND.raiseRpcException(); } else { JSONRPCError.ASSETREF_INVALID.raiseRpcException(); } } if (asset.getAssetState() != CSAsset.CSAssetState.VALID) { if (!CSMiscUtils.canSendInvalidAsset(controller)) { JSONRPCError.ASSET_STATE_INVALID.raiseRpcException(); } } // Check number of confirms int lastHeight = w.getLastBlockSeenHeight(); CoinSparkAssetRef assetReference = asset.getAssetReference(); if (assetReference != null) { final int blockIndex = (int) assetReference.getBlockNum(); final int numConfirmations = lastHeight - blockIndex + 1; // 0 means no confirmation, 1 is yes for sa int threshold = NUMBER_OF_CONFIRMATIONS_TO_SEND_ASSET_THRESHOLD; // FIXME: REMOVE/COMMENT OUT BEFORE RELEASE? String sendAssetWithJustOneConfirmation = controller.getModel() .getUserPreference("sendAssetWithJustOneConfirmation"); if (Boolean.TRUE.toString().equals(sendAssetWithJustOneConfirmation)) { threshold = 1; } //System.out.println(">>>> " + CSMiscUtils.getHumanReadableAssetRef(asset) + " num confirmations " + numConfirmations + ", threshold = " + threshold); if (numConfirmations < threshold) { JSONRPCError.ASSET_NOT_CONFIRMED.raiseRpcException(); } } String displayQtyString = new BigDecimal(quantity).toPlainString(); BigInteger assetAmountRawUnits = CSMiscUtils.getRawUnitsFromDisplayString(asset, displayQtyString); int assetID = asset.getAssetID(); BigInteger spendableAmount = w.CS.getAssetBalance(assetID).spendable; log.info("Want to send: " + assetAmountRawUnits + " , AssetID=" + assetID + ", total=" + w.CS.getAssetBalance(assetID).total + ", spendable=" + w.CS.getAssetBalance(assetID).spendable); // String sendAmount = Utils.bitcoinValueToPlainString(BitcoinModel.COINSPARK_SEND_MINIMUM_AMOUNT); CoinSparkGenesis genesis = asset.getGenesis(); long desiredRawUnits = assetAmountRawUnits.longValue(); short chargeBasisPoints = genesis.getChargeBasisPoints(); long rawFlatChargeAmount = genesis.getChargeFlat(); boolean chargeExists = (rawFlatChargeAmount > 0 || chargeBasisPoints > 0); if (chargeExists) { if (senderPays) { long x = genesis.calcGross(desiredRawUnits); assetAmountRawUnits = new BigInteger(String.valueOf(x)); } else { // We don't have to do anything if recipient pays, just send gross amount. // calcNet() returns what the recipient will receive, but it's not what we send. } } if (assetAmountRawUnits.compareTo(spendableAmount) > 0) { JSONRPCError.ASSET_INSUFFICIENT_BALANCE.raiseRpcException(); } // Create a SendRequest. Address sendAddressObject; String sendAddress = bitcoinAddress; sendAddressObject = new Address(controller.getModel().getNetworkParameters(), sendAddress); //SendRequest sendRequest = SendRequest.to(sendAddressObject, Utils.toNanoCoins(sendAmount)); //public static SendRequest to(Address destination,BigInteger value,int assetID, BigInteger assetValue,int split) { //BigInteger assetAmountRawUnits = new BigInteger(assetAmount); // BigInteger bitcoinAmountSatoshis = Utils.toNanoCoins(sendAmount); Wallet.SendRequest sendRequest = Wallet.SendRequest.to(sendAddressObject, bitcoinAmountSatoshis, assetID, assetAmountRawUnits, 1); sendRequest.ensureMinRequiredFee = true; sendRequest.fee = BigInteger.ZERO; sendRequest.feePerKb = BitcoinModel.SEND_FEE_PER_KB_DEFAULT; // Note - Request is populated with the AES key in the SendBitcoinNowAction after the user has entered it on the SendBitcoinConfirm form. // Send with txout vout if (canSpendSendWithTxOut) { boolean addedInput = sendRequest.addInput(w, new CSTransactionOutput(sendWithTxidHash, vout.intValue())); if (!addedInput) { // Failed to add input, so throw exception JSONRPCError.SEND_WITH_TXID_VOUT_FAILED.raiseRpcException(); } } // Send with payment ref - if it exists and is not 0 which SparkBit treats semantically as null if (paymentRef != null && paymentRef.getRef() != 0) { sendRequest.setPaymentRef(paymentRef); } // Set up message if one exists boolean isEmptyMessage = false; if (message == null || message.trim().isEmpty()) { isEmptyMessage = true; } if (!isEmptyMessage) { CoinSparkMessagePart[] parts = { CSMiscUtils.createPlainTextCoinSparkMessagePart(message) }; String[] serverURLs = CSMiscUtils.getMessageDeliveryServersArray(this.controller); sendRequest.setMessage(parts, serverURLs); } // Complete it (which works out the fee) but do not sign it yet. log.info("Just about to complete the tx (and calculate the fee)..."); // there is enough money, so let's do it for real now w.completeTx(sendRequest, false); sendValidated = true; log.info("The fee after completing the transaction was " + sendRequest.fee); // Let's do it for real now. sendTransaction = this.controller.getMultiBitService().sendCoins(wd, sendRequest, null); if (sendTransaction == null) { // a null transaction returned indicates there was not // enough money (in spite of our validation) JSONRPCError.ASSET_INSUFFICIENT_BALANCE.raiseRpcException(); } else { sendSuccessful = true; sendTxHash = sendTransaction.getHashAsString(); } if (sendSuccessful) { // There is enough money. /* If sending assets or BTC to a coinspark address, record transaction id --> coinspark address, into hashmap so we can use when displaying transactions */ if (address.startsWith("s")) { SparkBitMapDB.INSTANCE.putSendCoinSparkAddressForTxid(sendTxHash, address); } } else { // There is not enough money } //--- bolilerplate begins... } catch (InsufficientMoneyException ime) { JSONRPCError.ASSET_INSUFFICIENT_BALANCE.raiseRpcException(); } catch (com.bitmechanic.barrister.RpcException e) { throw (e); } catch (CSExceptions.CannotEncode e) { JSONRPCError.SEND_MESSAGE_CANNOT_ENCODE.raiseRpcException(e.getMessage()); } catch (Exception e) { JSONRPCError.throwAsRpcException("Could not send asset due to error: ", e); } finally { // Save the wallet. try { this.controller.getFileHandler().savePerWalletModelData(wd, false); } catch (WalletSaveException e) { // log.error(e.getMessage(), e); } if (sendSuccessful) { // This returns immediately if rpcsendassettimeout is 0. JSONRPCController.INSTANCE.waitForTxSelectable(sendTransaction); // JSONRPCController.INSTANCE.waitForTxBroadcast(sendTxHash); } // Declare that wallet is no longer busy with the task. wd.setBusyTaskKey(null); wd.setBusy(false); this.controller.fireWalletBusyChange(false); } if (sendSuccessful) { controller.fireRecreateAllViews(false); } return sendTxHash; } // @Override // public Boolean test(JSONRPCTestParams params) throws com.bitmechanic.barrister.RpcException { // log.info("TEST"); // log.info("walletname = " + params.getWalletname()); // log.info("address = " + params.getAddress()); // log.info("amount = " + params.getAmount()); // log.info("txid = " + params.getTxid()); // log.info("vout = " + params.getVout()); // log.info("message = " + params.getMessage()); // return true; // } // // 0.9.4 JSON-RPC Commands // @Override public String sendbitcoinasset(String walletname, String address, Double btc_amount, String assetref, Double asset_qty, Boolean senderpays) throws com.bitmechanic.barrister.RpcException { log.info("SEND BITCOIN ASSET"); log.info("wallet name = " + walletname); log.info("address = " + address); log.info("btc amount = " + btc_amount); log.info("asset ref = " + assetref); log.info("asset qty = " + asset_qty); log.info("sender pays = " + senderpays); return sendassetusing_impl(walletname, null, 0L, address, assetref, asset_qty, senderpays, null, btc_amount); } @Override public String sendassetmessage(String walletname, String address, String assetref, Double quantity, Boolean senderpays, String message) throws com.bitmechanic.barrister.RpcException { log.info("SEND ASSET"); log.info("wallet name = " + walletname); log.info("address = " + address); log.info("asset ref = " + assetref); log.info("quantity = " + quantity); log.info("sender pays = " + senderpays); log.info("message = " + message); return sendassetusing_impl(walletname, null, 0L, address, assetref, quantity, senderpays, message, null); } @Override public String sendbitcoinmessage(String walletname, String address, Double amount, String message) throws com.bitmechanic.barrister.RpcException { log.info("SEND BITCOIN MESSAGE"); log.info("wallet name = " + walletname); log.info("address = " + address); log.info("amount = " + amount); log.info("message = " + message); return sendbitcoinusing_impl(walletname, null, 0L, address, amount, message); } @Override public String sendbitcoinassetmessage(String walletname, String address, Double btc_amount, String assetref, Double asset_qty, Boolean senderpays, String message) throws com.bitmechanic.barrister.RpcException { log.info("SEND BITCOIN ASSET"); log.info("wallet name = " + walletname); log.info("address = " + address); log.info("btc amount = " + btc_amount); log.info("asset ref = " + assetref); log.info("asset qty = " + asset_qty); log.info("sender pays = " + senderpays); log.info("message = " + message); return sendassetusing_impl(walletname, null, 0L, address, assetref, asset_qty, senderpays, message, btc_amount); } }