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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion Balance iOS/Data/FlagsExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension Flags {

static var last_selected_network: EthereumChain {
get {
guard let id = UserDefaults.standard.value(forKey: "last_selected_ethereum_chain") as? Int else { return EthereumChain.ethereum }
guard let id = UserDefaults.standard.value(forKey: "last_selected_ethereum_chain") as? UInt else { return EthereumChain.ethereum }
return EthereumChain(rawValue: id) ?? .ethereum
}
set { UserDefaults.standard.set(newValue.rawValue, forKey: "last_selected_ethereum_chain") }
Expand Down
4 changes: 4 additions & 0 deletions Balance iOS/Resources/Entitlements.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
<array>
<string>group.io.balance</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)io.balance</string>
</array>
</dict>
</plist>
5 changes: 5 additions & 0 deletions Balance iOS/Services/ExtensionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ enum ExtensionService {
self.showErrorAlert("Can't get ETH Address")
return
}
guard let host = request.host else {
self.showErrorAlert("Can't get host")
return
}
Approvals.approve(account: address, on: host)
let response = ResponseToExtension(id: request.id, name: request.name, results: [address], chainId: chain.hexStringId, rpcURL: chain.nodeURLString)
self.respondTo(request: request, response: response, on: controller)
}, on: controller)
Expand Down
1 change: 1 addition & 0 deletions Balance iOS/Services/WalletsManagerExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ extension WalletsManager {
Flags.show_safari_extension_advice = true
AppDelegate.migration()
NotificationCenter.default.post(name: .walletsUpdated, object: nil)
Approvals.destroy()
}
}
141 changes: 113 additions & 28 deletions Balance.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,8 @@ target 'Balance iOS' do
platform :ios, '15.0'
shared_pods
end

target 'Safari iOS' do
platform :ios, '15.0'
shared_pods
end
2 changes: 1 addition & 1 deletion Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,6 @@ SPEC CHECKSUMS:
WalletConnect: 1df75d4355b1cacfc27d7ef2416fae43862d0eb4
Web3Swift.io: 18fd06aed9d56df9c704f9c6f87b06675bb05b53

PODFILE CHECKSUM: 6773ec1c4d4258d4c280c95fc82c4059cc57c2ea
PODFILE CHECKSUM: e039b8dda181cd1d25c39acddcf79565c6a216e0

COCOAPODS: 1.11.2
16 changes: 15 additions & 1 deletion Safari iOS/Resources/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const $ = (query) =>
const render = (query, view) =>
$(query).innerHTML = view(); // todo sanitize?

