Skip to content

Commit 1a41142

Browse files
committed
Check rent exemption for Solana
1 parent 6bbea39 commit 1a41142

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
@@ -708,6 +708,7 @@
708708
"insufficient_balance" = "Insufficient balance";
709709
"insufficient_balance_symbol" = "Insufficient %@";
710710
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
711+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
711712
"insufficient_transaction_fee" = "Insufficient transaction fee";
712713
"interface_style" = "Interface Style";
713714
"invalid_address" = "Invalid Address";
@@ -1143,6 +1144,7 @@
11431144
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11441145
"resend_code" = "Resend code";
11451146
"resend_code_pending" = "Resend code in %@";
1147+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11461148
"reset" = "Reset";
11471149
"reset_link" = "Reset Link";
11481150
"restore" = "Restore";

Mixin/Resources/es.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@
708708
"insufficient_balance" = "Insufficient balance";
709709
"insufficient_balance_symbol" = "Insufficient %@";
710710
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
711+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
711712
"insufficient_transaction_fee" = "Tarifa de transacción insuficiente";
712713
"interface_style" = "Estilo de interfaz";
713714
"invalid_address" = "Invalid Address";
@@ -1143,6 +1144,7 @@
11431144
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11441145
"resend_code" = "Reenviar codigo";
11451146
"resend_code_pending" = "Reenviar código en %@";
1147+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11461148
"reset" = "Reiniciar";
11471149
"reset_link" = "Restablecer enlace";
11481150
"restore" = "Restaurar";

Mixin/Resources/ja.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@
708708
"insufficient_balance" = "残高が不足しています";
709709
"insufficient_balance_symbol" = "Insufficient %@";
710710
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
711+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
711712
"insufficient_transaction_fee" = "取引手数料が不足しています";
712713
"interface_style" = "外観モード";
713714
"invalid_address" = "Invalid Address";
@@ -1143,6 +1144,7 @@
11431144
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11441145
"resend_code" = "コードを再送する";
11451146
"resend_code_pending" = "%@ 後にコードを再送";
1147+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11461148
"reset" = "リセット";
11471149
"reset_link" = "リンクを取り消す";
11481150
"restore" = "復元";

