Skip to content
Open
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
2 changes: 2 additions & 0 deletions Mixin/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficient_balance" = "Insufficient balance";
"insufficient_balance_symbol" = "Insufficient %@";
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
"insufficient_transaction_fee" = "Insufficient transaction fee";
"interface_style" = "Interface Style";
"invalid_address" = "Invalid Address";
Expand Down Expand Up @@ -1176,6 +1177,7 @@
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
"resend_code" = "Resend code";
"resend_code_pending" = "Resend code in %@";
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
"reset" = "Reset";
"reset_link" = "Reset Link";
"restore" = "Restore";
Expand Down
2 changes: 2 additions & 0 deletions Mixin/Resources/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficient_balance" = "Insufficient balance";
"insufficient_balance_symbol" = "Insufficient %@";
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
"insufficient_transaction_fee" = "Tarifa de transacción insuficiente";
"interface_style" = "Estilo de interfaz";
"invalid_address" = "Invalid Address";
Expand Down Expand Up @@ -1176,6 +1177,7 @@
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
"resend_code" = "Reenviar codigo";
"resend_code_pending" = "Reenviar código en %@";
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
"reset" = "Reiniciar";
"reset_link" = "Restablecer enlace";
"restore" = "Restaurar";
Expand Down
2 changes: 2 additions & 0 deletions Mixin/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficient_balance" = "残高が不足しています";
"insufficient_balance_symbol" = "Insufficient %@";
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
"insufficient_transaction_fee" = "取引手数料が不足しています";
"interface_style" = "外観モード";
"invalid_address" = "Invalid Address";
Expand Down Expand Up @@ -1176,6 +1177,7 @@
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
"resend_code" = "コードを再送する";
"resend_code_pending" = "%@ 後にコードを再送";
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
"reset" = "リセット";
"reset_link" = "リンクを取り消す";
"restore" = "復元";
Expand Down
2 changes: 2 additions & 0 deletions Mixin/Resources/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficient_balance" = "Insufficient balance";
"insufficient_balance_symbol" = "Insufficient %@";
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
"insufficient_transaction_fee" = "Недостаточная комиссия за транзакцию";
"interface_style" = "Стиль интерфейса";
"invalid_address" = "Invalid Address";
Expand Down Expand Up @@ -1176,6 +1177,7 @@
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
"resend_code" = "Отправить код еще раз";
"resend_code_pending" = "Повторно отправить код в %@";
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
"reset" = "Перезагрузить";
"reset_link" = "Сбросить ссылку";
"restore" = "Восстановить";
Expand Down
4 changes: 3 additions & 1 deletion Mixin/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficient_balance" = "余额不足";
"insufficient_balance_symbol" = "%@ 余额不足";
"insufficient_fee_description" = "需要 %1$@ 以支付 %2$@ 网络费用";
"insufficient_sol_for_sending_spl_token" = "SOL 余额不足,请至少预留 %@ SOL。";
"insufficient_transaction_fee" = "手续费不足";
"interface_style" = "外观";
"invalid_address" = "无效的地址";
Expand Down Expand Up @@ -1176,6 +1177,7 @@
"request_rejected_reason_another_request_in_process" = "正在处理另一请求,请完成当前请求后重试";
"resend_code" = "重发验证码";
"resend_code_pending" = "%@ 后重新发送验证码";
"reserve_sol_for_rent" = "请预留至少 %@ SOL 以支付租金";
"reset" = "重置";
"reset_link" = "重置邀请链接";
"restore" = "恢复";
Expand Down Expand Up @@ -1273,7 +1275,7 @@
"send_message" = "发消息";
"send_photo" = "发送 1 张图片";
"send_photo_count" = "发送 %d 张图片";
"send_sol_for_rent" = "发送至少 %@ SOL 以支付租金";
"send_sol_for_rent" = "请发送至少 %@ SOL 以支付租金";
"send_this_location" = "发送这个位置";
"send_to" = "发送给 %@";
"send_to_address_description" = "转到我地址薄中的地址,地址受到 PIN 保护,避免地址钓鱼攻击。";
Expand Down
4 changes: 3 additions & 1 deletion Mixin/Resources/zh-Hant.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficient_balance" = "餘額不足";
"insufficient_balance_symbol" = "%@ 餘額不足";
"insufficient_fee_description" = "需要 %1$@ 以支付 %2$@ 網路費用";
"insufficient_sol_for_sending_spl_token" = "SOL 餘額不足,請至少預留 %@ SOL。";
"insufficient_transaction_fee" = "手續費不足";
"interface_style" = "外觀";
"invalid_address" = "無效的地址";
Expand Down Expand Up @@ -1176,6 +1177,7 @@
"request_rejected_reason_another_request_in_process" = "正在處理另一請求,請完成當前請求後重試";
"resend_code" = "重發驗證碼";
"resend_code_pending" = "%@ 後重新發送驗證碼";
"reserve_sol_for_rent" = "請預留至少 %@ SOL 以支付租金";
"reset" = "重置";
"reset_link" = "重置邀請連結";
"restore" = "恢復";
Expand Down Expand Up @@ -1273,7 +1275,7 @@
"send_message" = "發訊息";
"send_photo" = "傳送 1 張圖片";
"send_photo_count" = "傳送 %d 張圖片";
"send_sol_for_rent" = "傳送至少 %@ SOL 以支付租金";
"send_sol_for_rent" = "請傳送至少 %@ SOL 以支付租金";
"send_this_location" = "傳送這個位置";
"send_to" = "傳送給 %@";
"send_to_address_description" = "轉到我地址薄中的地址,地址受到 PIN 保護,避免地址釣魚攻擊。";
Expand Down
8 changes: 0 additions & 8 deletions Mixin/Service/API/Model/SwapToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,6 @@ extension SwapToken {
}
}

