Files
Daniel Dimitrov dff020b80d feat: add rudimentary decoding for stake txs
Instead of showing “unknown operation” we now display the type of stake
tx.
2024-09-26 11:39:08 +02:00

906 lines
36 KiB
Swift

//
// TransactionDetailCellBuilder.swift
// Multisig
//
// Created by Dmitry Bespalov on 02.12.20.
// Copyright © 2020 Gnosis Ltd. All rights reserved.
//
import Foundation
import UIKit
import SwiftCryptoTokenFormatter
import SwiftUI
import Version
import Solidity
import SafeWeb3
class TransactionDetailCellBuilder {
private (set) weak var vc: UIViewController!
private (set) weak var tableView: UITableView!
// needed for proper safe selection for known addresses functionality. Also used to select the block explorer url.
private(set) var chain: Chain
private lazy var dateFormatter: DateFormatter = {
let d = DateFormatter()
d.locale = .autoupdatingCurrent
d.dateStyle = .medium
d.timeStyle = .medium
return d
}()
var result: [UITableViewCell] = []
init(vc: UIViewController, tableView: UITableView, chain: Chain) {
self.vc = vc
self.tableView = tableView
self.chain = chain
tableView.registerCell(DetailExpandableTextCell.self)
tableView.registerCell(DetailConfirmationCell.self)
tableView.registerCell(DetailAccountCell.self)
tableView.registerCell(DetailAccountAndTextCell.self)
tableView.registerCell(DetailMultiAccountsCell.self)
tableView.registerCell(DetailDisclosingCell.self)
tableView.registerCell(ExternalURLCell.self)
tableView.registerCell(DetailTransferInfoCell.self)
tableView.registerCell(DetailRejectionInfoCell.self)
tableView.registerCell(DetailStatusCell.self)
tableView.registerCell(WarningTableViewCell.self)
}
func build(_ tx: SCGModels.TransactionDetails) -> [UITableViewCell] {
result = []
buildTransaction(tx)
return result
}
func buildTransaction(_ tx: SCGModels.TransactionDetails) {
let isCreationTx = buildCreationTx(tx)
if !isCreationTx {
buildWarning(tx)
buildHeader(tx)
buildAssetContract(tx)
buildStatus(tx)
buildMultisigInfo(tx)
buildExecutedDate(tx)
buildAdvanced(tx)
buildOpenInExplorer(hash: tx.txHash)
}
}
func buildCreationTx(_ tx: SCGModels.TransactionDetails) -> Bool {
guard case let SCGModels.TxInfo.creation(creationTx) = tx.txInfo else {
return false
}
buildStatus(tx)
buildTransactionHash(creationTx)
buildCreatorAddress(creationTx)
buildMasterCopyUsed(creationTx)
buildFactoryUsed(creationTx)
buildCreatedDate(tx.executedAt)
buildOpenInExplorer(hash: creationTx.transactionHash)
return true
}
func buildFactoryUsed(_ creationTx: SCGModels.TxInfo.Creation) {
if let factory = creationTx.factory?.value.address {
let info = NamingPolicy.name(for: factory, chainId: chain.id!)
address(factory,
label: info.name ?? creationTx.factory?.name ?? "Unknown",
title: "Factory used",
imageUri: info.imageUri ?? creationTx.factory?.logoUri,
browseURL: chain.browserURL(address: factory.checksummed),
prefix: chain.shortName)
} else {
text("No factory used", title: "Factory used", expandableTitle: nil, copyText: nil)
}
}
func buildMasterCopyUsed(_ creationTx: SCGModels.TxInfo.Creation) {
if let implementation = creationTx.implementation?.value.address {
let info = NamingPolicy.name(for: implementation, chainId: chain.id!)
address(
implementation,
label: info.name ?? creationTx.implementation?.name ?? "Unknown",
title: "Base contract used",
imageUri: info.imageUri ?? creationTx.implementation?.logoUri,
browseURL: chain.browserURL(address: implementation.checksummed),
prefix: chain.shortName)
} else {
text(
"Not available",
title: "Base contract used",
expandableTitle: nil,
copyText: nil)
}
}
func buildTransactionHash(_ creationTx: SCGModels.TxInfo.Creation) {
text(
creationTx.transactionHash.description,
title: "Transaction hash",
expandableTitle: nil,
copyText: creationTx.transactionHash.description)
}
func buildCreatorAddress(_ creationTx: SCGModels.TxInfo.Creation) {
let info = NamingPolicy.name(for: creationTx.creator, chainId: chain.id!)
let creator = creationTx.creator.value.address
return address(creator,
label: info.name,
title: "Creator address",
imageUri: info.imageUri,
browseURL: chain.browserURL(address: creator.checksummed),
prefix: chain.shortName)
}
func buildHeader(_ tx: SCGModels.TransactionDetails) {
switch tx.txInfo {
case .transfer(let transferTx):
let isOutgoing = transferTx.direction == .outgoing
var address: Address
var label: String?
var addressLogoUri: URL?
if isOutgoing {
address = transferTx.recipient.value.address
(label, addressLogoUri) = NamingPolicy.name(for: transferTx.recipient, chainId: chain.id!)
} else {
address = transferTx.sender.value.address
(label, addressLogoUri) = NamingPolicy.name(for: transferTx.sender, chainId: chain.id!)
}
switch transferTx.transferInfo {
case .erc20(let erc20Tx):
buildTransferHeader(
address: address,
label: label,
addressLogoUri: addressLogoUri,
isOutgoing: isOutgoing,
status: tx.txStatus,
value: erc20Tx.value.value,
decimals: erc20Tx.decimals,
symbol: erc20Tx.tokenSymbol ?? "ERC20",
logoUri: erc20Tx.logoUri)
case .erc721(let erc721Tx):
buildTransferHeader(
address: address,
label: label,
addressLogoUri: addressLogoUri,
isOutgoing: isOutgoing,
status: tx.txStatus,
value: 1,
decimals: 0,
symbol: erc721Tx.tokenSymbol ?? "NFT",
logoUri: erc721Tx.logoUri,
logo: UIImage(named: "ico-nft-placeholder"),
detail: erc721Tx.tokenId.description)
case .nativeCoin(let nativeCoinTx):
let coin = Chain.nativeCoin!
buildTransferHeader(
address: address,
label: label,
addressLogoUri: addressLogoUri,
isOutgoing: isOutgoing,
status: tx.txStatus,
value: nativeCoinTx.value.value,
decimals: UInt64(coin.decimals),
symbol: coin.symbol!,
logoUri: coin.logoUrl.map(\.absoluteString))
case .unknown:
buildTransferHeader(
address: address,
label: label,
addressLogoUri: addressLogoUri,
isOutgoing: isOutgoing,
status: tx.txStatus,
value: nil,
decimals: nil,
symbol: "",
logoUri: nil)
}
case .settingsChange(let settingsTx):
switch settingsTx.settingsInfo {
case .setFallbackHandler(let fallbackTx):
let handler: Address = fallbackTx.handler.value.address
var (label, imageUri) = NamingPolicy.name(for: fallbackTx.handler, chainId: chain.id!)
if label == nil {
label = handler.isZero ? "Not set" : "Unknown"
}
address(
handler,
label: label,
title: "Set fallback handler:",
imageUri: imageUri,
browseURL: chain.browserURL(address: handler.checksummed),
prefix: chain.shortName,
showDelegateWarning: DelegateWarningCalculator.isUntrusted(txData: tx.txData)
)
case .addOwner(let addOwnerTx):
let (label, imgageUri) = NamingPolicy.name(for: addOwnerTx.owner, chainId: chain.id!)
addressAndText(
addOwnerTx.owner.value.address,
label: label,
imageUri: imgageUri,
addressTitle: "Add owner:",
text: "\(addOwnerTx.threshold)",
textTitle: "Change required confirmations:",
browseURL: chain.browserURL(address: addOwnerTx.owner.value.address.checksummed),
prefix: chain.shortName)
case .removeOwner(let removeOwnerTx):
let (label, imageUri) = NamingPolicy.name(for: removeOwnerTx.owner, chainId: chain.id!)
addressAndText(
removeOwnerTx.owner.value.address,
label: label,
imageUri: imageUri,
addressTitle: "Remove owner:",
text: "\(removeOwnerTx.threshold)",
textTitle: "Change required confirmations:",
browseURL: chain.browserURL(address: removeOwnerTx.owner.value.address.checksummed),
prefix: chain.shortName)
case .swapOwner(let swapOwnerTx):
let (oldOwnerLabel, oldOwnerImgageUri) = NamingPolicy.name(for: swapOwnerTx.oldOwner, chainId: chain.id!)
let (newOwnerLabel, newOwnerImgageUri) = NamingPolicy.name(for: swapOwnerTx.newOwner, chainId: chain.id!)
addresses(
[(address: swapOwnerTx.oldOwner.value.address,
label: oldOwnerLabel,
imageUri: oldOwnerImgageUri,
title: "Remove owner:",
browseURL: chain.browserURL(address: swapOwnerTx.oldOwner.value.address.checksummed),
prefix: chain.shortName),
(address: swapOwnerTx.newOwner.value.address,
label: newOwnerLabel,
imageUri: newOwnerImgageUri,
title: "Add owner:",
browseURL: chain.browserURL(address: swapOwnerTx.newOwner.value.address.checksummed),
prefix: chain.shortName)
])
case .changeThreshold(let thresholdTx):
text(
"\(thresholdTx.threshold)",
title: "Change required confirmations:",
expandableTitle: nil,
copyText: nil)
case .changeImplementation(let implementationTx):
let implementation = implementationTx.implementation.value.address
var (label, imageUri) = NamingPolicy.name(for: implementationTx.implementation, chainId: chain.id!)
if label == nil {
label = implementationTx.implementation.name ?? "Unknown"
}
address(implementation,
label: label,
title: "New mastercopy:",
imageUri: imageUri,
browseURL: chain.browserURL(address: implementation.checksummed),
prefix: chain.shortName,
showDelegateWarning: DelegateWarningCalculator.isUntrusted(txData: tx.txData)
)
case .enableModule(let moduleTx):
let (label, imageUri) = NamingPolicy.name(for: moduleTx.module, chainId: chain.id!)
let module = moduleTx.module.value.address
address(module,
label: label,
title: "Enable module:",
imageUri: imageUri,
browseURL: chain.browserURL(address: module.checksummed),
prefix: chain.shortName,
showDelegateWarning: DelegateWarningCalculator.isUntrusted(txData: tx.txData)
)
case .disableModule(let moduleTx):
let (label, imageUri) = NamingPolicy.name(for: moduleTx.module, chainId: chain.id!)
let module = moduleTx.module.value.address
address(module,
label: label,
title: "Disable module:",
imageUri: imageUri,
browseURL: chain.browserURL(address: module.checksummed),
prefix: chain.shortName,
showDelegateWarning: DelegateWarningCalculator.isUntrusted(txData: tx.txData)
)
case .setGuard(let guardTx):
let (label, imageUri) = NamingPolicy.name(for: guardTx.guard, chainId: chain.id!)
let guardContract = guardTx.guard.value.address
address(guardContract,
label: label,
title: "Set guard:",
imageUri: imageUri,
browseURL: chain.browserURL(address: guardContract.checksummed),
prefix: chain.shortName,
showDelegateWarning: DelegateWarningCalculator.isUntrusted(txData: tx.txData)
)
case .deleteGuard:
text("Delete Guard", title: "Settings change:", expandableTitle: nil, copyText: nil)
case .unknown:
text("Unknown operation", title: "Settings change:", expandableTitle: nil, copyText: nil)
}
case .custom(let customTx):
let (label, addressLogoUri) = NamingPolicy.name(for: customTx.to, chainId: chain.id!)
var title = "Interact with: "
let amount = Int256(customTx.value.value)
if customTx.value != "0" {
let nativeCoinDecimals = chain.nativeCurrency!.decimals
let decimalAmount = BigDecimal(amount, Int(nativeCoinDecimals))
let amount = TokenFormatter().string(
from: decimalAmount,
decimalSeparator: Locale.autoupdatingCurrent.decimalSeparator ?? ".",
thousandSeparator: Locale.autoupdatingCurrent.groupingSeparator ?? ",",
forcePlusSign: false
)
if let currencySymbol = chain.nativeCurrency?.symbol {
title = "Interact with (Send \(amount) \(currencySymbol) to): "
} else {
title = "Interact with (Send \(amount) to): "
}
}
address(customTx.to.value.address,
label: label,
title: title,
imageUri: addressLogoUri,
browseURL: chain.browserURL(address: customTx.to.value.address.checksummed),
prefix: chain.shortName,
showDelegateWarning: DelegateWarningCalculator.isUntrusted(txData: tx.txData)
)
buildActions(tx)
buildHexData(tx)
case .rejection(_):
if case let SCGModels.TransactionDetails.DetailedExecutionInfo.multisig(multisigInfo)? = tx.detailedExecutionInfo {
rejectionHeader(nonce: multisigInfo.nonce.value, isQueued: tx.txStatus.isInQueue)
} else {
rejectionHeader(nonce: nil, isQueued: tx.txStatus.isInQueue)
}
case .swapOrder(let orderInfo):
text(orderInfo.swapOrderDisplayName, title: "Contract Interaction", expandableTitle: nil, copyText: nil)
externalURL(text: "Order details", url: orderInfo.explorerUrl)
case .swapTransfer(let orderInfo):
text(orderInfo.swapTransferDisplayName, title: "Contract Interaction", expandableTitle: nil, copyText: nil)
externalURL(text: "Order details", url: orderInfo.explorerUrl)
case .twapOrder(let order):
text(order.displayName, title: "Contract Interaction", expandableTitle: nil, copyText: nil)
case .stake(let stake):
text(stake.displayName, title: "Contract Interaction", expandableTitle: nil, copyText: nil)
case .creation(_):
// ignore
fallthrough
case .unknown:
// ignore
break
}
}
// MARK: - Transaction Screen Pieces
func buildTransferHeader(
address: Address,
label: String?,
addressLogoUri: URL?,
isOutgoing: Bool,
status txStatus: SCGModels.TxStatus,
value: UInt256?,
decimals: UInt64?,
symbol: String,
logoUri: String?,
logo: UIImage? = UIImage(named: "ico-token-placeholder"),
detail: String? = nil
) {
let tokenText: String
if let value = value {
let decimalAmount = BigDecimal(Int256(value) * (isOutgoing ? -1 : +1),
decimals.flatMap { Int($0) } ?? 0)
let amount = TokenFormatter().string(
from: decimalAmount,
decimalSeparator: Locale.autoupdatingCurrent.decimalSeparator ?? ".",
thousandSeparator: Locale.autoupdatingCurrent.groupingSeparator ?? ",",
forcePlusSign: true
)
tokenText = "\(amount) \(symbol)"
} else {
tokenText = "Unknown token"
}
let style: GNOTextStyle = isOutgoing ? .headline : .headlineBaseSuccess
let iconURL = logoUri.flatMap { URL(string: $0) }
let alpha: CGFloat = [SCGModels.TxStatus.cancelled, .failed].contains(txStatus) ? 0.5 : 1
transfer(
token: tokenText,
style: style,
icon: logo,
iconURL: iconURL,
alpha: alpha,
detail: detail,
address: address,
label: label,
addressLogoUri: addressLogoUri,
isOutgoing: isOutgoing)
}
func validate(tx: SCGModels.TransactionDetails, safe: Safe) throws {
guard tx.txStatus.isAwatingConfiramtions || tx.txStatus == .awaitingExecution else {
// don't warn outside of signature or execution requests
return
}
// safe has owners
guard let ownersInfo = safe.ownersInfo, !ownersInfo.isEmpty else {
// not enough data for further checks
return
}
let ownerAddresses = ownersInfo.map(\.address)
// transaction has confirmations
guard let txMultisigInfo = tx.multisigInfo else {
// not a kind of transaction we can take a look into
return
}
guard !txMultisigInfo.confirmations.isEmpty else {
throw "Transaction has no confirmations."
}
// all confirming addresses are from safe owners
guard txMultisigInfo.confirmations.allSatisfy({ confirmation in
ownerAddresses.contains(confirmation.signer.value.address)
}) else {
throw "Not all confirmations are from safe owners."
}
// transaction hash is valid
guard var transaction = Transaction(tx: tx),
let contractVersion = safe.semVer,
let chainId = safe.chain?.id
else {
// not enough information for further checks
return
}
transaction.safe = AddressString(stringLiteral: safe.displayAddress)
transaction.safeVersion = contractVersion
transaction.chainId = chainId
guard
let computedSafeTxHash = transaction.safeTransactionHash(),
transaction.safeTxHash == computedSafeTxHash
else {
throw "Invalid safeTxHash. This may be a dangerous transaction."
}
// all confirming signatures are from a confirming addresses
func signer(of signature: Data) -> Address? {
guard signature.count >= 65 else {
// cannot decode signature
return nil
}
let r: Data /* 32 bytes */ = signature[0..<32]
let s: Data /* 32 bytes */ = signature[32..<64]
let v: UInt8 = signature[64]
let contractSignature: UInt8 = 0
let approvedHashSignature: UInt8 = 1
let ethSignSignature: ClosedRange<UInt8> = (31...UInt8.max)
if contractVersion >= Version(1, 1, 0) {
switch v {
case contractSignature, approvedHashSignature:
let owner = (try? Sol.Address(data: r)).map { Address($0) }
return owner
case ethSignSignature:
let hash = computedSafeTxHash.hash
let message = "\u{19}Ethereum Signed Message:\n\(hash.count)".data(using: .utf8)! + hash
let pubKey = try? EthereumPublicKey(
message: message.makeBytes(),
v: EthereumQuantity(quantity: (BigUInt(v) - 4) - 27),
r: EthereumQuantity(r.makeBytes()),
s: EthereumQuantity(s.makeBytes())
)
let owner = pubKey.map(\.address).map(Address.init)
return owner
default:
let message = transaction.encodeTransactionData()
let pubKey = try? EthereumPublicKey(
message: message.makeBytes(),
v: EthereumQuantity(quantity: v >= 27 ? BigUInt(v) - 27 : BigUInt(v)),
r: EthereumQuantity(r.makeBytes()),
s: EthereumQuantity(s.makeBytes())
)
let owner = pubKey.map(\.address).map(Address.init)
return owner
}
} else {
switch v {
case contractSignature, approvedHashSignature:
let owner = (try? Sol.Address(data: r)).map { Address($0) }
return owner
default:
let message = transaction.encodeTransactionData()
let pubKey = try? EthereumPublicKey(
message: message.makeBytes(),
v: EthereumQuantity(quantity: v >= 27 ? BigUInt(v) - 27 : BigUInt(v)),
r: EthereumQuantity(r.makeBytes()),
s: EthereumQuantity(s.makeBytes())
)
let owner = pubKey.map(\.address).map(Address.init)
return owner
}
}
}
guard txMultisigInfo.confirmations.allSatisfy({ confirmation in
signer(of: confirmation.signature.data) == confirmation.signer.value.address
}) else {
throw "Not all signatures are from safe's owners. This may be a dangerous transaction."
}
}
func buildWarning(_ tx: SCGModels.TransactionDetails) {
do {
guard let aSafe = Safe.by(address: tx.safeAddress.description, chainId: chain.id!) else {
return
}
try validate(tx: tx, safe: aSafe)
} catch {
let cell = newCell(WarningTableViewCell.self)
cell.set(title: "Warning!", description: error.localizedDescription)
result.append(cell)
}
}
func buildActions(_ tx: SCGModels.TransactionDetails) {
if let dataDecoded = tx.txData?.dataDecoded {
let addressInfoIndex = tx.txData?.addressInfoIndex
if dataDecoded.method == "multiSend",
let param = dataDecoded.parameters?.first,
param.type == "bytes",
case let SCGModels.DataDecoded.Parameter.ValueDecoded.multiSend(multiSendTxs)? = param.valueDecoded {
disclosure(text: "Multisend (\(multiSendTxs.count) actions)") { [weak self] in
guard let `self` = self else { return }
let root = MultiSendListTableViewController(transactions: multiSendTxs,
addressInfoIndex: addressInfoIndex,
chain: self.chain)
let vc = RibbonViewController(rootViewController: root)
self.vc.show(vc, sender: self)
}
} else {
disclosure(text: "Action (\(dataDecoded.method))") { [weak self] in
guard let `self` = self else { return }
let root = ActionDetailViewController(decoded: dataDecoded,
addressInfoIndex: addressInfoIndex,
chain: self.chain,
data: tx.txData?.hexData)
let vc = RibbonViewController(rootViewController: root)
self.vc.show(vc, sender: self)
}
}
}
}
func buildHexData(_ tx: SCGModels.TransactionDetails) {
if let data = tx.txData?.hexData {
text("\(data)", title: "Data", expandableTitle: "\(data.data.count) Bytes", copyText: "\(data)")
}
}
func buildAssetContract(_ tx: SCGModels.TransactionDetails) {
switch tx.txInfo {
case .transfer(let transferTx):
switch transferTx.transferInfo {
case .erc721(let erc721Tx):
let tokenAddress = erc721Tx.tokenAddress.address
address(tokenAddress,
label: "Asset Contract",
title: nil,
browseURL: chain.browserURL(address: tokenAddress.checksummed),
prefix: chain.shortName)
default:
break
}
default:
break
}
}
func buildStatus(_ tx: SCGModels.TransactionDetails) {
var type = ""
var tag: String = ""
var icon: UIImage?
var imageURL: URL?
switch tx.txInfo {
case .transfer(let transferTx):
let isOutgoing = transferTx.direction == .outgoing
type = isOutgoing ? "Outgoing transfer" : "Incoming transfer"
icon = isOutgoing ? UIImage(named: "ico-outgoing-tx") : UIImage(named: "ico-incomming-tx")?.withTintColor(.success)
case .settingsChange(_):
type = "Modify settings"
icon = UIImage(named: "ico-settings-tx")
case .custom(_):
if let safeAppInfo = tx.safeAppInfo {
type = safeAppInfo.name
imageURL = URL(string: safeAppInfo.logoUri)
tag = "App"
icon = UIImage(named: "ico-custom-tx")
} else {
type = "Contract interaction"
icon = UIImage(named: "ico-custom-tx")
}
case .rejection(_):
type = "On-chain rejection"
icon = UIImage(named: "ico-rejection-tx")
case .creation(_):
type = "Safe Account created"
icon = UIImage(named: "ico-settings-tx")
case .swapOrder(let order):
type = order.swapOrderDisplayName
icon = UIImage(named: "ico-custom-tx")
case .swapTransfer(let order):
type = order.swapTransferDisplayName
icon = UIImage(named: "ico-custom-tx")
case .twapOrder(let order):
type = order.displayName
icon = UIImage(named: "ico-custom-tx")
case .stake(let stake):
type = stake.displayName
icon = UIImage(named: "ico-custom-tx")
case .unknown:
type = "Unknown operation"
icon = UIImage(named: "ico-custom-tx")
}
status(tx.txStatus, type: type, icon: icon, iconURL: imageURL, address: nil, tag: tag)
}
func buildMultisigInfo(_ tx: SCGModels.TransactionDetails) {
guard case let SCGModels.TransactionDetails.DetailedExecutionInfo.multisig(multisigInfo)? =
tx.detailedExecutionInfo else {
return
}
confirmation(multisigInfo.confirmations.map { $0.signer.value.address },
required: Int(multisigInfo.confirmationsRequired),
status: tx.txStatus,
executor: multisigInfo.executor?.value.address, isRejectionTx: tx.txInfo.isRejection)
buildCreatedDate(multisigInfo.submittedAt)
}
func buildCreatedDate(_ date: Date?) {
guard let date = date else { return }
text(
dateFormatter.string(from: date),
title: "Created:",
expandableTitle: nil,
copyText: nil)
}
func buildExecutedDate(_ tx: SCGModels.TransactionDetails) {
guard let executedAt = tx.executedAt else { return }
text(
dateFormatter.string(from: executedAt),
title: "Executed:",
expandableTitle: nil,
copyText: nil)
}
func buildAdvanced(_ tx: SCGModels.TransactionDetails) {
switch tx.txInfo {
case .transfer(let transferTx):
guard transferTx.direction != .incoming else { return }
fallthrough
default:
disclosure(text: "Advanced") { [weak self] in
guard let `self` = self else { return }
let vc = AdvancedTransactionDetailsViewController(tx, chain: self.chain)
let ribbonVC = RibbonViewController(rootViewController: vc)
self.vc.show(ribbonVC, sender: self)
}
break
}
}
func buildOpenInExplorer(hash: DataString?) {
guard
let txHash = hash?.description
else { return }
let url = chain.browserURL(txHash: txHash)
externalURL(text: "View on block explorer", url: url)
}
// MARK: - Cell Builder
func disclosure(text: String, action: @escaping () -> Void) {
let cell = newCell(DetailDisclosingCell.self)
cell.action = action
cell.setText(text)
result.append(cell)
}
func externalURL(text: String, url: URL) {
let cell = newCell(ExternalURLCell.self)
cell.setText(text, url: url)
result.append(cell)
}
func text(_ text: String, title: String, expandableTitle: String?, copyText: String?) {
let cell = newCell(DetailExpandableTextCell.self)
cell.tableView = tableView
cell.setTitle(title)
cell.setText(text)
cell.setCopyText(copyText)
cell.setExpandableTitle(expandableTitle)
result.append(cell)
}
func confirmation(_ confirmations: [Address], required: Int, status: SCGModels.TxStatus, executor: Address?, isRejectionTx: Bool) {
let cell = newCell(DetailConfirmationCell.self)
cell.setConfirmations(confirmations,
chain: chain,
required: required,
status: status,
executor: executor,
isRejectionTx: isRejectionTx)
result.append(cell)
}
func status(_ status: SCGModels.TxStatus, type: String, icon: UIImage?, iconURL: URL? = nil, address: AddressString? = nil, tag: String = "") {
let cell = newCell(DetailStatusCell.self)
cell.setTitle(type)
cell.setStatus(status)
cell.set(tag: tag)
if let imageURL = iconURL, let placeholderAddress = address {
cell.set(contractImageUrl: imageURL, contractAddress: placeholderAddress)
} else if let imageURL = iconURL {
cell.set(imageUrl: imageURL, placeholder: icon)
} else if let image = icon {
cell.setIcon(image)
} else if let placeholderAddress = address {
cell.set(contractAddress: placeholderAddress)
}
result.append(cell)
}
func transfer(token: String,
style: GNOTextStyle,
icon: UIImage?,
iconURL: URL?,
alpha: CGFloat,
detail: String?,
address: Address,
label: String?, // todo: rename
addressLogoUri: URL?,
isOutgoing: Bool) {
let cell = newCell(DetailTransferInfoCell.self)
cell.setToken(text: token, style: style)
cell.setToken(image: iconURL, placeholder: icon)
cell.setToken(alpha: alpha)
cell.setDetail(detail)
cell.setAddress(address,
label: label,
imageUri: addressLogoUri,
browseURL: chain.browserURL(address: address.checksummed),
prefix: chain.shortName)
cell.setOutgoing(isOutgoing)
result.append(cell)
}
func rejectionHeader(nonce: UInt256?, isQueued: Bool) {
let cell = newCell(DetailRejectionInfoCell.self)
cell.setNonce(nonce, showHelpLink: isQueued)
result.append(cell)
}
func address(_ address: Address,
label: String?,
title: String?,
imageUri: URL? = nil,
browseURL: URL? = nil,
prefix: String? = nil,
showDelegateWarning: Bool = false) {
let cell = newCell(DetailAccountCell.self)
cell.setAccount(address: address,
label: label,
title: title,
imageUri: imageUri,
browseURL: browseURL,
prefix: prefix,
showDelegateWarning: showDelegateWarning)
result.append(cell)
}
func addressAndText(_ address: Address,
label: String?,
imageUri: URL?,
addressTitle: String,
text: String,
textTitle: String,
browseURL: URL?,
prefix: String?) {
let cell = newCell(DetailAccountAndTextCell.self)
cell.setText(title: textTitle, details: text)
cell.setAccount(address: address,
label: label,
title: addressTitle,
imageUri: imageUri,
browseURL: browseURL,
prefix: prefix)
result.append(cell)
}
func addresses(_ accounts: [(address: Address,
label: String?,
imageUri: URL?,
title: String?,
browseURL: URL?,
prefix: String?)]) {
let cell = newCell(DetailMultiAccountsCell.self)
cell.setAccounts(accounts: accounts)
result.append(cell)
}
func newCell<T: UITableViewCell>(_ cls: T.Type, reuseId: String? = nil) -> T {
tableView.dequeueCell(cls, reuseID: reuseId)
}
}
extension SCGModels.Operation {
static let strings: [Self: String] = [
.call: "call",
.delegate: "delegateCall"
]
var string: String {
Self.strings[self]!
}
}
extension SCGModels.TxInfo {
var isRejection: Bool {
if case SCGModels.TxInfo.rejection(_) = self {
return true
}
return false
}
}