Mixin/Resources/ru.lproj/Localizable.strings

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@
708708
"insufficient_balance" = "Insufficient balance";
709709
"insufficient_balance_symbol" = "Insufficient %@";
710710
"insufficient_fee_description" = "You need %1$@ on the %2$@ network to cover the network fee";
711+
"insufficient_sol_for_sending_spl_token" = "Insufficient SOL balance. Reserve at least %@ SOL.";
711712
"insufficient_transaction_fee" = "Недостаточная комиссия за транзакцию";
712713
"interface_style" = "Стиль интерфейса";
713714
"invalid_address" = "Invalid Address";
@@ -1143,6 +1144,7 @@
11431144
"request_rejected_reason_another_request_in_process" = "Another request is in process, please retry after current request is finished";
11441145
"resend_code" = "Отправить код еще раз";
11451146
"resend_code_pending" = "Повторно отправить код в %@";
1147+
"reserve_sol_for_rent" = "Reserve at least %@ SOL for the rent";
11461148
"reset" = "Перезагрузить";
11471149
"reset_link" = "Сбросить ссылку";
11481150
"restore" = "Восстановить";

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@
708708
"insufficient_balance" = "余额不足";
709709
"insufficient_balance_symbol" = "%@ 余额不足";
710710
"insufficient_fee_description" = "需要 %1$@ 以支付 %2$@ 网络费用";
711+
"insufficient_sol_for_sending_spl_token" = "SOL 余额不足,请至少预留 %@ SOL。";
711712
"insufficient_transaction_fee" = "手续费不足";
712713
"interface_style" = "外观";
713714
"invalid_address" = "无效的地址";
@@ -1143,6 +1144,7 @@
11431144
"request_rejected_reason_another_request_in_process" = "正在处理另一请求,请完成当前请求后重试";
11441145
"resend_code" = "重发验证码";
11451146
"resend_code_pending" = "%@ 后重新发送验证码";
1147+
"reserve_sol_for_rent" = "请预留至少 %@ SOL 以支付租金";
11461148
"reset" = "重置";
11471149
"reset_link" = "重置邀请链接";
11481150
"restore" = "恢复";
@@ -1240,7 +1242,7 @@
12401242
"send_message" = "发消息";
12411243
"send_photo" = "发送 1 张图片";
12421244
"send_photo_count" = "发送 %d 张图片";
1243-
"send_sol_for_rent" = "发送至少 %@ SOL 以支付租金";
1245+
"send_sol_for_rent" = "请发送至少 %@ SOL 以支付租金";
12441246
"send_this_location" = "发送这个位置";
12451247
"send_to" = "发送给 %@";
12461248
"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
@@ -708,6 +708,7 @@
708708
"insufficient_balance" = "餘額不足";
709709
"insufficient_balance_symbol" = "%@ 餘額不足";
710710
"insufficient_fee_description" = "需要 %1$@ 以支付 %2$@ 網路費用";
711+
"insufficient_sol_for_sending_spl_token" = "SOL 餘額不足,請至少預留 %@ SOL。";
711712
"insufficient_transaction_fee" = "手續費不足";
712713
"interface_style" = "外觀";
713714
"invalid_address" = "無效的地址";
@@ -1143,6 +1144,7 @@
11431144
"request_rejected_reason_another_request_in_process" = "正在處理另一請求,請完成當前請求後重試";
11441145
"resend_code" = "重發驗證碼";
11451146
"resend_code_pending" = "%@ 後重新發送驗證碼";
1147+
"reserve_sol_for_rent" = "請預留至少 %@ SOL 以支付租金";
11461148
"reset" = "重置";
11471149
"reset_link" = "重置邀請連結";
11481150
"restore" = "恢復";
@@ -1240,7 +1242,7 @@
12401242
"send_message" = "發訊息";
12411243
"send_photo" = "傳送 1 張圖片";
12421244
"send_photo_count" = "傳送 %d 張圖片";
1243-
"send_sol_for_rent" = "傳送至少 %@ SOL 以支付租金";
1245+
"send_sol_for_rent" = "請傳送至少 %@ SOL 以支付租金";
12441246
"send_this_location" = "傳送這個位置";
12451247
"send_to" = "傳送給 %@";
12461248
"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
@@ -232,6 +232,9 @@ final class SolanaTransferWithCustomRespondingOperation: ArbitraryTransactionSol
232232

