1- //! OAuth-authenticated query for the caller's OKX account `uid`.
1+ //! Query for the caller's OKX account `uid`.
22//!
33//! Intentionally outside the SDK: a self-contained raw HTTP call to OKX's
4- //! "Get account configuration" endpoint (`GET /api/v5/account/config`) using
5- //! the OAuth **bearer token** from the `okx-auth` broker, plus parsing to pull
6- //! the `uid` out of the response. Required by the setup workflow, so it is
7- //! always compiled (not feature-gated).
4+ //! "Get account configuration" endpoint (`GET /api/v5/account/config`), plus
5+ //! parsing to pull the `uid` out of the response. Required by the setup
6+ //! workflow (the wallet-binding deeplink), so it is always compiled.
7+ //!
8+ //! Auth mirrors [`crate::util::require_auth`]: the OAuth **bearer token** from
9+ //! the `okx-auth` broker when a session exists, otherwise a live **HMAC API
10+ //! key** from `~/.okx/config.toml` (`OK-ACCESS-*` headers). The fallback lets
11+ //! `setup` resolve the uid without an OAuth sign-in when an API key is present.
812
913use serde:: Deserialize ;
1014
@@ -69,14 +73,37 @@ fn resolve_api_base() -> String {
6973 base. trim_end_matches ( '/' ) . to_string ( )
7074}
7175
72- /// Fetch the caller's account `uid` using the OAuth bearer token from the
73- /// `okx-auth` broker (the token reflects the session established at `login` ).
76+ /// Fetch the caller's account `uid`, OAuth- bearer-first with an HMAC API-key
77+ /// fallback (mirrors [`crate::util::require_auth`] ).
7478///
75- /// The base URL comes from config.json (else [`DEFAULT_API_BASE`]).
76- /// The bearer token is never logged.
79+ /// On the fallback path the base URL is derived from the profile's region
80+ /// (`config.json` wins if set), since the HMAC key is region-scoped. No
81+ /// credential value is ever logged.
7782pub async fn fetch_account_uid ( ) -> Result < String , String > {
78- let token = super :: auth_token:: fetch_token ( ) . map_err ( |e| e. to_string ( ) ) ?;
79- let url = format ! ( "{}{ACCOUNT_CONFIG_PATH}" , resolve_api_base( ) ) ;
83+ // Pick the auth method and the matching base URL.
84+ let ( auth, base) = match super :: auth_token:: fetch_token ( ) {
85+ Ok ( token) => ( Auth :: Bearer ( token) , resolve_api_base ( ) ) ,
86+ Err ( oauth_err) => match crate :: util:: live_api_key_lookup ( ) {
87+ crate :: util:: ApiKeyLookup :: Found { creds, site } => {
88+ let base = match crate :: util:: resolve_api_base_opt ( ) {
89+ Some ( b) => b. trim_end_matches ( '/' ) . to_string ( ) ,
90+ None => crate :: util:: fallback_rest_base ( site) ?,
91+ } ;
92+ ( Auth :: Hmac ( creds) , base)
93+ }
94+ crate :: util:: ApiKeyLookup :: DemoOnly => {
95+ return Err (
96+ "found only a demo profile in ~/.okx/config.toml — the outcomes API has \
97+ no simulated-trading mode. Configure a live (non-demo) profile, or run \
98+ `okx-outcomes auth login`."
99+ . to_string ( ) ,
100+ ) ;
101+ }
102+ crate :: util:: ApiKeyLookup :: None => return Err ( oauth_err. to_string ( ) ) ,
103+ } ,
104+ } ;
105+
106+ let url = format ! ( "{base}{ACCOUNT_CONFIG_PATH}" ) ;
80107
81108 // Honor a caller-installed HTTP client (see `crate::set_http_client`) so
82109 // this raw call shares the same routing as the SDK — e.g. a client with
@@ -90,9 +117,24 @@ pub async fn fetch_account_uid() -> Result<String, String> {
90117 . build ( )
91118 . map_err ( |e| format ! ( "failed to build HTTP client: {e}" ) ) ?,
92119 } ;
93- let resp = client
94- . get ( & url)
95- . bearer_auth ( & token)
120+
121+ let req = client. get ( & url) ;
122+ let req = match & auth {
123+ Auth :: Bearer ( token) => req. bearer_auth ( token) ,
124+ Auth :: Hmac ( creds) => {
125+ // OKX HMAC: prehash = timestamp + METHOD + requestPath (+ body).
126+ // No body or query string on this GET.
127+ let timestamp = okx_timestamp ( ) ;
128+ let prehash = format ! ( "{timestamp}GET{ACCOUNT_CONFIG_PATH}" ) ;
129+ let sign = hmac_sha256_base64 ( & creds. secret_key , & prehash) ;
130+ req. header ( "OK-ACCESS-KEY" , & creds. api_key )
131+ . header ( "OK-ACCESS-SIGN" , sign)
132+ . header ( "OK-ACCESS-TIMESTAMP" , & timestamp)
133+ . header ( "OK-ACCESS-PASSPHRASE" , & creds. passphrase )
134+ }
135+ } ;
136+
137+ let resp = req
96138 . send ( )
97139 . await
98140 . map_err ( |e| format ! ( "request failed: {e}" ) ) ?;
@@ -108,6 +150,32 @@ pub async fn fetch_account_uid() -> Result<String, String> {
108150 parse_uid ( & body)
109151}
110152
153+ /// Account auth for the uid lookup: an OAuth bearer token, or HMAC API-key
154+ /// credentials for the `OK-ACCESS-*` signed-request fallback.
155+ enum Auth {
156+ Bearer ( String ) ,
157+ Hmac ( okx_outcomes_sdk:: ApiCredentials ) ,
158+ }
159+
160+ /// OKX request timestamp: ISO-8601 UTC with millisecond precision, e.g.
161+ /// `2020-12-08T09:08:57.715Z` — the form `OK-ACCESS-TIMESTAMP` expects.
162+ fn okx_timestamp ( ) -> String {
163+ chrono:: Utc :: now ( )
164+ . format ( "%Y-%m-%dT%H:%M:%S%.3fZ" )
165+ . to_string ( )
166+ }
167+
168+ /// `OK-ACCESS-SIGN` = Base64(HMAC-SHA256(secret_key, prehash)).
169+ fn hmac_sha256_base64 ( secret_key : & str , prehash : & str ) -> String {
170+ use base64:: Engine as _;
171+ use hmac:: { Hmac , Mac } ;
172+ use sha2:: Sha256 ;
173+ let mut mac = <Hmac < Sha256 > >:: new_from_slice ( secret_key. as_bytes ( ) )
174+ . expect ( "HMAC-SHA256 accepts a key of any length" ) ;
175+ mac. update ( prehash. as_bytes ( ) ) ;
176+ base64:: engine:: general_purpose:: STANDARD . encode ( mac. finalize ( ) . into_bytes ( ) )
177+ }
178+
111179#[ cfg( test) ]
112180#[ allow( clippy:: unwrap_used, clippy:: expect_used) ]
113181mod tests {
@@ -141,4 +209,15 @@ mod tests {
141209 . unwrap_err( )
142210 . contains( "parse response JSON" ) ) ;
143211 }
212+
213+ #[ test]
214+ fn hmac_sign_matches_reference_vector ( ) {
215+ // Reference computed with Python's hmac/hashlib (RFC 2104 HMAC-SHA256,
216+ // base64-encoded) — must match OKX's `OK-ACCESS-SIGN` scheme.
217+ let sign = super :: hmac_sha256_base64 (
218+ "test-secret-key" ,
219+ "2020-12-08T09:08:57.715ZGET/api/v5/account/config" ,
220+ ) ;
221+ assert_eq ! ( sign, "JhnPMxZeBAZBDiFbXMp1ijlwS2s/WCCTsq6+8s0aTd8=" ) ;
222+ }
144223}
0 commit comments