Author Dex_DQT
In this chapter we will build the functionality to view the list of transactions and transfer funds
Add WalletManager method
Because of the increase in the number of methods, I will pick the most important methods to explain. If you need other methods, you can go to find the source code.
1, Load All Wallet
Code as bellow:
fun LoadAllWallet() { val context = DiggingApplication.context try { val file = File(context.filesDir, WalletStorePath) if (!file.isDirectory) { return } file.listFiles()?.forEach { walletSet.add(it.name.split(".")[0]) } } catch (e: Exception) { } }
Since the wallet files we create/import are stored in the walletFiles directory as {wallet name}.json, we need to get the list of files in the walletFiles directory first and then extract the wallet name
2, Get Current WalletName
The code is as follow:
/** * 获取当前钱包名字 */ fun GetCurrentWalletName(): String { if (!TextUtils.isEmpty(walletName)) { return walletName } val context = DiggingApplication.context // val sp = context.getSharedPreferences("walletInfo", Context.MODE_PRIVATE) var curWalletName = sp.getString("curWallet", "") // 如果当前选中的钱包为空, 则获取第一个钱包名做为显示钱包 if (TextUtils.isEmpty(curWalletName)) { if (walletSet.size != 0) { curWalletName = walletSet.iterator().next() SwitchWallet(curWalletName) } } walletName = curWalletName!! return walletName }
If there is no wallet selected, you could get the list of wallets and take the first wallet as the displayed wallet.
3, Switch Wallet
/** * 切换钱包 */ fun SwitchWallet(walletName: String) { val context = DiggingApplication.context val sp = context.getSharedPreferences("walletInfo", Context.MODE_PRIVATE) val editor = sp.edit() editor.putString("curWallet", walletName) editor.commit() WalletManager.walletName = walletName }
Here we use SharedPreferences to get the way to store the currently displayed wallet name
The WalletManager class also adds a function to determine if the wallet name exists, which is used to determine if the creation or import of a wallet is renamed.
After the modification, call WalletManager.LoadAllWallet function in the initUI of SplashActivity class to determine whether there is a wallet, if there is no wallet, then jump to the OperateMenuActivity page to create or import a wallet. If there is already a wallet, then it will jump to the MainActivity page and display the wallet information. The specific code is as follows::
override fun initUI() { // 加载所有的钱包 WalletManager.LoadAllWallet() val handler = Handler(Looper.getMainLooper()) /** * 设置2s后跳转到页面 */ handler.postDelayed({ if (WalletManager.IsExistWallet()) { DXRouter.JumpAndFinish(this, MainActivity::class.java) } else { DXRouter.JumpAndFinish(this, OperateMenuActivity::class.java) } }, 2000) }
Building the Main Page
For specific layout code, please check res/layout/activity_main.xml. Here we use ViewPager2 to do the page jumping, where the main page Adapter is adapter/MainPageAdapter. The specific code is:
class MainPageAdapter : RecyclerView.Adapter<BaseViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val inflater = LayoutInflater.from(parent.context) return AssetPage(PageAssetsBinding.inflate(inflater, parent, false).root) } override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { } override fun getItemViewType(position: Int): Int { return position } override fun getItemCount(): Int { return 1 } }
Since this chapter only implements the wallet page, the number returned by getItemCount is 1.
Then we create the wallet page. For the exact layout code, please see res/layout/page_assets.xml
Then we write the logic to get the wallet. Create the logic class AssetPage in the page directory res/layout/page_assets.xml, the main code is as follows:
private fun initUI() { //设置顶部的状态栏高度 ViewUtil.SetStatusBarMargin(binding.nodeBar) // // 设置tab选项 var indicatorThickness = DensityUtil.DP2PX(itemView.context, 2.0f) binding.stbBar.setIndicatorThickness(indicatorThickness + 4) indicatorThickness += 4 binding.stbBar.setIndicatorCornerRadius(indicatorThickness / 2.0f) val titleList = ArrayList<String>(3) // 添加资产选项 titleList.add(ResourceUtil.GetString(R.string.wallet_tab_assets)) binding.stbBar.setCustomTabView { container, title -> getTableView(container, title) } binding.vpContent.adapter = AssetPageAdapter() binding.stbBar.setViewPager(binding.vpContent, titleList) // 获取当前钱包名字 val walletName = WalletManager.GetCurrentWalletName() // 获取当前钱包地址 val walletAddress = WalletManager.GetWalletAddress(walletName) // 显示在页面上 binding.walletName.text = walletName binding.walletAddress.text = walletAddress }
Here we use ATON’s CustomTabLayout control to display the asset page. Because the asset page is displayed using ViewPager2, we need to write the corresponding Adapter. create AssetPageAdapter in the adapter package, the code is as follows:
class AssetPageAdapter : RecyclerView.Adapter<BaseViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { val inflater = LayoutInflater.from(parent.context) val binding = PageCommonRecyclerviewBinding.inflate(inflater, parent, false) return AssetRecyclerPage(binding.root) } override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { holder.OnRender(position) } override fun getItemCount(): Int { return 1 } }
Since we only have the Asset page here, the return value of getItemCount is 1. This is because the Asset page is also displayed using RecyclerView, the corresponding Adapter is the AssetRecyclerAdAdapter class under the adapter package, and the Asset only shows the number of LATs in the current wallet, so there is only one record. The code to get the number of LAT is placed in the loadAsset of AssetRecyclerPage, which is as follows:
private suspend fun loadAsset() { // 获取当前钱包地址 val walletAddress = WalletManager.GetCurWalletAddress() // 获取钱包余额 val lat = PlatonApi.GetBalance(walletAddress) val assetItemData = AssetItemData(R.mipmap.icon_platon_item_default, "LAT", lat) val itemList = ArrayList<AssetItemData>() itemList.add(assetItemData) adapter.UpdateData(itemList) }
Note that since AssetRecyclerPage is not an Activity class, you cannot use the lifecycleScope, only GlobalScop. Call the loadAsset function as follows:
GlobalScope.launch(Dispatchers.Main) { loadAsset() }
Here you must specify to use Dispatchers.Main, otherwise GlobalScope.launch will be executed by default using sub-threads, and will crash when operating the UI.
The final result of getting the wallet balance is shown below:
Build the Page that Displays the List of Transactions
For the specific layout code, please see the res/layout/activity_assets_chain.xml. The page effect is as follows:
Then we create the AssetChainActivity class under the activity package. Instead of explain how to showing the number of balances, we will focus on how to get the transaction records. From ATON, we know that the address of the test network is: https://aton-dev.platon.network. Meanwhile, from the open source version of ATON, we know that the address to get the transaction list is:/app/v0760/transaction/list, and the call parameters of the interface are as follows:
data class TransactionListTO( var walletAddrs: List<String>? = null,// 数据列表 var beginSequence: Int = 1, // 页数 var listSize: Int = 20, // 每一页显示的数量 var direction: String = "new" //向前取最新的 )
The data type returned by this interface is Transaction, and the corresponding fields are described as follows:
@JsonIgnoreProperties(ignoreUnknown = true) data class Transaction( /** * 交易hash */ val hash: String? = null, /** * 当前交易所在快高 */ val blockNumber: String? = null, /** * 当前交易的链id */ val chainId: String? = null, /** * 交易实际花费值(手续费),单位:wei * “21168000000000” */ val actualTxCost: String? = null, /** * 交易发送方 */ val from: String? = null, /** * 交易接收方 */ val to: String? = null, /** * 交易序列号 */ val sequence: Long = 0, /** * 交易状态2 pending 1 成功 0 失败 */ val txReceiptStatus: Int = 0, /** * 0: 转账 * 1: 合约发布(合约创建) * 2: 合约调用(合约执行) * 3: 其他收入 * 4: 其他支出 * 5: MPC交易 * 1000: 发起质押(创建验证人) * 1001: 修改质押信息(编辑验证人) * 1002: 增持质押(增加自有质押) * 1003: 撤销质押(退出验证人) * 1004: 发起委托(委托) * 1005: 减持/撤销委托(赎回委托) * 2000: 提交文本提案(创建提案) * 2001: 提交升级提案(创建提案) * 2002: 提交参数提案(创建提案) * 2003: 给提案投票(提案投票) * 2004: 版本声明 * 3000: 举报多签(举报验证人) * 4000: 创建锁仓计划(创建锁仓) */ val txType: String? = null, /** * 交易金额 */ val value: String? = null, /** * 发送者钱包名称 */ val senderWalletName: String? = null, /** * {json}交易详细信息 */ val txInfo: String? = null, /** * 提交时间(单位:毫秒) */ val timestamp: Long = 0, /** * to类型 * contract —— 合约 * address —— 地址 */ val toType: String? = null, /** * Sent发送/Receive接收 */ val direction: String? = null, /** * 节点名称/委托给/验证人 * //txType = 1004,1005,1000,1001,1002,1003,3000,2000,2001,2002,2003,2004,nodeName不为空 * 详细描述:txType = 2000,2001,2002,2003(验证人) * 详细描述:txType = 1004,1005(委托给,同时也是节点名称) */ val nodeName: String? = null, /** * txType = 1004,1005,1000,1001,1002,1003,3000,2004,nodeId不为空 */ val nodeId: String? = null, /** * txType = 4000,lockAddress不为空 */ val lockAddress: String? = null, /** * 举报类型 */ val reportType: String? = null, /** * 版本 */ val version: String? = null, /** * 提案id(截取最后一个破折号) */ val url: String? = null, /** * PIP编号 eip-100(EIP-由前端拼接) */ val piDID: String? = null, /** * 提案类型 */ val proposalType: String? = null, /** * 提案id */ val proposalId: String? = null, /** * 投票 */ val vote: String? = null, //=======================下面是新加的三个字段================================= //=======================下面是新加的三个字段================================= /** * //赎回状态, 1: 退回中 2:退回成功 赎回失败查看交易txReceiptStatus */ val redeemStatus: String? = null, /** * 钱包头像 */ val walletIcon: String? = null, /** * 钱包名称 */ val walletName: String? = null, /** * "unDelegation":"10000", //赎回金额 txType = 1005(赎回数量) */ val unDelegation: String? = null, /** * 质押金额 txType = 1003(退回数量) */ val stakingValue: String? = null, /** * 领取数量 单位von 1LAT(ETH)=1000000000000000000von(wei) */ val totalReward: String? = null, /** * 交易备注 */ val remark: String? = null, )
The class already has specific descriptions for each field, so I quit repeating them. Then we create the AtonApi class under the api package, the code is as follows:
interface IAtonApi { @POST("/app/v0760/transaction/list") suspend fun GetTransactionList(@retrofit2.http.Body param: TransactionListTO): ApiResponse<List<Transaction>> } object AtonApi { private var url: String = "https://aton-dev.platon.network" fun <T> Create(tClass: Class<T>): T { val okHttpClient = OkHttpClient.Builder().build() val retrofit: Retrofit = Retrofit.Builder().baseUrl(AtonApi.url) .addConverterFactory(JacksonConverterFactory.create()) .client(okHttpClient) .build() return retrofit.create(tClass) } private val atonApi = Create(IAtonApi::class.java) /** * 获取交易列表 */ suspend fun GetTransactionList(param: TransactionListTO): ApiResponse<List<Transaction>> { return atonApi.GetTransactionList(param) } }
This allows us to get a list of transactions for the wallet address. The logic for displaying the transaction list is in the adapter/TransactionListAdapter, whereas that for displaying the transaction content is in the OnRender method of the TransactionViewHolder class of the TransactionListAdapter. The code is as follows:
override fun OnRender(position: Int) { val transaction = adapter.GetData(position) val transactionStatus = TransactionStatus.getTransactionStatusByIndex(transaction.txReceiptStatus) // 是否是发送者 val isSender: Boolean = transaction.from == WalletManager.GetCurWalletAddress() // 获取交易类型 val transactionType = TransactionType.getTxTypeByValue(NumberParserUtils.parseInt(transaction.txType)) // 判断当前是不是发送 val isSend = isSender && transactionType !== TransactionType.UNDELEGATE && transactionType !== TransactionType.EXIT_VALIDATOR && transactionType !== TransactionType.CLAIM_REWARDS // 判断交易额是否为0 val isValueZero: Boolean = !BigDecimalUtil.isBiggerThanZero(transaction.value) // 如果交易额为0,交易失败或者交易超时,则交易额显示为灰色 val isTransactionGray = isValueZero || transactionStatus === TransactionStatus.FAILED || transactionStatus === TransactionStatus.TIMEOUT if (isTransactionGray) { binding.transactionAmount.text = AmountUtil.formatAmountText(transaction.value) binding.transactionAmount.setTextColor(ResourceUtil.GetColor(R.color.color_b6bbd0)) } else if (isSend) { //发送LAT,数量显示为红色 binding.transactionAmount.text = "-${AmountUtil.formatAmountText(transaction.value)}" binding.transactionAmount.setTextColor(ResourceUtil.GetColor(R.color.color_ff3b3b)) } else { // 接收LAT数量显示为绿色 binding.transactionAmount.text = "+${AmountUtil.formatAmountText(transaction.value)}" binding.transactionAmount.setTextColor(ResourceUtil.GetColor(R.color.color_19a20e)) } if (isTransactionGray) { binding.transactionTime.setTextColor(ResourceUtil.GetColor(R.color.color_61646e_50)) } else { binding.transactionTime.setTextColor(ResourceUtil.GetColor(R.color.color_61646e)) } binding.transactionStatus.setTextColor(ResourceUtil.GetColor(R.color.color_000000)) binding.transactionStatus.text = getTxTDesc(transaction, isSend) binding.transactionTime.text = DateUtil.format( transaction.timestamp, DateUtil.DATETIME_FORMAT_PATTERN_WITH_SECOND ) binding.transactionStatus.visibility = if (transactionStatus === TransactionStatus.PENDING) View.GONE else View.VISIBLE // 如果是转账的,则显示箭头 if (transactionType == TransactionType.TRANSFER) { binding.transactionStatusIV.setImageResource(if (isSender) R.mipmap.icon_send_transation else R.mipmap.icon_receive_transaction) } else { binding.transactionStatusIV.setImageResource(if (isSend) R.mipmap.icon_delegate else R.mipmap.icon_undelegate) } }
Here the corresponding TransactionType, and TransactionStatus are placed in the entity package.
Build the Transaction List Showing Page
Please refer to res/layout/activity_send_transaction.xml to get the specific layout code. The page effect is as follows:
Then create the SendTransactionActivity class under the activity package, the main code is as follows:
/** * 获取当前钱包余额 */ private suspend fun getBalance() { val walletAddress = WalletManager.GetCurWalletAddress() val amount = PlatonApi.GetBalance(walletAddress) this.balance = amount binding.walletBalance.text = "当前余额: ${amount.toString()} LAT" } /** * 获取手续费 */ private suspend fun getGasFee() { val gasfee = PlatonApi.GetGasPrice().multiply(BigInteger("21000")) binding.feeAmount.text = AmountUtil.formatAmountText(gasfee.toString()) } override fun initEvent() { binding.sendBtn.setOnClickListener { // 1, 通过密码获取钱包秘钥 val password = binding.password.text.toString() if (TextUtils.isEmpty(password)) { ToastUtil.showLongToast(this, "密码不能为空") return@setOnClickListener } val privateKey = WalletManager.GetWalletPrivateKey(WalletManager.GetCurrentWalletName(), password) if (TextUtils.isEmpty(privateKey)) { ToastUtil.showLongToast(this, "密码错误") return@setOnClickListener } val toAddress = binding.walletAddress.text.toString() if (TextUtils.isEmpty(toAddress)) { ToastUtil.showLongToast(this, "接收地址不能为空") return@setOnClickListener } val lat = binding.walletAmount.text.toString() lifecycleScope.launch { // 发送lat val receiptInfo = PlatonApi.SendLATTO(privateKey, toAddress, lat.toLong()) ToastUtil.showLongToast(this@SendTransactionActivity, "发送成功") delay(1000) this@SendTransactionActivity.finish() } } }
The main logics of this category are:
1, Show wallet balance
2, Show current fees
3, Get the secret key by password, then call PlatonApi.SendLATTO to send LAT
Here we demonstrate sending 35 LATs to: lat1tgu6pts6nhmneu5zhqly3rc83r6y6ecfmde03e,
Then you can see the record in the transaction list like below:
Well, that’s all for this chapter, in the next chapter we will learn to build delegate functions.
This chapter covers a lot of new code,so if you have questions, please feel free to ask.
The URL of github: https://github.com/DQTechnology/Platon_DevGuideProject
This article is reproduced from https://forum.latticex.foundation/t/topic/5985