233233
final class SolanaTransferToAddressOperation: SolanaTransferOperation {
234234

235+
@MainActor
236+
private(set) var receiverAccountExists: Bool?
237+
235238
private let payment: Web3SendingTokenToAddressPayment
236239
private let decimalAmount: Decimal
237240
private let amount: UInt64
@@ -262,17 +265,24 @@ final class SolanaTransferToAddressOperation: SolanaTransferOperation {
262265

263266
override func loadFee() async throws -> DisplayFee {
264267
let tokenProgramID = try await RouteAPI.solanaGetAccountInfo(pubkey: payment.token.assetKey).owner
265-
let ata = try Solana.tokenAssociatedAccount(
266-
walletAddress: payment.toAddress,
267-
mint: payment.token.assetKey,
268-
tokenProgramID: tokenProgramID
269-
)
270-
let receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: ata)
271-
let createAccount = !receiverAccountExists
268+
let receiverAccountExists: Bool
269+
let createAssociatedTokenAccountForReceiver: Bool
270+
if payment.sendingNativeToken {
271+
receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: payment.toAddress)
272+
createAssociatedTokenAccountForReceiver = false
273+
} else {
274+
let ata = try Solana.tokenAssociatedAccount(
275+
walletAddress: payment.toAddress,
276+
mint: payment.token.assetKey,
277+
tokenProgramID: tokenProgramID
278+
)
279+
receiverAccountExists = try await RouteAPI.solanaAccountExists(pubkey: ata)
280+
createAssociatedTokenAccountForReceiver = !receiverAccountExists
281+
}
272282
let transaction = try Solana.Transaction(
273283
from: payment.fromAddress.destination,
274284
to: payment.toAddress,
275-
createAssociatedTokenAccountForReceiver: createAccount,
285+
createAssociatedTokenAccountForReceiver: createAssociatedTokenAccountForReceiver,
276286
tokenProgramID: tokenProgramID,
277287
mint: payment.token.assetKey,
278288
amount: amount,
@@ -289,7 +299,8 @@ final class SolanaTransferToAddressOperation: SolanaTransferOperation {
289299
let fee = DisplayFee(tokenAmount: tokenAmount, fiatMoneyAmount: fiatMoneyAmount)
290300

291301
await MainActor.run {
292-
self.createAssociatedTokenAccountForReceiver = createAccount
302+
self.receiverAccountExists = receiverAccountExists
303+
self.createAssociatedTokenAccountForReceiver = createAssociatedTokenAccountForReceiver
293304
self.tokenProgramID = tokenProgramID
294305
self.priorityFee = priorityFee
295306
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
@@ -14,7 +14,6 @@ enum Solana {
1414

1515
static let lamportsPerSOL = Decimal(SOLANA_LAMPORTS_PER_SOL)
1616
static let microLamportsPerLamport: Decimal = 1_000_000
17-
static let accountCreationCost: Decimal = 0.002_039_28
1817
static let keyPairCount = 64
1918

2019
static func publicKey(seed: Data) throws -> String {
@@ -154,8 +153,7 @@ extension Solana {
154153
priorityFee: PriorityFee?,
155154
token: Web3Token
156155
) throws {
157-
let isSendingSOL = token.chainID == ChainID.solana
158-
&& (token.assetKey == Web3Token.AssetKey.sol || token.assetKey == Web3Token.AssetKey.wrappedSOL)
156+
let isSendingSOL = token.chainID == ChainID.solana && token.assetKey == Web3Token.AssetKey.sol
159157
let solanaPriorityFee: SolanaPriorityFee? = if let fee = priorityFee {
160158
SolanaPriorityFee(price: fee.unitPrice, limit: fee.unitLimit)
161159
} else {
@@ -243,3 +241,77 @@ extension Solana {
243241
}
244242

245243
}
244+
245+
extension Solana {
246+
247+
enum RentExemptionFailedReason {
248+
249+
case reserveSOLForRent(Decimal)
250+
case sendSOLForRent(Decimal)
251+
case insufficientSOL(requiredAmount: Decimal)
252+
253+
var localizedDescription: String {
254+
switch self {
255+
case .reserveSOLForRent(let amount):
256+
R.string.localizable.reserve_sol_for_rent(
257+
CurrencyFormatter.localizedString(
258+
from: amount,
259+
format: .precision,
260+
sign: .never
261+
)
262+
)
263+
case .sendSOLForRent(let amount):
264+
R.string.localizable.send_sol_for_rent(
265+
CurrencyFormatter.localizedString(
266+
from: amount,
267+
format: .precision,
268+
sign: .never
269+
)
270+
)
271+
case .insufficientSOL(let requiredAmount):
272+
R.string.localizable.insufficient_sol_for_sending_spl_token(
273+
CurrencyFormatter.localizedString(
274+
from: requiredAmount,
275+
format: .precision,
276+
sign: .never
277+
)
278+
)
279+
}
280+
}
281+
282+
}
283+
284+
enum RentExemptionValue {
285+
static let systemAccount: Decimal = 0.00089088
286+
static let tokenAccount: Decimal = 0.00203928
287+
}
288+
289+
static func checkRentExemptionForSOLTransfer(
290+
sendingAmount: Decimal,
291+
feeAmount: Decimal,
292+
senderSOLBalance: Decimal,
293+
receiverAccountExists: Bool
294+
) -> RentExemptionFailedReason? {
295+
if senderSOLBalance - sendingAmount - feeAmount < RentExemptionValue.systemAccount {
296+
.reserveSOLForRent(RentExemptionValue.systemAccount)
297+
} else if !receiverAccountExists && sendingAmount < RentExemptionValue.tokenAccount {
298+
.sendSOLForRent(RentExemptionValue.tokenAccount)
299+
} else {
300+
nil
301+
}
302+
}
303+
304+
static func checkRentExemptionForSPLTokenTransfer(
305+
senderSOLBalance: Decimal,
306+
feeAmount: Decimal,
307+
receiverAccountExists: Bool
308+
) -> RentExemptionFailedReason? {
309+
let minBalance = RentExemptionValue.systemAccount + RentExemptionValue.tokenAccount + feeAmount
310+
if receiverAccountExists || senderSOLBalance >= minBalance {
311+
return nil
312+
} else {
313+
return .insufficientSOL(requiredAmount: minBalance)
314+
}
315+
}
316+
317+
}

0 commit comments

Comments
 (0)