document.addEventListener(`DOMContentLoaded`, () => {
document.addEventListener(`DOMContentLoaded`, async () => {
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (typeof request.message.address !== `undefined` && typeof request.message.balance !== `undefined` && typeof request.message.chainId !== `undefined` && typeof request.message.connected !== `undefined`) {
if (request.message.connected === true) {
Expand Down Expand Up @@ -156,4 +156,18 @@ document.addEventListener(`DOMContentLoaded`, () => {
message: `get_state`,
},
});



const result = await browser.runtime.sendNativeMessage("io.balance", {id: 1, subject: "getAccounts"});
const log = document.createElement('div')
log.style.color = "black"
log.textContent = JSON.stringify(result)
document.body.appendChild(log)

const chains = await browser.runtime.sendNativeMessage("io.balance", {id: 1, subject: "getChains"});
const log2 = document.createElement('div')
log2.style.color = "black"
log2.textContent = JSON.stringify(chains)
document.body.appendChild(log2)
});
4 changes: 4 additions & 0 deletions Safari iOS/Safari iOS.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
<array>
<string>group.io.balance</string>
</array>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)io.balance</string>
</array>
</dict>
</plist>
34 changes: 21 additions & 13 deletions Safari-Shared/Resources/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,27 @@ if (shouldInjectProvider()) {
getLatestConfiguration();
}

function getLatestConfiguration() {
const storageItem = browser.storage.local.get(window.location.host);
storageItem.then((storage) => {
const latest = storage[window.location.host];
var response = { results: [], chainId: "", name: "didLoadLatestConfiguration", rpcURL: "" };
if (typeof latest !== "undefined" && "results" in latest && latest.results.length > 0 && latest.rpcURL.length > 0) {
response.results = latest.results;
response.chainId = latest.chainId;
response.rpcURL = latest.rpcURL;
}
const id = new Date().getTime() + Math.floor(Math.random() * 1000);
window.postMessage({ direction: "from-content-script", response: response, id: id }, "*");
});
async function getLatestConfiguration() {
const id = new Date().getTime() + Math.floor(Math.random() * 1000);
const approvalsResponse = await browser.runtime.sendMessage({ subject: "process-inpage-message", message: { id, subject: "getApprovals", host: window.location.host } });
const approvals = JSON.parse(approvalsResponse.result);
const storageItem = await browser.storage.local.get(window.location.host);
const latest = storageItem[window.location.host];
var response = { results: [], chainId: "", name: "didLoadLatestConfiguration", rpcURL: "" };
if (
typeof latest === "object" &&
Array.isArray(latest.results) &&
latest.results.length > 0 &&
typeof latest.results[0] === "string" &&
Array.isArray(approvals) &&
approvals.includes(latest.results[0].toLowerCase()) &&
latest.rpcURL.length > 0
) {
response.results = latest.results;
response.chainId = latest.chainId;
response.rpcURL = latest.rpcURL;
}
window.postMessage({ direction: "from-content-script", response, id }, "*");
}

function storeConfigurationIfNeeded(request) {
Expand Down
87 changes: 69 additions & 18 deletions Safari-Shared/SafariWebExtensionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,69 @@ import SafariServices

let SFExtensionMessageKey = "message"

fileprivate func toJSON(from object:Any) throws -> String {
guard let result = String(data: try JSONSerialization.data(withJSONObject: object, options: []), encoding: .utf8) else {
throw "Failed to serialize JSON"
}
return result
}

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {

private var context: NSExtensionContext?
private let queue = DispatchQueue(label: "SafariWebExtensionHandler", qos: .default)

func beginRequest(with context: NSExtensionContext) {
guard let item = context.inputItems[0] as? NSExtensionItem,
let message = item.userInfo?[SFExtensionMessageKey],
let id = (message as? [String: Any])?["id"] as? Int else { return }
let message = item.userInfo?[SFExtensionMessageKey] as? [String: Any],
let id = message["id"] as? Int else { return }

let subject = (message as? [String: Any])?["subject"] as? String
if subject == "getResponse" {
let subject = message["subject"] as? String
if subject == "getAccounts" {
let manager = WalletsManager()
do {
try manager.start()
let addresses = try manager.wallets.map { wallet -> String in
guard let address = wallet.ethereumAddress else { throw "Failed retreiving Ethereum address" }
return address
}
let json = try toJSON(from: addresses)
context.respond(with: .init(id: id, name: "getAccounts", result: json))
} catch {
context.respond(with: .init(id: id, name: "getAccounts", error: "An error occurred when fetching accounts: \(error.localizedDescription)"))
}
} else if subject == "getChains" {
do {
let mainnets = EthereumChain.allMainnets.reduce(into: [String: [String: Any]]()) {
$0[String($1.id)] = ["name": $1.name, "symbol": $1.symbol, "rpc": $1.nodeURLString, "isTestnet": false]
}
let chains = EthereumChain.allTestnets.reduce(into: mainnets) {
$0[String($1.id)] = ["name": $1.name, "symbol": $1.symbol, "rpc": $1.nodeURLString, "isTestnet": true]
}
context.respond(with: .init(id: id, name: "getChains", result: try toJSON(from: chains)))
} catch {
context.respond(with: .init(id: id, name: "getChains", error: "An error occurred when fetching accounts: \(error.localizedDescription)"))
}
} else if subject == "getApprovals" {
do {
guard let host = message["host"] as? String, host.count > 0 else { throw "`host` was invalid" }
context.respond(with: .init(id: id, name: "getApprovals", result: try toJSON(from: Approvals.getApprovals(for: host))))
} catch {
context.respond(with: .init(id: id, name: "getApprovals", error: "An error occurred when fetching accounts: \(error.localizedDescription)"))
}
} else if subject == "approve" {
do {
// TODO: Actually validate address
guard let account = message["account"] as? String, account.count == 42 else { throw "`account` was invalid" }
guard let host = message["host"] as? String, host.count > 0 else { throw "`host` was invalid" }
Approvals.approve(account: account, on: host)
context.respond(with: .init(id: id, name: "getChains", result: "true"))
} catch {
context.respond(with: .init(id: id, name: "getChains", error: "An error occurred when fetching accounts: \(error.localizedDescription)"))
}
} else if subject == "getResponse" {
#if !os(macOS)
if let response = ExtensionBridge.getResponse(id: id) {
self.context = context
respond(with: response)
context.respond(with: response)
ExtensionBridge.removeResponse(id: id)
}
#endif
Expand All @@ -29,47 +76,51 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
let query = String(data: data, encoding: .utf8)?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let request = SafariRequest(query: query),
let url = URL(string: "balance://safari?request=\(query)") {
self.context = context
if request.method == .switchEthereumChain || request.method == .addEthereumChain {
if let chain = request.switchToChain {
let response = ResponseToExtension(id: request.id,
name: request.name,
results: [request.address],
chainId: chain.hexStringId,
rpcURL: chain.nodeURLString)
respond(with: response)
context.respond(with: response)
} else {
let response = ResponseToExtension(id: request.id, name: request.name, error: "Failed to switch chain")
respond(with: response)
context.respond(with: response)
}
} else {
ExtensionBridge.makeRequest(id: id)
#if os(macOS)
NSWorkspace.shared.open(url)
#endif
poll(id: id)
poll(id: id, context: context)
}
}
}

private func poll(id: Int) {
private func poll(id: Int, context: NSExtensionContext) {
if let response = ExtensionBridge.getResponse(id: id) {
respond(with: response)
context.respond(with: response)
#if os(macOS)
ExtensionBridge.removeResponse(id: id)
#endif
} else {
queue.asyncAfter(deadline: .now() + .milliseconds(500)) { [weak self] in
self?.poll(id: id)
self?.poll(id: id, context: context)
}
}
}

private func respond(with response: ResponseToExtension) {
}

extension NSExtensionContext {
func respond(with response: ResponseToExtension) {
let item = NSExtensionItem()
item.userInfo = [SFExtensionMessageKey: response.json]
context?.completeRequest(returningItems: [item], completionHandler: nil)
context = nil
self.completeRequest(returningItems: [item], completionHandler: nil)
}

}

extension String: LocalizedError {
public var errorDescription: String? { return self }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Foundation

enum EthereumChain: Int {
enum EthereumChain: UInt {
case ethereum = 1
case arbitrum = 42161
case polygon = 137
Expand All @@ -22,7 +22,7 @@ enum EthereumChain: Int {
case binanceTestnet = 97
case avalancheFuji = 43113

var id: Int {
var id: UInt {
return rawValue
}

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ struct SafariRequest {
}

var chain: EthereumChain? {
if let network = json["networkId"] as? String, let networkId = Int(network) {
if let network = json["networkId"] as? String, let networkId = UInt(network) {
return EthereumChain(rawValue: networkId)
} else {
return nil
Expand All @@ -83,7 +83,7 @@ struct SafariRequest {

var switchToChain: EthereumChain? {
if let chainId = (parameters?["chainId"] as? String)?.dropFirst(2),
let networkId = Int(chainId, radix: 16),
let networkId = UInt(chainId, radix: 16),
let chain = EthereumChain(rawValue: networkId) {
return chain
} else {
Expand Down
48 changes: 48 additions & 0 deletions Shared/Wallets/Approvals.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation

fileprivate typealias ApprovalsDictionary = [String: [String]]

enum Approvals {

private static let key = "approvals"

private static let defaults = UserDefaults(suiteName: "group.io.balance")!

private static var dictionary: ApprovalsDictionary {
get {
defaults.dictionary(forKey: key) as? ApprovalsDictionary ?? [:]
}
set {
defaults.set(newValue, forKey: key)
}
}

static func approve(account: String, on host: String) {
var approvals = getApprovals(for: host)
approvals.append(account.lowercased())
dictionary[host] = approvals
}

static func getApprovals(for host: String) -> [String] {
return dictionary[host] ?? []
}

static func removeApproval(for account: String, on host: String) {
dictionary[host] = getApprovals(for: host).filter { $0 != account.lowercased() }
}

static func clearAllApprovals(for account: String) {
var dictionary = dictionary
for key in dictionary.keys {
guard var array = dictionary[key], let index = array.firstIndex(of: account.lowercased()) else { continue }
array.remove(at: index)
dictionary[key] = array
}
self.dictionary = dictionary
}

static func destroy() {
self.dictionary = [:]
}

}
5 changes: 3 additions & 2 deletions Shared/Wallets/WalletsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ final class WalletsManager {
private let keychain = Keychain.shared
private(set) var wallets = [TokenaryWallet]()

private init() {}

func start() throws {
try load()
}
Expand Down Expand Up @@ -155,6 +153,9 @@ final class WalletsManager {
defer { privateKey.resetBytes(in: 0..<privateKey.count) }
wallets.remove(at: index)
try keychain.removeWallet(id: wallet.id)
if let address = wallet.ethereumAddress {
Approvals.clearAllApprovals(for: address)
}
postWalletsChangedNotification()
}

Expand Down