Skip to content

Commit cf0601b

Browse files
committed
Check rent exemption for Solana
1 parent 8edd402 commit cf0601b

File tree

14 files changed

+200
-104
lines changed

14 files changed

+200
-104
lines changed

Mixin/Resources/en.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
"insufficient_balance" = "Insufficient balance";
721721
"insufficient_balance_symbol" = "Insufficient %@";
722722
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
723+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
723724
"insufficient_transaction_fee" = "Insufficient transaction fee";
724725
"interface_style" = "Interface Style";
725726
"invalid_address" = "Invalid Address";
@@ -1176,6 +1177,7 @@
11761177
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11771178
"resend_code" = "Resend code";
11781179
"resend_code_pending" = "Resend code in %@";
1180+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11791181
"reset" = "Reset";
11801182
"reset_link" = "Reset Link";
11811183
"restore" = "Restore";

Mixin/Resources/es.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
"insufficient_balance" = "Insufficient balance";
721721
"insufficient_balance_symbol" = "Insufficient %@";
722722
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
723+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
723724
"insufficient_transaction_fee" = "Tarifa de transacción insuficiente";
724725
"interface_style" = "Estilo de interfaz";
725726
"invalid_address" = "Invalid Address";
@@ -1176,6 +1177,7 @@
11761177
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11771178
"resend_code" = "Reenviar codigo";
11781179
"resend_code_pending" = "Reenviar código en %@";
1180+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11791181
"reset" = "Reiniciar";
11801182
"reset_link" = "Restablecer enlace";
11811183
"restore" = "Restaurar";

Mixin/Resources/ja.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
"insufficient_balance" = "残高が不足しています";
721721
"insufficient_balance_symbol" = "Insufficient %@";
722722
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
723+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
723724
"insufficient_transaction_fee" = "取引手数料が不足しています";
724725
"interface_style" = "外観モード";
725726
"invalid_address" = "Invalid Address";
@@ -1176,6 +1177,7 @@
11761177
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11771178
"resend_code" = "コードを再送する";
11781179
"resend_code_pending" = "%@ 後にコードを再送";
1180+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11791181
"reset" = "リセット";
11801182
"reset_link" = "リンクを取り消す";
11811183
"restore" = "復元";

Mixin/Resources/ru.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
"insufficient_balance" = "Insufficient balance";
721721
"insufficient_balance_symbol" = "Insufficient %@";
722722
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
723+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
723724
"insufficient_transaction_fee" = "Недостаточная комиссия за транзакцию";
724725
"interface_style" = "Стиль интерфейса";
725726
"invalid_address" = "Invalid Address";
@@ -1176,6 +1177,7 @@
11761177
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11771178
"resend_code" = "Отправить код еще раз";
11781179
"resend_code_pending" = "Повторно отправить код в %@";
1180+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11791181
"reset" = "Перезагрузить";
11801182
"reset_link" = "Сбросить ссылку";
11811183
"restore" = "Восстановить";

Mixin/Resources/zh-Hans.lproj/Localizable.strings

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
"insufficient_balance" = "余额不足";
721721
"insufficient_balance_symbol" = "%@ 余额不足";
722722
"insufficient_fee_description" = "需要 %1$@ 以支付 %2$@ 网络费用";
723+
"insufficient_sol_for_sending_spl_token" = "SOL 余额不足,请至少预留 %@ SOL。";
723724
"insufficient_transaction_fee" = "手续费不足";
724725
"interface_style" = "外观";
725726
"invalid_address" = "无效的地址";
@@ -1176,6 +1177,7 @@
11761177
"request_rejected_reason_another_request_in_process" = "正在处理另一请求,请完成当前请求后重试";
11771178
"resend_code" = "重发验证码";
11781179
"resend_code_pending" = "%@ 后重新发送验证码";
1180+
"reserve_sol_for_rent" = "请预留至少 %@ SOL 以支付租金";
11791181
"reset" = "重置";
11801182
"reset_link" = "重置邀请链接";
11811183
"restore" = "恢复";
@@ -1273,7 +1275,7 @@
12731275
"send_message" = "发消息";
12741276
"send_photo" = "发送 1 张图片";
12751277
"send_photo_count" = "发送 %d 张图片";
1276-
"send_sol_for_rent" = "发送至少 %@ SOL 以支付租金";
1278+
"send_sol_for_rent" = "请发送至少 %@ SOL 以支付租金";
12771279
"send_this_location" = "发送这个位置";
12781280
"send_to" = "发送给 %@";
12791281
"send_to_address_description" = "转到我地址薄中的地址,地址受到 PIN 保护,避免地址钓鱼攻击。";

