Skip to content

Commit

Permalink
Connect to dapps with browser wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
wuyuehyang committed Apr 12, 2024
1 parent 6978789 commit 0d4dbef
Show file tree
Hide file tree
Showing 24 changed files with 1,382 additions and 641 deletions.
50 changes: 35 additions & 15 deletions Mixin.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Mixin/Assets.xcassets/unknown_session.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "[email protected]",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions Mixin/Resources/mixin-min.js

Large diffs are not rendered by default.

26 changes: 13 additions & 13 deletions Mixin/Service/Web3/WalletConnectService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ extension WalletConnectService {
id: 1,
internalID: ChainID.ethereum,
name: "Ethereum",
defaultRPCServerURL: URL(string: "https://cloudflare-eth.com")!,
failsafeRPCServerURL: URL(string: "https://cloudflare-eth.com")!,
feeSymbol: "ETH",
caip2: Blockchain("eip155:1")!
)
Expand All @@ -148,7 +148,7 @@ extension WalletConnectService {
id: 137,
internalID: ChainID.polygon,
name: "Polygon",
defaultRPCServerURL: URL(string: "https://polygon-rpc.com")!,
failsafeRPCServerURL: URL(string: "https://polygon-rpc.com")!,
feeSymbol: "MATIC",
caip2: Blockchain("eip155:137")!
)
Expand All @@ -157,7 +157,7 @@ extension WalletConnectService {
id: 56,
internalID: ChainID.bnbSmartChain,
name: "BSC",
defaultRPCServerURL: URL(string: "https://endpoints.omniatech.io/v1/bsc/mainnet/public")!,
failsafeRPCServerURL: URL(string: "https://endpoints.omniatech.io/v1/bsc/mainnet/public")!,
feeSymbol: "BNB",
caip2: Blockchain("eip155:56")!
)
Expand All @@ -166,18 +166,26 @@ extension WalletConnectService {
id: 11155111,
internalID: ChainID.ethereum,
name: "Sepolia",
defaultRPCServerURL: URL(string: "https://rpc.sepolia.dev")!,
failsafeRPCServerURL: URL(string: "https://rpc.sepolia.dev")!,
feeSymbol: "ETH",
caip2: Blockchain("eip155:11155111")!
)

let id: Int
let internalID: String
let name: String
let defaultRPCServerURL: URL
let failsafeRPCServerURL: URL
let feeSymbol: String
let caip2: Blockchain

var rpcServerURL: URL {
if let string = AppGroupUserDefaults.Wallet.web3RPCURL[internalID], let url = URL(string: string) {
url
} else {
failsafeRPCServerURL
}
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id
}
Expand All @@ -187,14 +195,6 @@ extension WalletConnectService {
}

