Skip to content

Commit 7688c36

Browse files
JamieMageejoostlek
andauthored
Remove coinbase v2 API support (#148387)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent a1dc3f3 commit 7688c36

File tree

11 files changed

+287
-242
lines changed

11 files changed

+287
-242
lines changed

homeassistant/components/coinbase/__init__.py

Lines changed: 24 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@
77

88
from coinbase.rest import RESTClient
99
from coinbase.rest.rest_base import HTTPError
10-
from coinbase.wallet.client import Client as LegacyClient
11-
from coinbase.wallet.error import AuthenticationError
1210

1311
from homeassistant.config_entries import ConfigEntry
1412
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform
1513
from homeassistant.core import HomeAssistant
14+
from homeassistant.exceptions import ConfigEntryAuthFailed
1615
from homeassistant.helpers import entity_registry as er
1716
from homeassistant.util import Throttle
1817

1918
from .const import (
2019
ACCOUNT_IS_VAULT,
2120
API_ACCOUNT_AMOUNT,
2221
API_ACCOUNT_AVALIABLE,
23-
API_ACCOUNT_BALANCE,
2422
API_ACCOUNT_CURRENCY,
25-
API_ACCOUNT_CURRENCY_CODE,
2623
API_ACCOUNT_HOLD,
2724
API_ACCOUNT_ID,
2825
API_ACCOUNT_NAME,
@@ -31,7 +28,6 @@
3128
API_DATA,
3229
API_RATES_CURRENCY,
3330
API_RESOURCE_TYPE,
34-
API_TYPE_VAULT,
3531
API_V3_ACCOUNT_ID,
3632
API_V3_TYPE_VAULT,
3733
CONF_CURRENCIES,
@@ -68,16 +64,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: CoinbaseConfigEntry) ->
6864

6965
def create_and_update_instance(entry: CoinbaseConfigEntry) -> CoinbaseData:
7066
"""Create and update a Coinbase Data instance."""
67+
68+
# Check if user is using deprecated v2 API credentials
7169
if "organizations" not in entry.data[CONF_API_KEY]:
72-
client = LegacyClient(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN])
73-
version = "v2"
74-
else:
75-
client = RESTClient(
76-
api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN]
70+
# Trigger reauthentication to ask user for v3 credentials
71+
raise ConfigEntryAuthFailed(
72+
"Your Coinbase API key appears to be for the deprecated v2 API. "
73+
"Please reconfigure with a new API key created for the v3 API. "
74+
"Visit https://www.coinbase.com/developer-platform to create new credentials."
7775
)
78-
version = "v3"
76+
77+
client = RESTClient(
78+
api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN]
79+
)
7980
base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD")
80-
instance = CoinbaseData(client, base_rate, version)
81+
instance = CoinbaseData(client, base_rate)
8182
instance.update()
8283
return instance
8384

@@ -105,31 +106,9 @@ async def update_listener(
105106
registry.async_remove(entity.entity_id)
106107

107108

108-
def get_accounts(client, version):
109+
def get_accounts(client):
109110
"""Handle paginated accounts."""
110111
response = client.get_accounts()
111-
if version == "v2":
112-
accounts = response[API_DATA]
113-
next_starting_after = response.pagination.next_starting_after
114-
115-
while next_starting_after:
116-
response = client.get_accounts(starting_after=next_starting_after)
117-
accounts += response[API_DATA]
118-
next_starting_after = response.pagination.next_starting_after
119-
120-
return [
121-
{
122-
API_ACCOUNT_ID: account[API_ACCOUNT_ID],
123-
API_ACCOUNT_NAME: account[API_ACCOUNT_NAME],
124-
API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY][
125-
API_ACCOUNT_CURRENCY_CODE
126-
],
127-
API_ACCOUNT_AMOUNT: account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT],
128-
ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_TYPE_VAULT,
129-
}
130-
for account in accounts
131-
]
132-
133112
accounts = response[API_ACCOUNTS]
134113
while response["has_next"]:
135114
response = client.get_accounts(cursor=response["cursor"])
@@ -153,37 +132,28 @@ def get_accounts(client, version):
153132
class CoinbaseData:
154133
"""Get the latest data and update the states."""
155134

156-
def __init__(self, client, exchange_base, version):
135+
def __init__(self, client, exchange_base):
157136
"""Init the coinbase data object."""
158137

159138
self.client = client
160139
self.accounts = None
161140
self.exchange_base = exchange_base
162141
self.exchange_rates = None
163-
if version == "v2":
164-
self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
165-
else:
166-
self.user_id = (
167-
"v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID]
168-
)
169-
self.api_version = version
142+
self.user_id = (
143+
"v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID]
144+
)
170145

171146
@Throttle(MIN_TIME_BETWEEN_UPDATES)
172147
def update(self):
173148
"""Get the latest data from coinbase."""
174149