Mixin/Resources/zh-Hant.lproj/Localizable.strings

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
"insufficient_balance" = "餘額不足";
721721
"insufficient_balance_symbol" = "%@ 餘額不足";
722722
"insufficient_fee_description" = "需要 %1$@ 以支付 %2$@ 網路費用";
723+
"insufficient_sol_for_sending_spl_token" = "SOL 餘額不足,請至少預留 %@ SOL。";
723724
"insufficient_transaction_fee" = "手續費不足";
724725
"interface_style" = "外觀";
725726
"invalid_address" = "無效的地址";
@@ -1176,6 +1177,7 @@
11761177
"request_rejected_reason_another_request_in_process" = "正在處理另一請求,請完成當前請求後重試";
11771178
"resend_code" = "重發驗證碼";
11781179
"resend_code_pending" = "%@ 後重新發送驗證碼";
1180+
"reserve_sol_for_rent" = "請預留至少 %@ SOL 以支付租金";
11791181
"reset" = "重置";
11801182
"reset_link" = "重置邀請連結";
11811183
"restore" = "恢復";
@@ -1273,7 +1275,7 @@
12731275
"send_message" = "發訊息";
12741276
"send_photo" = "傳送 1 張圖片";
12751277
"send_photo_count" = "傳送 %d 張圖片";
1276-
"send_sol_for_rent" = "傳送至少 %@ SOL 以支付租金";
1278+
"send_sol_for_rent" = "請傳送至少 %@ SOL 以支付租金";
12771279
"send_this_location" = "傳送這個位置";
12781280
"send_to" = "傳送給 %@";
12791281
"send_to_address_description" = "轉到我地址薄中的地址,地址受到 PIN 保護,避免地址釣魚攻擊。";

Mixin/Service/API/Model/SwapToken.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,6 @@ extension SwapToken {
7474
}
7575
}
7676

