Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MOB-1956 Sending non native erc20 tokens #509

Merged
merged 24 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
2917EDDD2A6F25AB004EAB31 /* Web3ContractABI in Frameworks */ = {isa = PBXBuildFile; productRef = 2917EDDC2A6F25AB004EAB31 /* Web3ContractABI */; };
2917EDDF2A6F25AB004EAB31 /* Web3PromiseKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2917EDDE2A6F25AB004EAB31 /* Web3PromiseKit */; };
291A9C6A29E444CF00527FF1 /* WC_v1+v2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291A9C6929E444CF00527FF1 /* WC_v1+v2.swift */; };
291AD2452BD5265E006B8096 /* EVMCryptoSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291AD2442BD5265E006B8096 /* EVMCryptoSender.swift */; };
291AD2462BD5265E006B8096 /* EVMCryptoSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291AD2442BD5265E006B8096 /* EVMCryptoSender.swift */; };
291AD2472BD5265E006B8096 /* EVMCryptoSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 291AD2442BD5265E006B8096 /* EVMCryptoSender.swift */; };
295506F02954D14400EDA42D /* WCConnectedAppsStorageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 295506EF2954D14400EDA42D /* WCConnectedAppsStorageV2.swift */; };
2959AC4729A61916006387DD /* UDWallet+SigningTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2959AC4629A61916006387DD /* UDWallet+SigningTransaction.swift */; };
296BB61F2A309DD90068EEEC /* Encrypting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296BB61E2A309DD90068EEEC /* Encrypting.swift */; };
Expand Down Expand Up @@ -2552,6 +2555,7 @@
290A60412950A89900882109 /* WalletConnectServiceV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletConnectServiceV2.swift; sourceTree = "<group>"; };
29150FBA2975DA4D00169A1A /* PushSubscriberInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSubscriberInfo.swift; sourceTree = "<group>"; };
291A9C6929E444CF00527FF1 /* WC_v1+v2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WC_v1+v2.swift"; sourceTree = "<group>"; };
291AD2442BD5265E006B8096 /* EVMCryptoSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EVMCryptoSender.swift; sourceTree = "<group>"; };
295506EF2954D14400EDA42D /* WCConnectedAppsStorageV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WCConnectedAppsStorageV2.swift; sourceTree = "<group>"; };
2959AC4629A61916006387DD /* UDWallet+SigningTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UDWallet+SigningTransaction.swift"; sourceTree = "<group>"; };
296BB61E2A309DD90068EEEC /* Encrypting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encrypting.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7314,6 +7318,7 @@
C6B45DAF2BBA97B600B44C33 /* CryptoSender+Entities.swift */,
29AA6AA72BAC389D00D24FB5 /* CryptoSender.swift */,
29018C8C2BACB7BC0004545D /* JRPC_Client.swift */,
291AD2442BD5265E006B8096 /* EVMCryptoSender.swift */,
);
path = CryptoSender;
sourceTree = "<group>";
Expand Down Expand Up @@ -9048,6 +9053,7 @@
C635193C28D03F8F00FC6AF8 /* ChooseReverseResolutionDomainViewController.swift in Sources */,
C6386843285C2D4E000F98C4 /* UpgradeToPolygonTutorial.swift in Sources */,
C628E37227FDE1D60044E408 /* UDSubtitleLabel.swift in Sources */,
291AD2452BD5265E006B8096 /* EVMCryptoSender.swift in Sources */,
C671E3E42902817000A2B3A0 /* DomainProfileGeneralInfoCell.swift in Sources */,
C6E041A42951A8C20080F8E3 /* ConnectedAppImageView.swift in Sources */,
C666E09929CB3F960003DECB /* FirebaseDomain.swift in Sources */,
Expand Down Expand Up @@ -9916,6 +9922,7 @@
C66FFD052B01E2EE00988A6F /* CoreDataMessagingStorageServiceTests.swift in Sources */,
C617CFA82B9EDA2F00663516 /* TestableWalletNFTsService.swift in Sources */,
C60610E829A7469A005DC0D5 /* WCRequestsHandlingServiceTests.swift in Sources */,
291AD2462BD5265E006B8096 /* EVMCryptoSender.swift in Sources */,
C6DF9875290F83020098733A /* UnsConfigManagerTests.swift in Sources */,
C6DF986B290F83020098733A /* WalletAutonamingTests.swift in Sources */,
C6DF9871290F83020098733A /* ResolutionInitTests.swift in Sources */,
Expand Down Expand Up @@ -10306,6 +10313,7 @@
C6D646A82B1ED15A00D724AC /* PublicProfileCryptoListView.swift in Sources */,
C630E4AD2B7F4B8D008F3269 /* FlippedUpsideDownModifier.swift in Sources */,
C6D646E22B1ED49D00D724AC /* TableViewSelectionCell.swift in Sources */,
291AD2472BD5265E006B8096 /* EVMCryptoSender.swift in Sources */,
C6FBCAA32B91C6AC00BA39DF /* HomeExploreRecentProfilesSectionView.swift in Sources */,
C6C8F8262B217CDD00A9834D /* UDWallet+RecoveryType.swift in Sources */,
C6D647182B1ED83800D724AC /* CollectionTextFooterReusableView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ extension UDWallet {
return try appContext.walletConnectServiceV2.handle(response: response)
}

func signViaWalletConnectTransaction(tx: EthereumTransaction, chainId: Int) async throws -> String {
func sendViaWalletConnectTransaction(tx: EthereumTransaction, chainId: Int) async throws -> String {
let wc2Sessions = try getWC2Session()
let response = try await appContext.walletConnectServiceV2.sendSignTx(sessions: wc2Sessions,
chainId: chainId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
// Unified container for the token amount.
// Init with units, gwei's or wei's
// Read in units, gwei's or wei's
struct EVMTokenAmount {
struct EVMCoinAmount: OnChainCountable {
static let Billion = 1_000_000_000.0
private let gweiTotal: Double

Expand Down Expand Up @@ -41,14 +41,44 @@ struct EVMTokenAmount {
var wei: UDBigUInt { // can only be integer and may be very big
UDBigUInt(gweiTotal * Self.Billion)
}

func getOnChainCountable() -> UDBigUInt {
self.wei
}
}

protocol DecimalPointFloatable {
var decimals: UInt8 { get }
}

struct ERC20Token: DecimalPointFloatable, OnChainCountable {
var elementaryUnits: UDBigUInt
var decimals: UInt8

init(units: Double, decimals: UInt8) {
self.decimals = decimals
self.elementaryUnits = UDBigUInt(units * pow(10, Double(decimals)))
}

func getOnChainCountable() -> UDBigUInt {
elementaryUnits
}

var units: Double {
Double(elementaryUnits) / pow(10, Double(decimals))
}
}

protocol OnChainCountable {
func getOnChainCountable() -> UDBigUInt
}

struct EstimatedGasPrices {
let normal: EVMTokenAmount
let fast: EVMTokenAmount
let urgent: EVMTokenAmount
let normal: EVMCoinAmount
let fast: EVMCoinAmount
let urgent: EVMCoinAmount

func getPriceForSpeed(_ txSpeed: CryptoSendingSpec.TxSpeed) -> EVMTokenAmount {
func getPriceForSpeed(_ txSpeed: CryptoSendingSpec.TxSpeed) -> EVMCoinAmount {
switch txSpeed {
case .normal:
return normal
Expand Down Expand Up @@ -80,25 +110,93 @@ struct CryptoSendingSpec {
}

let token: CryptoSender.SupportedToken
let amount: EVMTokenAmount
let amount: OnChainCountable
let speed: TxSpeed

init(token: CryptoSender.SupportedToken, amount: EVMTokenAmount, speed: TxSpeed = .normal) {
init(token: CryptoSender.SupportedToken, units: Double, speed: TxSpeed = .normal) throws {

switch token {
case .eth, .matic: self.amount = EVMCoinAmount(units: units)
default: self.amount = ERC20Token(units: units, decimals: try token.getContractDecimals(for: .Ethereum))
}

self.token = token
self.amount = amount
self.speed = speed
}
}

extension CryptoSender {
enum Error: Swift.Error {
case sendingNotSupported
case tokenNotSupportedOnChain
case decimalsNotIdentified
case failedFetchGasPrice
case failedCreateSendTransaction
case insufficientFunds
case invalidAddresses
}

enum SupportedToken: String {
case eth = "ETH"
case matic = "MATIC"
case usdt = "USDT"
case usdc = "USDC"
case bnb = "BNB"
case weth = "WETH"

static let array: [CryptoSender.SupportedToken :
[BlockchainType : (mainnet: String,
testnet: String?,
decimals: UInt8)]] =
[
.usdt: [.Ethereum: (mainnet: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
testnet: "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0", // sepolia
decimals: 6)],

.usdt: [.Matic: (mainnet: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
testnet: nil, // amoy
decimals: 6)],

.bnb: [.Ethereum: (mainnet: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52",
testnet: nil, // sepolia
decimals: 18)],

.bnb: [.Matic: (mainnet: "0x3BA4c387f786bFEE076A58914F5Bd38d668B42c3",
testnet: nil, // amoy
decimals: 18)],

.usdc: [.Ethereum: (mainnet: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
testnet: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", // sepolia
decimals: 6)],

.usdc: [.Matic: (mainnet: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359",
testnet: nil, // amoy
decimals: 6)],

.weth: [.Ethereum: (mainnet: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
testnet: "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9", // sepolia
decimals: 18)],

.weth: [.Matic: (mainnet: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619",
testnet: nil, // amoy
decimals: 18)],
rommex marked this conversation as resolved.
Show resolved Hide resolved
]

func getContractAddress(for chain: ChainSpec) throws -> HexAddress {
guard let addresses = Self.array[self]?[chain.blockchainType] else {
throw CryptoSender.Error.tokenNotSupportedOnChain
}
guard let contract = chain.env == .mainnet ? addresses.mainnet : addresses.testnet else {
throw CryptoSender.Error.tokenNotSupportedOnChain
}
return contract
}

func getContractDecimals(for chainType: BlockchainType) throws -> UInt8 {
guard let decimals = Self.array[self]?[chainType]?.decimals else {
throw CryptoSender.Error.decimalsNotIdentified
}
return decimals
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,133 +6,43 @@
//

import Foundation
import Boilertalk_Web3
import BigInt

typealias UDBigUInt = BigUInt

struct CryptoSender: CryptoSenderProtocol {

let wallet: UDWallet

func canSendCrypto(token: CryptoSender.SupportedToken, chainType: BlockchainType) -> Bool {
func canSendCrypto(token: CryptoSender.SupportedToken, chain: ChainSpec) -> Bool {
// only native tokens supported for Ethereum and Polygon
return NativeCryptoSender(wallet: wallet).canSendCrypto(token: token, chainType: chainType)
return NativeCoinCryptoSender(wallet: wallet).canSendCrypto(token: token, chain: chain) || TokenCryptoSender(wallet: wallet).canSendCrypto(token: token, chain: chain)
}

func sendCrypto(crypto: CryptoSendingSpec, chain: ChainSpec, toAddress: HexAddress) async throws -> String {
let cryptoSender: CryptoSenderProtocol = NativeCryptoSender(wallet: wallet)
return try await cryptoSender.sendCrypto(crypto: crypto, chain: chain, toAddress: toAddress)

}

func computeGasFeeFrom(maxCrypto: CryptoSendingSpec, on chain: ChainSpec, toAddress: HexAddress) async throws -> EVMTokenAmount {
let cryptoSender: CryptoSenderProtocol = NativeCryptoSender(wallet: wallet)
return try await cryptoSender.computeGasFeeFrom(maxCrypto: maxCrypto,
on: chain,
toAddress: toAddress)
}

func fetchGasPrices(on chain: ChainSpec) async throws -> EstimatedGasPrices {
let cryptoSender: CryptoSenderProtocol = NativeCryptoSender(wallet: wallet)
return try await cryptoSender.fetchGasPrices(on: chain)
}
}

struct NativeCryptoSender: CryptoSenderProtocol {
static let defaultSendTxGasPrice: BigUInt = 21_000

let wallet: UDWallet

func canSendCrypto(token: CryptoSender.SupportedToken, chainType: BlockchainType) -> Bool {
// only native tokens supported
return (token == CryptoSender.SupportedToken.eth && chainType == .Ethereum) ||
(token == CryptoSender.SupportedToken.matic && chainType == .Matic)
}

func sendCrypto(crypto: CryptoSendingSpec,
chain: ChainSpec,
toAddress: HexAddress) async throws -> String {
guard canSendCrypto(token: crypto.token, chainType: chain.blockchainType) else {
throw CryptoSender.Error.sendingNotSupported
let cryptoSender: CryptoSenderProtocol = NativeCoinCryptoSender(wallet: wallet)
if cryptoSender.canSendCrypto(token: crypto.token, chain: chain) {
return try await cryptoSender.sendCrypto(crypto: crypto, chain: chain, toAddress: toAddress)
}

let tx = try await createNativeSendTransaction(crypto: crypto,
fromAddress: self.wallet.address,
toAddress: toAddress,
chainId: chain.id)

guard wallet.walletState != .externalLinked else {
let response = try await wallet.signViaWalletConnectTransaction(tx: tx, chainId: chain.id)
return response
let cryptoSender2: CryptoSenderProtocol = TokenCryptoSender(wallet: wallet)
if cryptoSender2.canSendCrypto(token: crypto.token, chain: chain) {
return try await cryptoSender2.sendCrypto(crypto: crypto, chain: chain, toAddress: toAddress)
}


let hash = try await JRPC_Client.instance.sendTx(transaction: tx, udWallet: self.wallet, chainIdInt: chain.id)
return hash
throw CryptoSender.Error.sendingNotSupported
}

func computeGasFeeFrom(maxCrypto: CryptoSendingSpec,
on chain: ChainSpec,
toAddress: HexAddress) async throws -> EVMTokenAmount {

guard canSendCrypto(token: maxCrypto.token, chainType: chain.blockchainType) else {
throw CryptoSender.Error.sendingNotSupported

func computeGasFeeFrom(maxCrypto: CryptoSendingSpec, on chain: ChainSpec, toAddress: HexAddress) async throws -> EVMCoinAmount {
let cryptoSender: CryptoSenderProtocol = NativeCoinCryptoSender(wallet: wallet)
if cryptoSender.canSendCrypto(token: maxCrypto.token, chain: chain) {
return try await cryptoSender.computeGasFeeFrom(maxCrypto: maxCrypto, on: chain, toAddress: toAddress)
}

let transaction = try await createNativeSendTransaction(crypto: maxCrypto,
fromAddress: self.wallet.address,
toAddress: toAddress,
chainId: chain.id)

guard let gasPriceWei = transaction.gasPrice?.quantity else {
throw CryptoSender.Error.failedFetchGasPrice
let cryptoSender2: CryptoSenderProtocol = TokenCryptoSender(wallet: wallet)
if cryptoSender2.canSendCrypto(token: maxCrypto.token, chain: chain) {
return try await cryptoSender2.computeGasFeeFrom(maxCrypto: maxCrypto, on: chain, toAddress: toAddress)
}
let gasPrice = EVMTokenAmount(wei: gasPriceWei)

let gas = transaction.gas?.quantity ?? Self.defaultSendTxGasPrice
let gasFee = EVMTokenAmount(gwei: gasPrice.gwei * Double(gas))
return gasFee
throw CryptoSender.Error.sendingNotSupported
}

func fetchGasPrices(on chain: ChainSpec) async throws -> EstimatedGasPrices {
try await fetchGasPrices(chainId: chain.id)
}

// Private methods

private func createNativeSendTransaction(crypto: CryptoSendingSpec,
fromAddress: HexAddress,
toAddress: HexAddress,
chainId: Int) async throws -> EthereumTransaction {
let nonce: EthereumQuantity = try await JRPC_Client.instance.fetchNonce(address: fromAddress,
chainId: chainId)
let speedBasedGasPrice = try await fetchGasPrice(chainId: chainId, for: crypto.speed)

let sender = EthereumAddress(hexString: fromAddress)
let receiver = EthereumAddress(hexString: toAddress)

var transaction = EthereumTransaction(nonce: nonce,
gasPrice: try EthereumQuantity(speedBasedGasPrice.wei),
gas: try EthereumQuantity(Self.defaultSendTxGasPrice),
from: sender,
to: receiver,
value: try EthereumQuantity(crypto.amount.wei)
)

if let gasEstimate = try? await JRPC_Client.instance.fetchGasLimit(transaction: transaction, chainId: chainId) {
transaction.gas = gasEstimate
}
return transaction
}

private func fetchGasPrice(chainId: Int, for speed: CryptoSendingSpec.TxSpeed) async throws -> EVMTokenAmount {
let prices: EstimatedGasPrices = try await fetchGasPrices(chainId: chainId)
return prices.getPriceForSpeed(speed)
}

private func fetchGasPrices(chainId: Int) async throws -> EstimatedGasPrices {
// here routes to Status or Infura source
try await NetworkService().fetchInfuraGasPrices(chainId: chainId)
let cryptoSender: CryptoSenderProtocol = NativeCoinCryptoSender(wallet: wallet)
return try await cryptoSender.fetchGasPrices(on: chain)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ protocol CryptoSenderProtocol {
/// - token: tokan name
/// - chain: chain
/// - Returns: true if the sending is supported
func canSendCrypto(token: CryptoSender.SupportedToken, chainType: BlockchainType) -> Bool
func canSendCrypto(token: CryptoSender.SupportedToken, chain: ChainSpec) -> Bool

/// Create TX, send it to the chain and store it to the storage as 'pending'.
/// Method fails if sending TX failed. Otherwise it returns TX hash
Expand All @@ -36,7 +36,7 @@ protocol CryptoSenderProtocol {
/// - Returns: Amount of crypto that must be deducted from maxCrypto as the gas fee in the future tx, in token units
func computeGasFeeFrom(maxCrypto: CryptoSendingSpec,
on chain: ChainSpec,
toAddress: HexAddress) async throws -> EVMTokenAmount
toAddress: HexAddress) async throws -> EVMCoinAmount

func fetchGasPrices(on chain: ChainSpec) async throws -> EstimatedGasPrices
}
Loading