func makeEthereumClient() -> EthereumHttpClient {
let rpcServerURL: URL
if let string = AppGroupUserDefaults.Wallet.web3RPCURL[internalID], let url = URL(string: string) {
Logger.web3.info(category: "Service", message: "Using saved RPC")
rpcServerURL = url
} else {
Logger.web3.info(category: "Service", message: "Using fail-safe RPC")
rpcServerURL = defaultRPCServerURL
}
let network: EthereumNetwork = switch self {
case .ethereum:
.mainnet
Expand Down
26 changes: 16 additions & 10 deletions Mixin/Service/Web3/WalletConnectSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ extension WalletConnectSession {

private func requestSendTransaction(with request: Request) {
assert(Thread.isMainThread)
let proposer = Web3Proposer(name: name, host: host)
DispatchQueue.global().async {
Logger.web3.info(category: "Session", message: "Got tx: \(request.id) \(request.params)")
do {
let params = try request.params.get([WalletConnectTransactionPreview].self)
let params = try request.params.get([Web3TransactionPreview].self)
guard let transactionPreview = params.first else {
throw Error.noTransaction
}
Expand All @@ -143,14 +144,18 @@ extension WalletConnectSession {
guard let address: String = PropertiesDAO.shared.value(forKey: .evmAddress) else {
throw Error.noAccount
}
let operation = Web3TransactionWithWalletConnectOperation(
address: address,
proposer: proposer,
transaction: transactionPreview,
chain: chain,
chainToken: chainToken,
session: self,
request: request
)
DispatchQueue.main.async {
let transactionRequest = TransactionRequestViewController(address: address,
session: self,
request: request,
transaction: transactionPreview,
chain: chain,
chainToken: chainToken)
Web3PopupCoordinator.enqueue(popup: .request(transactionRequest))
let transaction = Web3TransactionViewController(operation: operation)
Web3PopupCoordinator.enqueue(popup: .request(transaction))
}
} catch {
Logger.web3.error(category: "Session", message: "Failed to request tx: \(error)")
Expand All @@ -175,10 +180,11 @@ extension WalletConnectSession {
do {
let decoded = try decode(request)
// TODO: Get account by `request.chainId`
guard let address: String = PropertiesDAO.shared.value(forKey: .evmAddress) else {
guard let address: String = PropertiesDAO.shared.unsafeValue(forKey: .evmAddress) else {
throw Error.noAccount
}
let signRequest = SignRequestViewController(address: address, session: self, request: decoded)
let operation = Web3SignWithWalletConnectOperation(address: address, session: self, request: decoded)
let signRequest = Web3SignViewController(operation: operation, chainName: decoded.chain.name)
Web3PopupCoordinator.enqueue(popup: .request(signRequest))
} catch {
Logger.web3.error(category: "Session", message: "Failed to sign: \(error)")
Expand Down
6 changes: 6 additions & 0 deletions Mixin/Service/Web3/Web3Proposer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

struct Web3Proposer {
let name: String
let host: String
}
202 changes: 202 additions & 0 deletions Mixin/Service/Web3/Web3SignOperation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import Foundation
import web3
import Web3Wallet
import MixinServices

class Web3SignOperation {

enum State {
case pending
case signing
case signingFailed(Error)
case sending
case sendingFailed(Error)
case success
}

enum SigningError: Error {
case mismatchedAddress
}

let address: String
let proposer: Web3Proposer
let humanReadableMessage: String

fileprivate let signable: WalletConnectDecodedSigningRequest.Signable

@Published
fileprivate(set) var state: State = .pending

fileprivate var signature: String?
fileprivate var hasSignatureSent = false

fileprivate init(
address: String,
proposer: Web3Proposer,
humanReadableMessage: String,
signable: WalletConnectDecodedSigningRequest.Signable
) {
self.address = address
self.proposer = proposer
self.humanReadableMessage = humanReadableMessage
self.signable = signable
}

func start(with pin: String) {
state = .signing
Task.detached { [signable] in
Logger.web3.info(category: "Sign", message: "Will sign")
let signature: String
do {
let priv = try await TIP.web3WalletPrivateKey(pin: pin)
let keyStorage = InPlaceKeyStorage(raw: priv)
let account = try EthereumAccount(keyStorage: keyStorage)
signature = switch signable {
case .raw(let data):
try account.signMessage(message: data)
case .typed(let data):
try account.signMessage(message: data)
}
} catch {
Logger.web3.error(category: "Sign", message: "Failed to sign: \(error)")
await MainActor.run {
self.state = .signingFailed(error)
}
return
}
Logger.web3.info(category: "Sign", message: "Will send")
await MainActor.run {
self.signature = signature
self.state = .sending
}
await self.send(signature: signature)
}
}

func reject() {
assertionFailure("Must override")
}

func rejectRequestIfSignatureNotSent() {
guard !hasSignatureSent else {
return
}
Logger.web3.info(category: "Sign", message: "Rejected by dismissing")
reject()
}

@objc func resendSignature(_ sender: Any) {
guard let signature else {
return
}
state = .sending
Logger.web3.info(category: "Sign", message: "Will resend")
Task.detached {
await self.send(signature: signature)
}
}

fileprivate func send(signature: String) async {
assertionFailure("Must override")
}

}

final class Web3SignWithWalletConnectOperation: Web3SignOperation {

let session: WalletConnectSession
let request: WalletConnectDecodedSigningRequest

init(
address: String,
session: WalletConnectSession,
request: WalletConnectDecodedSigningRequest
) {
self.session = session
self.request = request
let proposer = Web3Proposer(name: session.name, host: session.host)
super.init(address: address,
proposer: proposer,
humanReadableMessage: request.humanReadable,
signable: request.signable)
}

override func start(with pin: String) {
guard address.lowercased() == request.address.lowercased() else {
Logger.web3.error(category: "Sign", message: "Mismatched Address")
state = .signingFailed(SigningError.mismatchedAddress)
return
}
super.start(with: pin)
}

override func send(signature: String) async {
do {
let response = RPCResult.response(AnyCodable(signature))
try await Web3Wallet.instance.respond(topic: request.raw.topic,
requestId: request.raw.id,
response: response)
Logger.web3.info(category: "Sign", message: "Signature sent")
await MainActor.run {
self.state = .success
self.hasSignatureSent = true
}
} catch {
Logger.web3.error(category: "Sign", message: "Failed to send: \(error)")
await MainActor.run {
self.state = .sendingFailed(error)
}
}
}

override func reject() {
Task {
let error = JSONRPCError(code: 0, message: "User Rejected")
try await Web3Wallet.instance.respond(topic: request.raw.topic, requestId: request.raw.id, response: .error(error))
}
}

}

final class Web3SignWithBrowserWalletOperation: Web3SignOperation {

private let sendImpl: ((String) async throws -> Void)?
private let rejectImpl: (() -> Void)?

init(
address: String,
proposer: Web3Proposer,
humanReadableMessage: String,
signable: WalletConnectDecodedSigningRequest.Signable,
sendWith sendImpl: @escaping ((String) async throws -> Void),
rejectWith rejectImpl: @escaping (() -> Void)
) {
self.sendImpl = sendImpl
self.rejectImpl = rejectImpl
super.init(address: address,
proposer: proposer,
humanReadableMessage: humanReadableMessage,
signable: signable)
}

override func send(signature: String) async {
do {
try await sendImpl?(signature)
Logger.web3.info(category: "Sign", message: "Signature sent")
await MainActor.run {
self.state = .success
self.hasSignatureSent = true
}
} catch {
Logger.web3.error(category: "Sign", message: "Failed to send: \(error)")
await MainActor.run {
self.state = .sendingFailed(error)
}
}
}

override func reject() {
rejectImpl?()
}

}
Loading

0 comments on commit 0d4dbef

Please sign in to comment.