77-
func isEqual(to token: Web3Token) -> Bool {
78-
if address == Web3Token.AssetKey.wrappedSOL && token.assetKey == Web3Token.AssetKey.sol {
79-
true
80-
} else {
81-
address == token.assetKey
82-
}
83-
}
84-
8577
func decimalAmount(nativeAmount: Decimal) -> NSDecimalNumber? {
8678
let nativeAmountNumber = nativeAmount as NSDecimalNumber
8779
return nativeAmountNumber.multiplying(byPowerOf10: -decimals)

Mixin/Service/Web3/SolanaTransferOperation.swift

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ final class SolanaTransferWithCustomRespondingOperation: ArbitraryTransactionSol
236236

237237
final class SolanaTransferToAddressOperation: SolanaTransferOperation {
238238

239+
@MainActor
240+
private(set) var receiverAccountExists: Bool?
241+
239242
private let payment: Web3SendingTokenToAddressPayment
240243
private let decimalAmount: Decimal
241244
private let amount: UInt64
@@ -268,17 +271,24 @@ final class SolanaTransferToAddressOperation: SolanaTransferOperation {
268271

269272
override func loadFee() async throws -> DisplayFee {
270273
let tokenProgramID = try await RouteAPI.solanaGetAccountInfo(pubkey: payment.token.assetKey).owner
271-
let ata = try Solana.tokenAssociatedAccount(
272-
walletAddress: payment.toAddress,
273-
mint: payment.token.assetKey,
274-
tokenProgramID: tokenProgramID
275-
)
276-
let receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: ata)
277-
let createAccount = !receiverAccountExists
274+
let receiverAccountExists: Bool
275+
let createAssociatedTokenAccountForReceiver: Bool
276+
if payment.sendingNativeToken {
277+
receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: payment.toAddress)
278+
createAssociatedTokenAccountForReceiver = false
279+
} else {
280+
let ata = try Solana.tokenAssociatedAccount(
281+
walletAddress: payment.toAddress,
282+
mint: payment.token.assetKey,
283+
tokenProgramID: tokenProgramID
284+
)
285+
receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: ata)
286+
createAssociatedTokenAccountForReceiver = !receiverAccountExists
287+
}
278288
let transaction = try Solana.Transaction(
279289
from: payment.fromAddress.destination,
280290
to: payment.toAddress,
281-
createAssociatedTokenAccountForReceiver: createAccount,
291+
createAssociatedTokenAccountForReceiver: createAssociatedTokenAccountForReceiver,
282292
tokenProgramID: tokenProgramID,
283293
mint: payment.token.assetKey,
284294
amount: amount,
@@ -295,7 +305,8 @@ final class SolanaTransferToAddressOperation: SolanaTransferOperation {
295305
let fee = DisplayFee(tokenAmount: tokenAmount, fiatMoneyAmount: fiatMoneyAmount)
296306

297307
await MainActor.run {
298-
self.createAssociatedTokenAccountForReceiver = createAccount
308+
self.receiverAccountExists = receiverAccountExists
309+
self.createAssociatedTokenAccountForReceiver = createAssociatedTokenAccountForReceiver
299310
self.tokenProgramID = tokenProgramID
300311
self.priorityFee = priorityFee
301312
self.fee = fee

Mixin/UserInterface/Controllers/Wallet/Models/Web3AddressValidator.swift

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ enum Web3AddressValidator {
77
enum Web3TransferValidationResult {
88
case address(address: String, label: AddressLabel?)
99
case insufficientBalance(transferring: BalanceRequirement, fee: BalanceRequirement)
10-
case solAmountTooSmall
10+
case rentExemptionFailed(Solana.RentExemptionFailedReason)
1111
case transfer(operation: Web3TransferOperation, toAddressLabel: AddressLabel?)
1212
}
1313

@@ -84,15 +84,6 @@ enum Web3AddressValidator {
8484
assetID: token.assetID,
8585
destination: link.destination
8686
)
87-
if chain.kind == .solana && payment.sendingNativeToken {
88-
let accountExists = try await RouteAPI.solanaAccountExists(pubkey: address)
89-
if !accountExists && amount < Solana.accountCreationCost {
90-
await MainActor.run {
91-
onSuccess(.solAmountTooSmall)
92-
}
93-
return
94-
}
95-
}
9687
let addressPayment = Web3SendingTokenToAddressPayment(
9788
payment: payment,
9889
toAddress: address,
@@ -112,6 +103,30 @@ enum Web3AddressValidator {
112103
)
113104
}
114105
let fee = try await operation.loadFee()
106+
if let operation = operation as? SolanaTransferToAddressOperation,
107+
let accountExists = await operation.receiverAccountExists
108+
{
109+
let reason = if payment.sendingNativeToken {
110+
Solana.checkRentExemptionForSOLTransfer(
111+
sendingAmount: amount,
112+
feeAmount: fee.tokenAmount,
113+
senderSOLBalance: payment.token.decimalBalance,
114+
receiverAccountExists: accountExists
115+
)
116+
} else {
117+
Solana.checkRentExemptionForSPLTokenTransfer(
118+
senderSOLBalance: operation.feeToken.decimalBalance,
119+
feeAmount: fee.tokenAmount,
120+
receiverAccountExists: accountExists
121+
)
122+
}
123+
if let reason {
124+
await MainActor.run {
125+
onSuccess(.rentExemptionFailed(reason))
126+
}
127+
return
128+
}
129+
}
115130
let transferRequirement = BalanceRequirement(token: token, amount: amount)
116131
let feeRequirement = BalanceRequirement(token: operation.feeToken, amount: fee.tokenAmount)
117132
let requirements = transferRequirement.merging(with: feeRequirement)

Mixin/UserInterface/Controllers/Web3/Model/Solana.swift

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ enum Solana {
1919

2020
static let lamportsPerSOL = Decimal(SOLANA_LAMPORTS_PER_SOL)
2121
static let microLamportsPerLamport: Decimal = 1_000_000
22-
static let accountCreationCost: Decimal = 0.002_039_28
2322
static let keyPairCount = 64
2423

2524
static func publicKey(seed: Data) throws -> String {
@@ -172,8 +171,7 @@ extension Solana {
172171
priorityFee: PriorityFee?,
173172
token: Web3Token
174173
) throws {
175-
let isSendingSOL = token.chainID == ChainID.solana
176-
&& (token.assetKey == Web3Token.AssetKey.sol || token.assetKey == Web3Token.AssetKey.wrappedSOL)
174+
let isSendingSOL = token.chainID == ChainID.solana && token.assetKey == Web3Token.AssetKey.sol
177175
let solanaPriorityFee: SolanaPriorityFee? = if let fee = priorityFee {
178176
SolanaPriorityFee(price: fee.unitPrice, limit: fee.unitLimit)
179177
} else {
@@ -261,3 +259,77 @@ extension Solana {
261259
}
262260

263261
}
262+
263+
extension Solana {
264+
265+
enum RentExemptionFailedReason {
266+
267+
case reserveSOLForRent(Decimal)
268+
case sendSOLForRent(Decimal)
269+
case insufficientSOL(requiredAmount: Decimal)
270+
271+
var localizedDescription: String {
272+
switch self {
273+
case .reserveSOLForRent(let amount):
274+
R.string.localizable.reserve_sol_for_rent(
275+
CurrencyFormatter.localizedString(
276+
from: amount,
277+
format: .precision,
278+
sign: .never
279+
)
280+
)
281+
case .sendSOLForRent(let amount):
282+
R.string.localizable.send_sol_for_rent(
283+
CurrencyFormatter.localizedString(
284+
from: amount,
285+
format: .precision,
286+
sign: .never
287+
)
288+
)
289+
case .insufficientSOL(let requiredAmount):
290+
R.string.localizable.insufficient_sol_for_sending_spl_token(
291+
CurrencyFormatter.localizedString(
292+
from: requiredAmount,
293+
format: .precision,
294+
sign: .never
295+
)
296+
)
297+
}
298+
}
299+
300+
}
301+
302+
enum RentExemptionValue {
303+
static let systemAccount: Decimal = 0.00089088
304+
static let tokenAccount: Decimal = 0.00203928
305+
}
306+
307+
static func checkRentExemptionForSOLTransfer(
308+
sendingAmount: Decimal,
309+
feeAmount: Decimal,
310+
senderSOLBalance: Decimal,
311+
receiverAccountExists: Bool
312+
) -> RentExemptionFailedReason? {
313+
if senderSOLBalance - sendingAmount - feeAmount < RentExemptionValue.systemAccount {
314+
.reserveSOLForRent(RentExemptionValue.systemAccount)
315+
} else if !receiverAccountExists && sendingAmount < RentExemptionValue.tokenAccount {
316+
.sendSOLForRent(RentExemptionValue.tokenAccount)
317+
} else {
318+
nil
319+
}
320+
}
321+
322+
static func checkRentExemptionForSPLTokenTransfer(
323+
senderSOLBalance: Decimal,
324+
feeAmount: Decimal,
325+
receiverAccountExists: Bool
326+
) -> RentExemptionFailedReason? {
327+
let minBalance = RentExemptionValue.systemAccount + RentExemptionValue.tokenAccount + feeAmount
328+
if receiverAccountExists || senderSOLBalance >= minBalance {
329+
return nil
330+
} else {
331+
return .insufficientSOL(requiredAmount: minBalance)
332+
}
333+
}
334+
335+
}

0 commit comments

Comments
 (0)