175150
try:
176-
self.accounts = get_accounts(self.client, self.api_version)
177-
if self.api_version == "v2":
178-
self.exchange_rates = self.client.get_exchange_rates(
179-
currency=self.exchange_base
180-
)
181-
else:
182-
self.exchange_rates = self.client.get(
183-
"/v2/exchange-rates",
184-
params={API_RATES_CURRENCY: self.exchange_base},
185-
)[API_DATA]
186-
except (AuthenticationError, HTTPError) as coinbase_error:
151+
self.accounts = get_accounts(self.client)
152+
self.exchange_rates = self.client.get(
153+
"/v2/exchange-rates",
154+
params={API_RATES_CURRENCY: self.exchange_base},
155+
)[API_DATA]
156+
except HTTPError as coinbase_error:
187157
_LOGGER.error(
188158
"Authentication error connecting to coinbase: %s", coinbase_error
189159
)

homeassistant/components/coinbase/config_flow.py

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Mapping
56
import logging
67
from typing import Any
78

89
from coinbase.rest import RESTClient
910
from coinbase.rest.rest_base import HTTPError
10-
from coinbase.wallet.client import Client as LegacyClient
11-
from coinbase.wallet.error import AuthenticationError
1211
import voluptuous as vol
1312

1413
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
15-
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION
14+
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
1615
from homeassistant.core import HomeAssistant, callback
1716
from homeassistant.exceptions import HomeAssistantError
1817
from homeassistant.helpers import config_validation as cv
@@ -45,9 +44,6 @@
4544

4645
def get_user_from_client(api_key, api_token):
4746
"""Get the user name from Coinbase API credentials."""
48-
if "organizations" not in api_key:
49-
client = LegacyClient(api_key, api_token)
50-
return client.get_current_user()["name"]
5147
client = RESTClient(api_key=api_key, api_secret=api_token)
5248
return client.get_portfolios()["portfolios"][0]["name"]
5349

@@ -59,7 +55,7 @@ async def validate_api(hass: HomeAssistant, data):
5955
user = await hass.async_add_executor_job(
6056
get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN]
6157
)
62-
except (AuthenticationError, HTTPError) as error:
58+
except HTTPError as error:
6359
if "api key" in str(error) or " 401 Client Error" in str(error):
6460
_LOGGER.debug("Coinbase rejected API credentials due to an invalid API key")
6561
raise InvalidKey from error
@@ -74,8 +70,8 @@ async def validate_api(hass: HomeAssistant, data):
7470
raise InvalidAuth from error
7571
except ConnectionError as error:
7672
raise CannotConnect from error
77-
api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2"
78-
return {"title": user, "api_version": api_version}
73+
74+
return {"title": user}
7975

8076

