Skip to content
Merged
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
12 changes: 12 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,10 @@
AA00030F2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */; };
AA0003112CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */; };
AA0003122CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */; };
AA0004012DA0F58E00C1C501 /* AddressSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */; };
AA0004022DA0F58E00C1C501 /* AddressSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */; };
AA0004042DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */; };
AA0004052DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -3238,6 +3242,8 @@
AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericQRScannerController.swift; sourceTree = "<group>"; };
AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCaptureView.swift; sourceTree = "<group>"; };
AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericQRScannerView.swift; sourceTree = "<group>"; };
AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSourceView.swift; sourceTree = "<group>"; };
AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MayaExchangeAddressProvider.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -4108,6 +4114,7 @@
AA00030A2CA0F58E00B1B404 /* GenericQRScannerController.swift */,
AA00030D2CA0F58E00B1B405 /* QRCaptureView.swift */,
AA0003102CA0F58E00B1B406 /* GenericQRScannerView.swift */,
AA0004002DA0F58E00C1C501 /* AddressSourceView.swift */,
);
path = Maya;
sourceTree = "<group>";
Expand All @@ -4119,6 +4126,7 @@
AA0002042BA0F58E00A1B302 /* MayaPool.swift */,
AA0002072BA0F58E00A1B303 /* MayaEndpoint.swift */,
AA00020A2BA0F58E00A1B304 /* MayaAPIService.swift */,
AA0004032DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift */,
);
path = Maya;
sourceTree = "<group>";
Expand Down Expand Up @@ -9276,6 +9284,8 @@
AA00030B2CA0F58E00B1B404 /* GenericQRScannerController.swift in Sources */,
AA00030E2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */,
AA0003112CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */,
AA0004012DA0F58E00C1C501 /* AddressSourceView.swift in Sources */,
AA0004042DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -10179,6 +10189,8 @@
AA00030C2CA0F58E00B1B404 /* GenericQRScannerController.swift in Sources */,
AA00030F2CA0F58E00B1B405 /* QRCaptureView.swift in Sources */,
AA0003122CA0F58E00B1B406 /* GenericQRScannerView.swift in Sources */,
AA0004022DA0F58E00C1C501 /* AddressSourceView.swift in Sources */,
AA0004052DA0F58E00C1C502 /* MayaExchangeAddressProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "coinbase-logo.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "uphold-logo.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "original"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,27 @@ class AccountRepository {

return items
}

/// Fetch all crypto accounts regardless of balance.
/// Used by Maya to find accounts for currencies with zero balance.
func allIncludingEmpty() async throws -> [CBAccount] {
var items: [CBAccount] = []
items.reserveCapacity(300)

var endpoint: CoinbaseEndpoint? = .accounts
while endpoint != nil {
let response: BasePaginationResponse<CoinbaseUserAccountData> = try await CoinbaseAPI.shared.request(endpoint!)
items += response.data
.filter { $0.currency.type == .crypto }
.map { .init(info: $0, authInterop: authInterop) }

if let nextUri = response.pagination.nextURI, !nextUri.isEmpty {
endpoint = .path(nextUri)
} else {
endpoint = nil
}
}

return items
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ class AccountService {
try await accountRepository.all()
}

/// Returns all crypto accounts regardless of balance.
/// Used by Maya to find accounts for currencies with zero balance.
public func allAccountsIncludingEmpty() async throws -> [CBAccount] {
try await accountRepository.allIncludingEmpty()
}

public func retrieveAddress(for accountName: String) async throws -> String {
let account = try await account(by: accountName)
return try await account.retrieveAddress()
Expand Down
15 changes: 4 additions & 11 deletions DashWallet/Sources/Models/Coinbase/Auth/CBAuth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,17 @@ extension CBAuth {
extension CBAuth {
nonisolated
private var oAuth2URL: URL {
let path = CoinbaseEndpoint.signIn.path

var queryItems = [
URLQueryItem(name: "redirect_uri", value: Coinbase.redirectUri),
URLQueryItem(name: "response_type", value: Coinbase.responseType),
URLQueryItem(name: "client_id", value: Coinbase.clientID),
URLQueryItem(name: "redirect_uri", value: Coinbase.redirectUri),
URLQueryItem(name: "scope", value: Coinbase.scope),
URLQueryItem(name: "meta[send_limit_amount]", value: "\((Coinbase.sendLimitAmount as NSDecimalNumber).intValue)"),
URLQueryItem(name: "meta[send_limit_currency]", value: Coinbase.sendLimitCurrency),
URLQueryItem(name: "meta[send_limit_period]", value: Coinbase.sendLimitPeriod),
URLQueryItem(name: "account", value: Coinbase.account),
]

queryItems.append(URLQueryItem(name: "client_id", value: Coinbase.clientID))

var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "coinbase.com"
urlComponents.path = path
urlComponents.host = "login.coinbase.com"
urlComponents.path = "/oauth2/auth"
urlComponents.queryItems = queryItems

guard let url = urlComponents.url else {
Expand Down
4 changes: 2 additions & 2 deletions DashWallet/Sources/Models/Coinbase/Coinbase+Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import Foundation

extension Coinbase {
// MARK: API
static let callbackURLScheme = "authhub"
static let redirectUri = "authhub://oauth-callback"
static let callbackURLScheme = "dashwallet"
static let redirectUri = "dashwallet://brokers/coinbase/connect"
static let grantType = "authorization_code"
static let responseType = "code"
static let scope =
Expand Down
13 changes: 13 additions & 0 deletions DashWallet/Sources/Models/Coinbase/Coinbase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@ extension Coinbase {
try await accountService.allAccounts()
}

/// Returns all crypto accounts regardless of balance.
/// Used by Maya to find accounts for currencies with zero balance.
public func accountsIncludingEmpty() async throws -> [CBAccount] {
try await accountService.allAccountsIncludingEmpty()
}

/// Fetches a specific account by currency code (e.g., "BTC", "ETH").
/// Uses direct `GET /v2/accounts/{currencyCode}` lookup which is more reliable
/// than listing all accounts when you know the currency you need.
public func account(byCurrencyCode currencyCode: String) async throws -> CBAccount {
try await accountService.account(by: currencyCode)
}

public func addUserDidChangeListener(_ listener: @escaping UserDidChangeListenerBlock) -> UserDidChangeListenerHandle {
auth.addUserDidChangeListener(listener)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,15 @@ extension CoinbaseEndpoint: TargetType, AccessTokenAuthorizable {
}

public var baseURL: URL {
guard case .path(let string) = self else {
switch self {
case .getToken, .refreshToken, .revokeToken:
return URL(string: "https://login.coinbase.com")!
case .path(let string):
let path = string.removingPercentEncoding ?? string
return URL(string: "https://api.coinbase.com" + path)!
default:
return kBaseURL
}

let path = string.removingPercentEncoding ?? string
let url = URL(string: "https://api.coinbase.com" + path)!
return url
}

public var path: String {
Expand All @@ -188,9 +190,9 @@ extension CoinbaseEndpoint: TargetType, AccessTokenAuthorizable {
case .swapTradeCommit(let tradeId): return "/v2/trades/\(tradeId)/commit"
case .accountAddress(let accountId): return "/v2/accounts/\(accountId)/addresses"
case .createCoinbaseAccountAddress(let accountId): return "/v2/accounts/\(accountId)/addresses"
case .getToken, .refreshToken: return "/oauth/token"
case .revokeToken: return "/oauth/revoke"
case .signIn: return "/oauth/authorize"
case .getToken, .refreshToken: return "/oauth2/token"
case .revokeToken: return "/oauth2/revoke"
case .signIn: return "/oauth2/auth"
default:
return ""
}
Expand All @@ -212,13 +214,12 @@ extension CoinbaseEndpoint: TargetType, AccessTokenAuthorizable {
"redirect_uri": Coinbase.redirectUri,
"code": code,
"grant_type": Coinbase.grantType,
"account": Coinbase.account,
]

queryItems["client_id"] = Coinbase.clientID
queryItems["client_secret"] = Coinbase.clientSecret
return .requestParameters(parameters: queryItems, encoding: JSONEncoding.default)

return .requestParameters(parameters: queryItems, encoding: URLEncoding.httpBody)
case .refreshToken(let refreshToken):
var queryItems: [String: Any] = [
"refresh_token": refreshToken,
Expand All @@ -227,10 +228,10 @@ extension CoinbaseEndpoint: TargetType, AccessTokenAuthorizable {

queryItems["client_id"] = Coinbase.clientID
queryItems["client_secret"] = Coinbase.clientSecret
return .requestParameters(parameters: queryItems, encoding: JSONEncoding.default)

return .requestParameters(parameters: queryItems, encoding: URLEncoding.httpBody)
case .revokeToken(let token):
return .requestParameters(parameters: ["token": token], encoding: JSONEncoding.default)
return .requestParameters(parameters: ["token": token], encoding: URLEncoding.httpBody)
case .sendCoinsToWallet(_, _, let dto):
return .requestJSONEncodable(dto)
case .swapTrade(let dto):
Expand Down
Loading
Loading