func isEqual(to token: Web3Token) -> Bool {
if address == Web3Token.AssetKey.wrappedSOL && token.assetKey == Web3Token.AssetKey.sol {
true
} else {
address == token.assetKey
}
}

func decimalAmount(nativeAmount: Decimal) -> NSDecimalNumber? {
let nativeAmountNumber = nativeAmount as NSDecimalNumber
return nativeAmountNumber.multiplying(byPowerOf10: -decimals)
Expand Down
29 changes: 20 additions & 9 deletions Mixin/Service/Web3/SolanaTransferOperation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ final class SolanaTransferWithCustomRespondingOperation: ArbitraryTransactionSol

final class SolanaTransferToAddressOperation: SolanaTransferOperation {

@MainActor
private(set) var receiverAccountExists: Bool?

private let payment: Web3SendingTokenToAddressPayment
private let decimalAmount: Decimal
private let amount: UInt64
Expand Down Expand Up @@ -268,17 +271,24 @@ final class SolanaTransferToAddressOperation: SolanaTransferOperation {

override func loadFee() async throws -> DisplayFee {
let tokenProgramID = try await RouteAPI.solanaGetAccountInfo(pubkey: payment.token.assetKey).owner
let ata = try Solana.tokenAssociatedAccount(
walletAddress: payment.toAddress,
mint: payment.token.assetKey,
tokenProgramID: tokenProgramID
)
let receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: ata)
let createAccount = !receiverAccountExists
let receiverAccountExists: Bool
let createAssociatedTokenAccountForReceiver: Bool
if payment.sendingNativeToken {
receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: payment.toAddress)
createAssociatedTokenAccountForReceiver = false
} else {
let ata = try Solana.tokenAssociatedAccount(
walletAddress: payment.toAddress,
mint: payment.token.assetKey,
tokenProgramID: tokenProgramID
)
receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: ata)
createAssociatedTokenAccountForReceiver = !receiverAccountExists
}
let transaction = try Solana.Transaction(
from: payment.fromAddress.destination,
to: payment.toAddress,
createAssociatedTokenAccountForReceiver: createAccount,
createAssociatedTokenAccountForReceiver: createAssociatedTokenAccountForReceiver,
tokenProgramID: tokenProgramID,
mint: payment.token.assetKey,
amount: amount,
Expand All @@ -295,7 +305,8 @@ final class SolanaTransferToAddressOperation: SolanaTransferOperation {
let fee = DisplayFee(tokenAmount: tokenAmount, fiatMoneyAmount: fiatMoneyAmount)

await MainActor.run {
self.createAssociatedTokenAccountForReceiver = createAccount
self.receiverAccountExists = receiverAccountExists
self.createAssociatedTokenAccountForReceiver = createAssociatedTokenAccountForReceiver
self.tokenProgramID = tokenProgramID
self.priorityFee = priorityFee
self.fee = fee
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ enum Web3AddressValidator {
enum Web3TransferValidationResult {
case address(address: String, label: AddressLabel?)
case insufficientBalance(transferring: BalanceRequirement, fee: BalanceRequirement)
case solAmountTooSmall
case rentExemptionFailed(Solana.RentExemptionFailedReason)
case transfer(operation: Web3TransferOperation, toAddressLabel: AddressLabel?)
}

Expand Down Expand Up @@ -84,15 +84,6 @@ enum Web3AddressValidator {
assetID: token.assetID,
destination: link.destination
)
if chain.kind == .solana && payment.sendingNativeToken {
let accountExists = try await RouteAPI.solanaAccountExists(pubkey: address)
if !accountExists && amount < Solana.accountCreationCost {
await MainActor.run {
onSuccess(.solAmountTooSmall)
}
return
}
}
let addressPayment = Web3SendingTokenToAddressPayment(
payment: payment,
toAddress: address,
Expand All @@ -112,6 +103,30 @@ enum Web3AddressValidator {
)
}
let fee = try await operation.loadFee()
if let operation = operation as? SolanaTransferToAddressOperation,
let accountExists = await operation.receiverAccountExists
{
let reason = if payment.sendingNativeToken {
Solana.checkRentExemptionForSOLTransfer(
sendingAmount: amount,
feeAmount: fee.tokenAmount,
senderSOLBalance: payment.token.decimalBalance,
receiverAccountExists: accountExists
)
} else {
Solana.checkRentExemptionForSPLTokenTransfer(
senderSOLBalance: operation.feeToken.decimalBalance,
feeAmount: fee.tokenAmount,
receiverAccountExists: accountExists
)
}
if let reason {
await MainActor.run {
onSuccess(.rentExemptionFailed(reason))
}
return
}
}
let transferRequirement = BalanceRequirement(token: token, amount: amount)
let feeRequirement = BalanceRequirement(token: operation.feeToken, amount: fee.tokenAmount)
let requirements = transferRequirement.merging(with: feeRequirement)
Expand Down
78 changes: 75 additions & 3 deletions Mixin/UserInterface/Controllers/Web3/Model/Solana.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ enum Solana {

static let lamportsPerSOL = Decimal(SOLANA_LAMPORTS_PER_SOL)
static let microLamportsPerLamport: Decimal = 1_000_000
static let accountCreationCost: Decimal = 0.002_039_28
static let keyPairCount = 64

static func publicKey(seed: Data) throws -> String {
Expand Down Expand Up @@ -172,8 +171,7 @@ extension Solana {
priorityFee: PriorityFee?,
token: Web3Token
) throws {
let isSendingSOL = token.chainID == ChainID.solana
&& (token.assetKey == Web3Token.AssetKey.sol || token.assetKey == Web3Token.AssetKey.wrappedSOL)
let isSendingSOL = token.chainID == ChainID.solana && token.assetKey == Web3Token.AssetKey.sol
let solanaPriorityFee: SolanaPriorityFee? = if let fee = priorityFee {
SolanaPriorityFee(price: fee.unitPrice, limit: fee.unitLimit)
} else {
Expand Down Expand Up @@ -261,3 +259,77 @@ extension Solana {
}

}

extension Solana {

enum RentExemptionFailedReason {

case reserveSOLForRent(Decimal)
case sendSOLForRent(Decimal)
case insufficientSOL(requiredAmount: Decimal)

var localizedDescription: String {
switch self {
case .reserveSOLForRent(let amount):
R.string.localizable.reserve_sol_for_rent(
CurrencyFormatter.localizedString(
from: amount,
format: .precision,
sign: .never
)
)
case .sendSOLForRent(let amount):
R.string.localizable.send_sol_for_rent(
CurrencyFormatter.localizedString(
from: amount,
format: .precision,
sign: .never
)
)
case .insufficientSOL(let requiredAmount):
R.string.localizable.insufficient_sol_for_sending_spl_token(
CurrencyFormatter.localizedString(
from: requiredAmount,
format: .precision,
sign: .never
)
)
}
}

}

enum RentExemptionValue {
static let systemAccount: Decimal = 0.00089088
static let tokenAccount: Decimal = 0.00203928
}

static func checkRentExemptionForSOLTransfer(
sendingAmount: Decimal,
feeAmount: Decimal,
senderSOLBalance: Decimal,
receiverAccountExists: Bool
) -> RentExemptionFailedReason? {
if senderSOLBalance - sendingAmount - feeAmount < RentExemptionValue.systemAccount {
.reserveSOLForRent(RentExemptionValue.systemAccount)
} else if !receiverAccountExists && sendingAmount < RentExemptionValue.tokenAccount {
.sendSOLForRent(RentExemptionValue.tokenAccount)
} else {
nil
}
}

static func checkRentExemptionForSPLTokenTransfer(
senderSOLBalance: Decimal,
feeAmount: Decimal,
receiverAccountExists: Bool
) -> RentExemptionFailedReason? {
let minBalance = RentExemptionValue.systemAccount + RentExemptionValue.tokenAccount + feeAmount
if receiverAccountExists || senderSOLBalance >= minBalance {
return nil
} else {
return .insufficientSOL(requiredAmount: minBalance)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,8 @@ final class Web3TokenReceiverViewController: TokenReceiverViewController {
)
transfer.manipulateNavigationStackOnFinished = true
Web3PopupCoordinator.enqueue(popup: .request(transfer))
case .solAmountTooSmall:
let cost = CurrencyFormatter.localizedString(
from: Solana.accountCreationCost,
format: .precision,
sign: .never,
)
let description = R.string.localizable.send_sol_for_rent(cost)
self.showError(description: description)
case let .rentExemptionFailed(reason):
self.showError(description: reason.localizedDescription)
}
} onFailure: { [weak self] error in
self?.showError(description: error.localizedDescription)
Expand Down
Loading