8177
async def validate_options(
@@ -85,20 +81,17 @@ async def validate_options(
8581

8682
client = config_entry.runtime_data.client
8783

88-
accounts = await hass.async_add_executor_job(
89-
get_accounts, client, config_entry.data.get("api_version", "v2")
90-
)
84+
accounts = await hass.async_add_executor_job(get_accounts, client)
9185

9286
accounts_currencies = [
9387
account[API_ACCOUNT_CURRENCY]
9488
for account in accounts
9589
if not account[ACCOUNT_IS_VAULT]
9690
]
97-
if config_entry.data.get("api_version", "v2") == "v2":
98-
available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
99-
else:
100-
resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates")
101-
available_rates = resp[API_DATA]
91+
92+
resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates")
93+
available_rates = resp[API_DATA]
94+
10295
if CONF_CURRENCIES in options:
10396
for currency in options[CONF_CURRENCIES]:
10497
if currency not in accounts_currencies:
@@ -117,6 +110,8 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
117110

118111
VERSION = 1
119112

113+
reauth_entry: CoinbaseConfigEntry
114+
120115
async def async_step_user(
121116
self, user_input: dict[str, str] | None = None
122117
) -> ConfigFlowResult:
@@ -143,12 +138,63 @@ async def async_step_user(
143138
_LOGGER.exception("Unexpected exception")
144139
errors["base"] = "unknown"
145140
else:
146-
user_input[CONF_API_VERSION] = info["api_version"]
147141
return self.async_create_entry(title=info["title"], data=user_input)
148142
return self.async_show_form(
149143
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
150144
)
151145

146+
async def async_step_reauth(
147+
self, entry_data: Mapping[str, Any]
148+
) -> ConfigFlowResult:
149+
"""Handle reauthentication flow."""
150+
self.reauth_entry = self._get_reauth_entry()
151+
return await self.async_step_reauth_confirm()
152+
153+
async def async_step_reauth_confirm(
154+
self, user_input: dict[str, str] | None = None
155+
) -> ConfigFlowResult:
156+
"""Handle reauthentication confirmation."""
157+
errors: dict[str, str] = {}
158+
159+
if user_input is None:
160+
return self.async_show_form(
161+
step_id="reauth_confirm",
162+
data_schema=STEP_USER_DATA_SCHEMA,
163+
description_placeholders={
164+
"account_name": self.reauth_entry.title,
165+
},
166+
errors=errors,
167+
)
168+
169+
try:
170+
await validate_api(self.hass, user_input)
171+
except CannotConnect:
172+
errors["base"] = "cannot_connect"
173+
except InvalidKey:
174+
errors["base"] = "invalid_auth_key"
175+
except InvalidSecret:
176+
errors["base"] = "invalid_auth_secret"
177+
except InvalidAuth:
178+
errors["base"] = "invalid_auth"
179+
except Exception:
180+
_LOGGER.exception("Unexpected exception")
181+
errors["base"] = "unknown"
182+
else:
183+
return self.async_update_reload_and_abort(
184+
self.reauth_entry,
185+
data_updates=user_input,
186+
reason="reauth_successful",
187+
)
188+
189+
return self.async_show_form(
190+
step_id="reauth_confirm",
191+
data_schema=STEP_USER_DATA_SCHEMA,
192+
description_placeholders={
193+
"account_name": self.reauth_entry.title,
194+
},
195+
errors=errors,
196+
)
197+
152198
@staticmethod
153199
@callback
154200
def async_get_options_flow(

homeassistant/components/coinbase/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66
"documentation": "https://www.home-assistant.io/integrations/coinbase",
77
"iot_class": "cloud_polling",
88
"loggers": ["coinbase"],
9-
"requirements": ["coinbase==2.1.0", "coinbase-advanced-py==1.2.2"]
9+
"requirements": ["coinbase-advanced-py==1.2.2"]
1010
}

homeassistant/components/coinbase/sensor.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
_LOGGER = logging.getLogger(__name__)
2828

2929
ATTR_NATIVE_BALANCE = "Balance in native currency"
30-
ATTR_API_VERSION = "API Version"
3130

3231
CURRENCY_ICONS = {
3332
"BTC": "mdi:currency-btc",
@@ -71,9 +70,8 @@ async def async_setup_entry(
7170

7271
for currency in desired_currencies:
7372
_LOGGER.debug(
74-
"Attempting to set up %s account sensor with %s API",
73+
"Attempting to set up %s account sensor",
7574
currency,
76-
instance.api_version,
7775
)
7876
if currency not in provided_currencies:
7977
_LOGGER.warning(
@@ -89,9 +87,8 @@ async def async_setup_entry(
8987
if CONF_EXCHANGE_RATES in config_entry.options:
9088
for rate in config_entry.options[CONF_EXCHANGE_RATES]:
9189
_LOGGER.debug(
92-
"Attempting to set up %s account sensor with %s API",
90+
"Attempting to set up %s exchange rate sensor",
9391
rate,
94-
instance.api_version,
9592
)
9693
entities.append(
9794
ExchangeRateSensor(
@@ -146,15 +143,13 @@ def extra_state_attributes(self) -> dict[str, str]:
146143
"""Return the state attributes of the sensor."""
147144
return {
148145
ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}",
149-
ATTR_API_VERSION: self._coinbase_data.api_version,
150146
}
151147

152148
def update(self) -> None:
153149
"""Get the latest state of the sensor."""
154150
_LOGGER.debug(
155-
"Updating %s account sensor with %s API",
151+
"Updating %s account sensor",
156152
self._currency,
157-
self._coinbase_data.api_version,
158153
)
159154
self._coinbase_data.update()
160155
for account in self._coinbase_data.accounts:
@@ -210,9 +205,8 @@ def __init__(
210205
def update(self) -> None:
211206
"""Get the latest state of the sensor."""
212207
_LOGGER.debug(
213-
"Updating %s rate sensor with %s API",
208+
"Updating %s rate sensor",
214209
self._currency,
215-
self._coinbase_data.api_version,
216210
)
217211
self._coinbase_data.update()
218212
self._attr_native_value = round(

homeassistant/components/coinbase/strings.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
"api_key": "[%key:common::config_flow::data::api_key%]",
99
"api_token": "API secret"
1010
}
11+
},
12+
"reauth_confirm": {
13+
"title": "Update Coinbase API credentials",
14+
"description": "Your current Coinbase API key appears to be for the deprecated v2 API. Please reconfigure with a new API key created for the v3 API. Visit https://www.coinbase.com/developer-platform to create new credentials for {account_name}.",
15+
"data": {
16+
"api_key": "[%key:common::config_flow::data::api_key%]",
17+
"api_token": "API secret"
18+
}
1119
}
1220
},
1321
"error": {
@@ -18,7 +26,8 @@
1826
"unknown": "[%key:common::config_flow::error::unknown%]"
1927
},
2028
"abort": {
21-
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
29+
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
30+
"reauth_successful": "Successfully updated credentials"
2231
}
2332
},
2433
"options": {

requirements_all.txt

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)