- Created a specific package entities for Room entities and do not mix them with plain models.

- Simplified the Transfers entity so that we store only the information that will actually be used.
- Create a TransfersLoader, which once launched checks and updated Transfer operations.
This commit is contained in:
Severiano Jaramillo 2018-11-27 13:33:04 -06:00
parent c63388155a
commit 08cf6b3c40
27 changed files with 817 additions and 69 deletions

View file

@ -89,11 +89,11 @@ class DatabaseLoadActivity: ConnectedActivity() {
* @param assetList The list of assets obtained in the last 'list_assets' API call. * @param assetList The list of assets obtained in the last 'list_assets' API call.
*/ */
private fun handlePlatformAssetBatch(assetList: List<Asset>) { private fun handlePlatformAssetBatch(assetList: List<Asset>) {
val assets = mutableListOf<cy.agorise.bitsybitshareswallet.models.Asset>() val assets = mutableListOf<cy.agorise.bitsybitshareswallet.entities.Asset>()
// TODO find if there is a better way to convert to Bitsy Asset instances // TODO find if there is a better way to convert to Bitsy Asset instances
for (_asset in assetList) { for (_asset in assetList) {
val asset = cy.agorise.bitsybitshareswallet.models.Asset( val asset = cy.agorise.bitsybitshareswallet.entities.Asset(
_asset.objectId, _asset.objectId,
_asset.symbol, _asset.symbol,
_asset.precision, _asset.precision,

View file

@ -9,8 +9,7 @@ import android.widget.Toast
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsSingleChoice import com.afollestad.materialdialogs.list.listItemsSingleChoice
import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase import cy.agorise.bitsybitshareswallet.entities.Authority
import cy.agorise.bitsybitshareswallet.models.Authority
import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository import cy.agorise.bitsybitshareswallet.repositories.AuthorityRepository
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
@ -236,7 +235,7 @@ class ImportBrainkeyActivity : ConnectedActivity() {
val name = accountProperties.name val name = accountProperties.name
val isLTM = accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE val isLTM = accountProperties.membership_expiration_date == Constants.LIFETIME_EXPIRATION_DATE
val userAccount = cy.agorise.bitsybitshareswallet.models.UserAccount(id, name, isLTM) val userAccount = cy.agorise.bitsybitshareswallet.entities.UserAccount(id, name, isLTM)
val userAccountRepository = UserAccountRepository(application) val userAccountRepository = UserAccountRepository(application)
userAccountRepository.insert(userAccount) userAccountRepository.insert(userAccount)

View file

@ -5,7 +5,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.Asset import cy.agorise.bitsybitshareswallet.entities.Asset
@Dao @Dao
interface AssetDao { interface AssetDao {

View file

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.Authority import cy.agorise.bitsybitshareswallet.entities.Authority
@Dao @Dao
interface AuthorityDao { interface AuthorityDao {

View file

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.Balance import cy.agorise.bitsybitshareswallet.entities.Balance
@Dao @Dao
interface BalanceDao { interface BalanceDao {

View file

@ -4,14 +4,13 @@ import android.content.Context
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import cy.agorise.bitsybitshareswallet.models.* import cy.agorise.bitsybitshareswallet.entities.*
@Database(entities = [ @Database(entities = [
Asset::class, Asset::class,
Authority::class, Authority::class,
Balance::class, Balance::class,
EquivalentValue::class, EquivalentValue::class,
Operation::class,
Transfer::class, Transfer::class,
UserAccount::class UserAccount::class
], version = 1, exportSchema = false) ], version = 1, exportSchema = false)
@ -21,7 +20,6 @@ abstract class BitsyDatabase : RoomDatabase() {
abstract fun authorityDao(): AuthorityDao abstract fun authorityDao(): AuthorityDao
abstract fun balanceDao(): BalanceDao abstract fun balanceDao(): BalanceDao
abstract fun equivalentValueDao(): EquivalentValueDao abstract fun equivalentValueDao(): EquivalentValueDao
abstract fun operationDao(): OperationDao
abstract fun transferDao(): TransferDao abstract fun transferDao(): TransferDao
abstract fun userAccountDao(): UserAccountDao abstract fun userAccountDao(): UserAccountDao

View file

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.EquivalentValue import cy.agorise.bitsybitshareswallet.entities.EquivalentValue
@Dao @Dao
interface EquivalentValueDao { interface EquivalentValueDao {

View file

@ -1,16 +0,0 @@
package cy.agorise.bitsybitshareswallet.daos
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.Operation
@Dao
interface OperationDao {
@Insert
fun insert(operation: Operation)
@Query("SELECT * FROM operations")
fun getAllOperations(): LiveData<List<Operation>>
}

View file

@ -3,14 +3,25 @@ package cy.agorise.bitsybitshareswallet.daos
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.Transfer import cy.agorise.bitsybitshareswallet.entities.Transfer
@Dao @Dao
interface TransferDao { interface TransferDao {
@Insert @Insert
fun insert(transfer: Transfer) fun insert(transfer: Transfer)
// TODO find a way to return number of added rows
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertAll(transfers: List<Transfer>)
@Query("SELECT COUNT(*) FROM transfers")
fun getCount(): Int
@Query("SELECT * FROM transfers") @Query("SELECT * FROM transfers")
fun getAllTransfers(): LiveData<List<Transfer>> fun getAllTransfers(): LiveData<List<Transfer>>
@Query("DELETE FROM transfers")
fun deleteAll()
} }

View file

@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import cy.agorise.bitsybitshareswallet.models.UserAccount import cy.agorise.bitsybitshareswallet.entities.UserAccount
@Dao @Dao
interface UserAccountDao { interface UserAccountDao {

View file

@ -1,4 +1,4 @@
package cy.agorise.bitsybitshareswallet.models package cy.agorise.bitsybitshareswallet.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View file

@ -1,4 +1,4 @@
package cy.agorise.bitsybitshareswallet.models package cy.agorise.bitsybitshareswallet.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View file

@ -1,4 +1,4 @@
package cy.agorise.bitsybitshareswallet.models package cy.agorise.bitsybitshareswallet.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View file

@ -1,4 +1,4 @@
package cy.agorise.bitsybitshareswallet.models package cy.agorise.bitsybitshareswallet.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View file

@ -1,20 +1,15 @@
package cy.agorise.bitsybitshareswallet.models package cy.agorise.bitsybitshareswallet.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "transfers",foreignKeys = @Entity(tableName = "transfers")
[ForeignKey(
entity = Operation::class,
parentColumns = ["id"],
childColumns = ["operation_id"]
)]
)
data class Transfer ( data class Transfer (
@PrimaryKey @PrimaryKey
@ColumnInfo(name = "operation_id") val operationId: String, @ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "block_number") val blockNumber: Long,
@ColumnInfo(name = "timestamp") val timestamp: Long,
@ColumnInfo(name = "fee_amount") val feeAmount: Long, @ColumnInfo(name = "fee_amount") val feeAmount: Long,
@ColumnInfo(name = "fee_asset_id") val feeAssetId: String, // TODO should be foreign key to Asset @ColumnInfo(name = "fee_asset_id") val feeAssetId: String, // TODO should be foreign key to Asset
@ColumnInfo(name = "source") val source: String, // TODO should be foreign key to UserAccount @ColumnInfo(name = "source") val source: String, // TODO should be foreign key to UserAccount

View file

@ -1,4 +1,4 @@
package cy.agorise.bitsybitshareswallet.models package cy.agorise.bitsybitshareswallet.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity

View file

@ -1,21 +1,16 @@
package cy.agorise.bitsybitshareswallet.fragments package cy.agorise.bitsybitshareswallet.fragments
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.Nullable
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import cy.agorise.bitsybitshareswallet.R import cy.agorise.bitsybitshareswallet.R
import cy.agorise.bitsybitshareswallet.activities.ReceiveTransactionActivity import cy.agorise.bitsybitshareswallet.entities.UserAccount
import cy.agorise.bitsybitshareswallet.activities.SendTransactionActivity
import cy.agorise.bitsybitshareswallet.models.UserAccount
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.bitsybitshareswallet.viewmodels.BalancesViewModel import cy.agorise.bitsybitshareswallet.viewmodels.BalancesViewModel
import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel import cy.agorise.bitsybitshareswallet.viewmodels.UserAccountViewModel

View file

@ -0,0 +1,68 @@
package cy.agorise.bitsybitshareswallet.models
import cy.agorise.graphenej.AssetAmount
import cy.agorise.graphenej.models.OperationHistory
import cy.agorise.graphenej.operations.TransferOperation
/**
* This class is very similar to the OperationHistory, but while the later is used to deserialize
* the transfer information exactly as it comes from the 'get_relative_account_history' API call,
* this class is used to represent a single entry in the local database.
*
*
* Every entry in the transfers table needs a bit more information than what is provided by the
* HistoricalTransfer. We need to know the specific timestamp of a transaction for instance, instead
* of just a block number.
*
*
* There's also the data used for the equivalent fiat value.
*
*
* Created by nelson on 12/18/16.
*/
class HistoricalOperationEntry {
var historicalTransfer: OperationHistory? = null
var timestamp: Long = 0
var equivalentValue: AssetAmount? = null
override fun toString(): String {
if (historicalTransfer != null) {
// Since for now we know that all stored historical operations are 'transfers'
val op = historicalTransfer!!.operation as TransferOperation
var memo = "?"
if (op.memo != null && op.memo.plaintextMessage != null) {
memo = op.memo.plaintextMessage
}
return String.format(
"<%d, %s -> %s, %d of %s, memo: %s>",
timestamp,
op.from.objectId,
op.to.objectId,
op.assetAmount.amount.toLong(),
op.assetAmount.asset.objectId,
memo
)
} else {
return "<>"
}
}
override fun hashCode(): Int {
return historicalTransfer!!.objectId.hashCode()
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other == null) {
return false
}
return if (javaClass != other.javaClass) {
false
} else {
val otherEntry = other as HistoricalOperationEntry?
otherEntry!!.historicalTransfer!!.objectId == this.historicalTransfer!!.objectId
}
}
}

View file

@ -1,14 +0,0 @@
package cy.agorise.bitsybitshareswallet.models
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "operations")
data class Operation (
@PrimaryKey
@ColumnInfo(name = "id") val id: String,
@ColumnInfo(name = "type") val type: Int,
@ColumnInfo(name = "timestamp") val timestamp: Long,
@ColumnInfo(name = "block_number") val blockNumber: Long
)

View file

@ -0,0 +1,42 @@
//package cy.agorise.bitsybitshareswallet.processors
//
//import android.content.Context
//import androidx.lifecycle.Lifecycle
//import cy.agorise.graphenej.api.android.RxBus
//import cy.agorise.graphenej.api.calls.GetAccounts
//import cy.agorise.graphenej.entities.AccountProperties
//import cy.agorise.graphenej.entities.JsonRpcResponse
//import io.reactivex.android.schedulers.AndroidSchedulers
//
///**
// * Loader used to fetch the missing accounts and update the database
// */
//class MissingAccountsLoader(context: Context, lifecycle: Lifecycle) : BaseDataLoader(context, lifecycle) {
//
// init {
// mDisposable = RxBus.getBusInstance()
// .asFlowable()
// .observeOn(AndroidSchedulers.mainThread())
// .subscribe { message ->
// if (message is JsonRpcResponse<*>) {
// if (message.result is List<*> &&
// (message.result as List<*>).size > 0 &&
// (message.result as List<*>)[0] is AccountProperties
// ) {
// database.putUserAccounts(message.result as List<AccountProperties>)
// }
// }
// }
// }
//
// protected fun onNetworkReady() {
// requestAccountInfo()
// }
//
// fun requestAccountInfo() {
// val missingAccountNames = database.getMissingAccountNames()
// if (missingAccountNames.size > 0) {
// mNetworkService.sendMessage(GetAccounts(missingAccountNames), GetAccounts.REQUIRED_API)
// }
// }
//}

View file

@ -0,0 +1,624 @@
package cy.agorise.bitsybitshareswallet.processors
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.preference.PreferenceManager
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import cy.agorise.bitsybitshareswallet.entities.Transfer
import cy.agorise.bitsybitshareswallet.models.HistoricalOperationEntry
import cy.agorise.bitsybitshareswallet.repositories.TransferRepository
import cy.agorise.bitsybitshareswallet.utils.Constants
import cy.agorise.graphenej.*
import cy.agorise.graphenej.api.ConnectionStatusUpdate
import cy.agorise.graphenej.api.android.NetworkService
import cy.agorise.graphenej.api.android.RxBus
import cy.agorise.graphenej.api.calls.GetRelativeAccountHistory
import cy.agorise.graphenej.errors.ChecksumException
import cy.agorise.graphenej.models.BlockHeader
import cy.agorise.graphenej.models.JsonRpcResponse
import cy.agorise.graphenej.models.OperationHistory
import cy.agorise.graphenej.operations.TransferOperation
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.functions.Consumer
import org.bitcoinj.core.DumpedPrivateKey
import org.bitcoinj.core.ECKey
import java.util.*
/**
* This class is responsible for loading the local database with all past transfer operations of the
* currently selected account.
*
* The procedure used to load the database in 3 steps:
*
* 1- Load all transfer operations
* 2- Load all missing times
* 3- Load all missing equivalent times
*
* Since the 'get_relative_account_history' will not provide either timestamps nor equivalent values
* for every transfer, we must first load all historical transfer operations, and then proceed to
* handle those missing columns.
*/
class TransfersLoader(private var mContext: Context?, private val mLifeCycle: Lifecycle) : LifecycleObserver {
private val TAG = this.javaClass.name
/* Constant used to fix the number of historical transfers to fetch from the network in one batch */
private val HISTORICAL_TRANSFER_BATCH_SIZE = 100
/* Constant used to split the missing times and equivalent values in batches of constant time */
private val MISSING_TIMES_BATCH_SIZE = 100
/* Constants used to specify which type of conversion will be used. A direct conversion is performed
* only between BTS -> bitUSD. For all other assets, we'll need to perform a two step conversion
* in the form XXX -> BTS -> bitUSD. That is the basic difference between direct and indirect conversions.
*/
private val DIRECT_CONVERSION: Short = 0
private val INDIRECT_CONVERSION: Short = 1
private val RESPONSE_GET_RELATIVE_ACCOUNT_HISTORY = 0
private val RESPONSE_GET_MARKET_HISTORY = 1
// The current conversion type
private var mConversionType: Short = -1
/* Variable used in the 2-step currency conversion, in order to temporally hold the value of the
* XXX -> BTS conversion. */
private var coreCurrencyEqValue: AssetAmount? = null
private var mDisposable: Disposable? = null
/* Current user account */
private var mCurrentAccount: UserAccount? = null
/** Variable holding the current user's private key in the WIF format */
private var wifKey: String? = null
/** Repository to access and update Transfers */
private var transferRepository: TransferRepository? = null
/* Network service connection */
private var mNetworkService: NetworkService? = null
/* Counter used to keep track of the transfer history batch count */
private var historicalTransferCount = 0
/* List of block numbers with missing date information in the database */
private var missingTimes: LinkedList<Long>? = null
/* List of transactions for which we don't have the equivalent value data */
private var missingEquivalentValues: LinkedList<HistoricalOperationEntry>? = null
/* Reference to the current operation entry that we're currently working with */
private var mTransferEntry: HistoricalOperationEntry? = null
/* Map used to exclude operation ids that were already checked for equivalent value, but could
* not be obtained for some reason. This is important in order to add some finality to the
* procedure
*/
private val eqValueBlacklist = HashMap<String, Boolean>()
// /*
// * Account loader, used to fetch extra information about any new account that we might come
// * across after updating the transfers information
// */
// private var missingAccountsLoader: MissingAccountsLoader? = null
// /*
// * Assets loader, used to fetch missing asset data after the transfers table has been updated
// */
// private var missingAssetsLoader: MissingAssetsLoader? = null
// Used to keep track of the current state
private var mState = State.IDLE
/**
* Flag used to keep track of the NetworkService binding state
*/
private var mShouldUnbindNetwork: Boolean = false
private val DEBUG = false
private var lastId: Long = 0
private var lastEquivalentValueBlockNum: Long = 0
private var lastMissingTimeBlockNum: Long = 0
// Map used to keep track of request and response id pairs
private val responseMap = HashMap<Long, Int>()
private val mNetworkServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// We've bound to LocalService, cast the IBinder and get LocalService instance
val binder = service as NetworkService.LocalBinder
mNetworkService = binder.service
// Start the transfers update
startTransfersUpdateProcedure()
}
override fun onServiceDisconnected(componentName: ComponentName) {}
}
/**
* Enum class used to keep track of the current state of the loader
*/
private enum class State {
IDLE,
LOADING_MISSING_TIMES,
LOADING_EQ_VALUES,
CANCELLED,
FINISHED
}
init {
this.mLifeCycle.addObserver(this)
transferRepository = TransferRepository(mContext!!)
val pref = PreferenceManager.getDefaultSharedPreferences(mContext)
val userId = pref.getString(Constants.KEY_CURRENT_ACCOUNT_ID, "")
if (userId != "") {
mCurrentAccount = UserAccount(userId)
// wifkey = database.getWif(mContext, mCurrentAccount, AuthorityType.MEMO) TODO RESTORE
mDisposable = RxBus.getBusInstance()
.asFlowable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(Consumer { message ->
if (mState == State.FINISHED) return@Consumer
if (message is JsonRpcResponse<*>) {
if (message.result is List<*>) {
if (responseMap.containsKey(message.id)) {
val responseType = responseMap[message.id]
when (responseType) {
RESPONSE_GET_RELATIVE_ACCOUNT_HISTORY -> handleOperationList(message.result as List<OperationHistory>)
// RESPONSE_GET_MARKET_HISTORY -> handlePastMarketData(message.result as List<BucketObject>)
}
responseMap.remove(message.id)
}
} else if (message.result is BlockHeader) {
// handleMissingTimeResponse(message.result as BlockHeader)
}
} else if (message is ConnectionStatusUpdate) {
if (message.updateCode == ConnectionStatusUpdate.DISCONNECTED) {
// If we got a disconnection notification, we should clear our response map, since
// all its stored request ids will now be reset
responseMap.clear()
}
}
})
} else {
// If there is no current user, we should not do anything
mState = State.CANCELLED
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
internal fun onStart() {
if (mState != State.CANCELLED) {
val intent = Intent(mContext, NetworkService::class.java)
if (mContext!!.bindService(intent, mNetworkServiceConnection, Context.BIND_AUTO_CREATE)) {
mShouldUnbindNetwork = true
} else {
Log.e(TAG, "Binding to the network service failed.")
}
}
}
/**
* Starts the procedure that will try to update the 'transfers' table
*/
private fun startTransfersUpdateProcedure() {
if (DEBUG) {
// If we are in debug mode, we first erase all entries in the 'transfer' table
transferRepository!!.deleteAll()
}
val transferCount = transferRepository!!.getCount()
if (transferCount > 0) {
// If we already have some transfers in the database, we might want to skip the request
// straight to the last batch
historicalTransferCount = Math.floor((transferCount / HISTORICAL_TRANSFER_BATCH_SIZE).toDouble()).toInt()
}
// Retrieving account transactions
loadNextOperationsBatch()
}
/**
* Handles a freshly obtained list of OperationHistory instances. This is how the full node
* answers our 'get_relative_account_history' API call.
*
* This response however, has to be processed before being stored in the local database.
*
* @param operationHistoryList List of OperationHistory instances
*/
private fun handleOperationList(operationHistoryList: List<OperationHistory>) {
historicalTransferCount++
val insertedCount = transferRepository!!.insertAll(processOperationList(operationHistoryList))
// Log.d(TAG, String.format("Inserted count: %d, list size: %d", insertedCount, operationHistoryList.size))
if (operationHistoryList.isEmpty()) {
// We finally reached the end of the transactions, so now we must check for missing
// transfer times
// mState = State.LOADING_MISSING_TIMES
// missingTimes = database.getMissingTransferTimes(lastMissingTimeBlockNum, MISSING_TIMES_BATCH_SIZE)
// if (missingTimes!!.size > 0) {
// // Proceed to load missing times
// processNextMissingTime()
// } else {
// // If we got no missing times, proceed to check for missing equivalent values
// mState = State.LOADING_EQ_VALUES
//
// // If we're done loading missing transfer times, we check for missing equivalent values.
// missingEquivalentValues = database.getMissingEquivalentValues(
// lastEquivalentValueBlockNum,
// PalmPayDatabase.DEFAULT_EQUIVALENT_VALUE_BATCH_SIZE
// )
//
// // Processing one of the missing values entries
// processNextEquivalentValue()
// }
} else {
// If we inserted more than one operation, we cannot yet be sure we've reached the
// end of the operation list, so we issue another call to the 'get_relative_account_history'
// API call
loadNextOperationsBatch()
}
}
/**
* Method used to issue a new 'get_relative_account_history' API call. This is expected to retrieve
* at most HISTORICAL_TRANSFER_BATCH_SIZE operations.
*/
private fun loadNextOperationsBatch() {
val stop = historicalTransferCount * HISTORICAL_TRANSFER_BATCH_SIZE
val start = stop + HISTORICAL_TRANSFER_BATCH_SIZE
lastId = mNetworkService!!.sendMessage(
GetRelativeAccountHistory(
mCurrentAccount,
stop,
HISTORICAL_TRANSFER_BATCH_SIZE,
start
), GetRelativeAccountHistory.REQUIRED_API
)
responseMap[lastId] = RESPONSE_GET_RELATIVE_ACCOUNT_HISTORY
}
/**
* Method that will transform a list of OperationHistory instances to a list of
* HistoricalOperationEntry.
*
* The HistoricalOperationEntry class is basically a wrapper around the OperationHistory class
* provided by the Graphenej library. It is used to better reflect what we store in the internal
* database for every transfer and expands the OperationHistory class basically adding
* two things:
*
* 1- A timestamp
* 2- An AssetAmount instance to represent the equivalent value in a fiat value
*
* @param operations List of OperationHistory instances
* @return List of HistoricalOperationEntry instances
*/
private fun processOperationList(operations: List<OperationHistory>): List<Transfer> {
val transfers = ArrayList<Transfer>()
if (wifKey == null) {
// In case of key storage corruption, we give up on processing this list of operations
return transfers
}
val memoKey = DumpedPrivateKey.fromBase58(null, wifKey!!).key
val publicKey = PublicKey(ECKey.fromPublicOnly(memoKey.pubKey))
val myAddress = Address(publicKey.key)
for (historicalOp in operations) {
if (historicalOp.operation == null || historicalOp.operation !is TransferOperation) {
// Some historical operations might not be transfer operations.
// As of right now non-transfer operations get deserialized as null
continue
}
val entry = HistoricalOperationEntry()
val op = historicalOp.operation as TransferOperation
val memo = op.memo
if (memo.byteMessage != null) {
val destinationAddress = memo.destination
try {
if (destinationAddress.toString() == myAddress.toString()) {
val decryptedMessage = Memo.decryptMessage(memoKey, memo.source, memo.nonce, memo.byteMessage)
memo.plaintextMessage = decryptedMessage
}
} catch (e: ChecksumException) {
Log.e(TAG, "ChecksumException. Msg: " + e.message)
} catch (e: NullPointerException) {
// This is expected in case the decryption fails, so no need to log this event.
Log.e(TAG, "NullPointerException. Msg: " + e.message)
} catch (e: Exception) {
Log.e(TAG, "Exception while decoding memo. Msg: " + e.message)
}
}
val transfer = Transfer(
historicalOp.objectId,
historicalOp.blockNum,
entry.timestamp,
op.fee.amount.toLong(),
op.fee.asset.objectId,
op.from.objectId,
op.to.objectId,
op.assetAmount.amount.toLong(),
op.assetAmount.asset.objectId,
memo.plaintextMessage,
memo.source.toString(),
memo.destination.toString()
)
// TODO build transfer object, save Wif in ImportBrainkeyActivity
transfers.add(transfer)
}
return transfers
}
// /**
// * Handles the response to the 'get_block_header' API call
// * @param blockHeader The requested block header
// */
// private fun handleMissingTimeResponse(blockHeader: BlockHeader) {
// if (missingTimes == null || missingTimes!!.size == 0 || mState == State.CANCELLED) {
// // If the missingTimes attribute is null, this means that the request was probably issued
// // by another instance of this class.
// Log.d(TAG, "Cancelling loader instance")
// mState = State.CANCELLED
// return
// }
// val updated = database.setBlockTime(blockHeader, missingTimes!!.peek())
// if (!updated) {
// Log.w(TAG, "Failed to update time from transaction at block: " + missingTimes!!.peek())
// } else {
// Log.d(TAG, "Missing time updated")
// }
//
// // Removing missing time from stack
// lastMissingTimeBlockNum = missingTimes!!.poll()
//
// if (!processNextMissingTime()) {
// Log.d(
// TAG,
// String.format("Checking missing time for transfers later than block %d", lastMissingTimeBlockNum)
// )
// missingTimes = database.getMissingTransferTimes(lastMissingTimeBlockNum, MISSING_TIMES_BATCH_SIZE)
// if (missingTimes!!.size == 0) {
// // If we have no more missing times to handle, we can finally proceed to the last step, which
// // is to take care of the missing equivalent values
// Log.d(TAG, "Would be trying to check equivalent values")
// // If we're done loading missing transfer times, we check for missing equivalent values.
// missingEquivalentValues = database.getMissingEquivalentValues(
// lastEquivalentValueBlockNum,
// PalmPayDatabase.DEFAULT_EQUIVALENT_VALUE_BATCH_SIZE
// )
//
// // Processing one of the missing values entries
// processNextEquivalentValue()
// } else {
// processNextMissingTime()
// }
// }
// }
// private fun processNextEquivalentValue() {
// if (missingEquivalentValues!!.size > 0) {
// Log.d(TAG, String.format("Found %d missing equivalent values", missingEquivalentValues!!.size))
// val missingAssets = database.getMissingAssets()
// if (missingAssets.size == 0 &&
// mTransferEntry != null &&
// mTransferEntry!!.historicalTransfer!!.operation is TransferOperation
// ) {
// mTransferEntry = missingEquivalentValues!!.peek()
// lastEquivalentValueBlockNum = mTransferEntry!!.historicalTransfer!!.blockNum
// val transferOperation = mTransferEntry!!.historicalTransfer!!.operation as TransferOperation
// val transferredAsset = transferOperation.assetAmount.asset
// if (transferredAsset == Constants.bitUSD) {
// // Easier case, in which the transferred asset was already the designated
// // smartcoin (bitUSD)
// mTransferEntry!!.equivalentValue = AssetAmount(
// transferOperation.assetAmount.amount,
// transferredAsset
// )
// database.updateEquivalentValue(mTransferEntry)
//
// // Removing the now solved equivalent value
// missingEquivalentValues!!.poll()
//
// // Processing the next missing equivalent value
// processNextEquivalentValue()
// } else if (transferredAsset == Constants.BTS) {
// // If the transferred asset was the core BTS currency, then we must just
// // perform a single network request to find out how much a single BTS was
// // worth at the time of this operation
// val quote = Constants.bitUSD
// val bucket: Long = 86400
// val end = mTransferEntry!!.timestamp * 1000
// val start = end - 86400 * 1000
// lastId = mNetworkService!!.sendMessage(
// GetMarketHistory(transferredAsset, quote, bucket, start, end),
// GetMarketHistory.REQUIRED_API
// )
// responseMap[lastId] = RESPONSE_GET_MARKET_HISTORY
//
// // Direct conversion
// mConversionType = DIRECT_CONVERSION
// } else {
// // If the transferred asset was not bitUSD not BTS, then we cannot be sure
// // that a market for this asset (which can either be a smartcoin or a User Issued Asset)
// // even exists. For this reason, we opt to convert this value to BTS first, and
// // then find out its equivalent value in bitUSD at the time of the transfer.
// Log.d(TAG, "Missing equivalent value was of other denomination")
// val quote = Constants.BTS
// val bucket: Long = 86400
// val end = mTransferEntry!!.timestamp * 1000
// val start = end - 86400 * 1000
// lastId = mNetworkService!!.sendMessage(
// GetMarketHistory(transferredAsset, quote, bucket, start, end),
// GetMarketHistory.REQUIRED_API
// )
// responseMap[lastId] = RESPONSE_GET_MARKET_HISTORY
//
// // Indirect conversion
// mConversionType = INDIRECT_CONVERSION
// }
// }
// } else {
// Log.d(TAG, "Reached the end of the equivalent values stack")
// missingEquivalentValues = database.getMissingEquivalentValues(
// lastEquivalentValueBlockNum,
// PalmPayDatabase.DEFAULT_EQUIVALENT_VALUE_BATCH_SIZE
// )
//
// Log.d(TAG, String.format("Got %d entries initially", missingEquivalentValues!!.size))
//
// // Clearing the equivalent values stack from blacklisted item
// for (i in missingEquivalentValues!!.indices) {
// val entry = missingEquivalentValues!!.peek()
// if (eqValueBlacklist[entry.historicalTransfer!!.objectId] != null) {
// // Removing this equivalent value from the stack, since we've already tried to solve it
// missingEquivalentValues!!.poll()
//
// Log.d(TAG, "Removed entry from stack thanks to the black list")
// }
// }
//
// if (missingEquivalentValues!!.size > 0) {
// Log.d(TAG, "Got a new stack")
// processNextEquivalentValue()
// } else {
// Log.d(TAG, "Really reached the final")
//// missingAssetsLoader = MissingAssetsLoader(mContext, mLifeCycle)
//// missingAccountsLoader = MissingAccountsLoader(mContext!!, mLifeCycle)
// mState = State.FINISHED
// }
// }
// }
// /**
// * Handle the requested past market data, in order to calculate the equivalent value
// * @param buckets List of market buckets
// */
// private fun handlePastMarketData(buckets: List<BucketObject>) {
// if (buckets.isNotEmpty()) {
// // Taking the last bucket from the list
// val bucket = buckets[buckets.size - 1]
//
// // Obtaining the transferred amount
// val transferAmount =
// (mTransferEntry!!.historicalTransfer!!.operation as TransferOperation).assetAmount
//
// // Obtaining the full asset data of both base and quote
// var base = database.fillAssetDetails(bucket.key.base)
// var quote = database.fillAssetDetails(bucket.key.quote)
// if (mConversionType == DIRECT_CONVERSION) {
// // Doing conversion and updating the database
// val converter = Converter(base, quote, bucket)
// val convertedBaseValue = converter.convert(transferAmount, Converter.CLOSE_VALUE)
// val equivalentValue = AssetAmount(UnsignedLong.valueOf(convertedBaseValue), Constants.bitUSD)
//
// mTransferEntry!!.equivalentValue = equivalentValue
// database.updateEquivalentValue(mTransferEntry)
//
// // Removing the now solved equivalent value
// missingEquivalentValues!!.poll()
//
// // Process the next equivalent value, in case we have one
// processNextEquivalentValue()
// } else if (mConversionType == INDIRECT_CONVERSION) {
// if (coreCurrencyEqValue == null) {
// // We are in the first step of an indirect conversion
//
// val originalTransfer =
// (mTransferEntry!!.historicalTransfer!!.operation as TransferOperation).assetAmount
//
// // Doing conversion and updating the database
// val converter = Converter(base, quote, bucket)
// val convertedBaseValue = converter.convert(originalTransfer, Converter.CLOSE_VALUE)
// coreCurrencyEqValue = AssetAmount(UnsignedLong.valueOf(convertedBaseValue), base)
//
// base = database.fillAssetDetails(Constants.BTS)
// quote = database.fillAssetDetails(Constants.bitUSD)
//
// // Performing the 2nd step of the equivalent value calculation. We already hold the
// // relationship XXX -> BTS, now we need the BTS -> bitUSD for this time bucket.
// val bucketWindow: Long = 86400
// val end = mTransferEntry!!.timestamp * 1000
// val start = end - 86400 * 1000
// lastId = mNetworkService!!.sendMessage(
// GetMarketHistory(base, quote, bucketWindow, start, end),
// GetMarketHistory.REQUIRED_API
// )
// responseMap[lastId] = RESPONSE_GET_MARKET_HISTORY
// } else {
// // We are in the second step of an indirect transaction
//
// // Doing the conversion
// val converter = Converter(base, quote, bucket)
// val convertedFinalValue = converter.convert(coreCurrencyEqValue, Converter.CLOSE_VALUE)
//
// // Obtaining the equivalent value in bitUSD
// val equivalentValue = AssetAmount(UnsignedLong.valueOf(convertedFinalValue), Constants.bitUSD)
//
// mTransferEntry!!.equivalentValue = equivalentValue
// database.updateEquivalentValue(mTransferEntry)
//
// // Removing the now solved equivalent value
// missingEquivalentValues!!.poll()
//
// // Re-setting some fields
// mConversionType = -1
// coreCurrencyEqValue = null
//
// // Process the next equivalent value, in case we have one
// processNextEquivalentValue()
// }
// }
// } else {
// // Removing the for-now skipped equivalent value
// val skipped = missingEquivalentValues!!.poll()
//
// // Adding the skipped operation to a blacklist
// eqValueBlacklist[skipped.historicalTransfer!!.objectId] = true
//
// Log.w(TAG, "Got no buckets in the selected period")
// processNextEquivalentValue()
// }
// }
// /**
// * Method used to issue a new 'get_block_header' API call to the full node, in case we
// * still have some missing times left in the missingTimes list.
// *
// * @return True if we did issue the API, false if the list was empty and nothing was done
// */
// private fun processNextMissingTime(): Boolean {
// if (missingTimes!!.size > 0) {
// val blockNum = missingTimes!!.peek()
// lastId = mNetworkService!!.sendMessage(GetBlockHeader(blockNum!!), GetBlockHeader.REQUIRED_API)
// return true
// } else {
// return false
// }
// }
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
internal fun onDestroy() {
if (mDisposable != null && !mDisposable!!.isDisposed) mDisposable!!.dispose()
if (mShouldUnbindNetwork) {
mContext!!.unbindService(mNetworkServiceConnection)
mShouldUnbindNetwork = false
}
mContext = null
}
}

View file

@ -4,7 +4,7 @@ import android.app.Application
import android.os.AsyncTask import android.os.AsyncTask
import cy.agorise.bitsybitshareswallet.daos.AssetDao import cy.agorise.bitsybitshareswallet.daos.AssetDao
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.models.Asset import cy.agorise.bitsybitshareswallet.entities.Asset
class AssetRepository internal constructor(application: Application) { class AssetRepository internal constructor(application: Application) {

View file

@ -4,7 +4,7 @@ import android.app.Application
import android.os.AsyncTask import android.os.AsyncTask
import cy.agorise.bitsybitshareswallet.daos.AuthorityDao import cy.agorise.bitsybitshareswallet.daos.AuthorityDao
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.models.Authority import cy.agorise.bitsybitshareswallet.entities.Authority
class AuthorityRepository internal constructor(application: Application) { class AuthorityRepository internal constructor(application: Application) {

View file

@ -0,0 +1,38 @@
package cy.agorise.bitsybitshareswallet.repositories
import android.content.Context
import android.os.AsyncTask
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.daos.TransferDao
import cy.agorise.bitsybitshareswallet.entities.Transfer
class TransferRepository internal constructor(context: Context) {
private val mTransferDao: TransferDao
init {
val db = BitsyDatabase.getDatabase(context)
mTransferDao = db!!.transferDao()
}
fun insertAll(transfers: List<Transfer>) {
insertAllAsyncTask(mTransferDao).execute(transfers)
}
fun getCount(): Int {
return mTransferDao.getCount()
}
fun deleteAll() {
mTransferDao.deleteAll()
}
private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: TransferDao) :
AsyncTask<List<Transfer>, Void, Void>() {
override fun doInBackground(vararg transfers: List<Transfer>): Void? {
mAsyncTaskDao.insertAll(transfers[0])
return null
}
}
}

View file

@ -5,7 +5,7 @@ import android.os.AsyncTask
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase import cy.agorise.bitsybitshareswallet.daos.BitsyDatabase
import cy.agorise.bitsybitshareswallet.daos.UserAccountDao import cy.agorise.bitsybitshareswallet.daos.UserAccountDao
import cy.agorise.bitsybitshareswallet.models.UserAccount import cy.agorise.bitsybitshareswallet.entities.UserAccount
class UserAccountRepository internal constructor(application: Application) { class UserAccountRepository internal constructor(application: Application) {

View file

@ -1,5 +1,7 @@
package cy.agorise.bitsybitshareswallet.utils package cy.agorise.bitsybitshareswallet.utils
import cy.agorise.graphenej.Asset
object Constants { object Constants {
@ -36,6 +38,12 @@ object Constants {
*/ */
const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59" const val LIFETIME_EXPIRATION_DATE = "1969-12-31T23:59:59"
/**
* Smartcoin options for output
*/
val BTS = Asset("1.3.0")
val bitUSD = Asset("1.3.121")
/** Key used to store the night mode setting into the shared preferences */ /** Key used to store the night mode setting into the shared preferences */
const val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated" const val KEY_NIGHT_MODE_ACTIVATED = "key_night_mode_activated"
} }

View file

@ -3,7 +3,7 @@ package cy.agorise.bitsybitshareswallet.viewmodels
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import cy.agorise.bitsybitshareswallet.models.UserAccount import cy.agorise.bitsybitshareswallet.entities.UserAccount
import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository import cy.agorise.bitsybitshareswallet.repositories.UserAccountRepository
class UserAccountViewModel(application: Application) : AndroidViewModel(application) { class UserAccountViewModel(application: Application) : AndroidViewModel(application) {