Repository: ccxt/ccxt
Pull request: safeStringLower and safeStringUpper convert defaultValue to lowercase and uppercase
# Updates

- for `safeStringUpper(dict, 'value', 'default')`, the default value is returned as `DEFAULT` and vice versa for `safeStringLower`

-------------

# Testing

# JavaScript 

## dict = {"test":"","test2":"10"}

`safe_string_lower (dict, 'test')`: `undefined`
`safe_string_upper (dict, 'test')`: `undefined`
`safe_string_lower_2 (dict, 'test', 'test2')`: `10`
`safe_string_upper_2 (dict, 'test', 'test2')`: `10`
`safe_string_lower_n (dict, ['test', 'test2'])`: `10`
`safe_string_upper_n (dict, ['test', 'test2'])`: `10`
`safe_string_lower (dict, 'missing_key', 'DEFAULT') *returns default*`: `default`
`safe_string_upper (dict, 'missing_key', 'default') *returns DEFAULT*`: `DEFAULT`
`safe_string_lower_2 (dict, 'missing_key', 'missing_key_2', 'DEFAULT') *returns default*`: `default`
`safe_string_upper_2 (dict, 'missing_key', 'missing_key_2', 'default') *returns DEFAULT*`: `DEFAULT`
`safe_string_lower_n (dict, ['missing_key', 'missing_key_2'], 'DEFAULT') *returns default*`: `default`
`safe_string_upper_n (dict, ['missing_key', 'missing_key_2'], 'default') *returns DEFAULT*`: `DEFAULT`

## dict = {"test":"5","test2":""}

`safe_string_lower (dict, 'test')`: `5`
`safe_string_upper (dict, 'test')`: `5`
`safe_string_lower_2 (dict, 'test', 'test2')`: `5`
`safe_string_upper_2 (dict, 'test', 'test2')`: `5`
`safe_string_lower_n (dict, ['test', 'test2'])`: `5`
`safe_string_upper_n (dict, ['test', 'test2'])`: `5`

## dict = {"test":"5","test2":"10"}

`safe_string_lower (dict, 'test')`: `5`
`safe_string_upper (dict, 'test')`: `5`
`safe_string_lower_2 (dict, 'test', 'test2')`: `5`
`safe_string_upper_2 (dict, 'test', 'test2')`: `5`
`safe_string_lower_n (dict, ['test', 'test2'])`: `5`
`safe_string_upper_n (dict, ['test', 'test2'])`: `5`

## dict = {"test":5}

`safe_string_lower (dict, 'test')`: `5`
`safe_string_upper (dict, 'test')`: `5`
`safe_string_lower_2 (dict, 'test', 'test2')`: `5`
`safe_string_upper_2 (dict, 'test', 'test2')`: `5`
`safe_string_lower_n (dict, ['test', 'test2'])`: `5`
`safe_string_upper_n (dict, ['test', 'test2'])`: `5`

## dict = {"test2":"5"}

`safe_string_lower (dict, 'test')`: `undefined`
`safe_string_upper (dict, 'test')`: `undefined`
`safe_string_lower_2 (dict, 'test', 'test2')`: `5`
`safe_string_upper_2 (dict, 'test', 'test2')`: `5`
`safe_string_lower_n (dict, ['test', 'test2'])`: `5`
`safe_string_upper_n (dict, ['test', 'test2'])`: `5`

## dict = {"test":""}

`safe_string_lower (dict, 'test')`: `undefined`
`safe_string_upper (dict, 'test')`: `undefined`
`safe_string_lower_2 (dict, 'test', 'test2')`: `undefined`
`safe_string_upper_2 (dict, 'test', 'test2')`: `undefined`
`safe_string_lower_n (dict, ['test', 'test2'])`: `undefined`
`safe_string_upper_n (dict, ['test', 'test2'])`: `undefined`

## dict = {"test2":""}

`safe_string_lower (dict, 'test')`: `undefined`
`safe_string_upper (dict, 'test')`: `undefined`
`safe_string_lower_2 (dict, 'test', 'test2')`: `undefined`
`safe_string_upper_2 (dict, 'test', 'test2')`: `undefined`
`safe_string_lower_n (dict, ['test', 'test2'])`: `undefined`
`safe_string_upper_n (dict, ['test', 'test2'])`: `undefined`

# python

safe_string_lower ('missing_key', 'DEFAULT') *returns default*: default
safe_string_upper ('missing_key', 'default') *returns DEFAULT*: DEFAULT
safe_string_lower_2 ('missing_key', 'missing_key_2', 'DEFAULT') *returns default*: default
safe_string_upper_2 ('missing_key', 'missing_key_2', 'default') *returns DEFAULT*: DEFAULT
safe_string_lower_n (['missing_key', 'missing_key_2'], 'DEFAULT') *returns default*: default
safe_string_upper_n (['missing_key', 'missing_key_2'], 'default') *returns DEFAULT*: DEFAULT


### {'test': '', 'test2': '10'}

safe_string_lower ('test'): None
safe_string_upper ('test'): None
safe_string_lower_2 ('test', 'test2'): 10
safe_string_upper_2 ('test', 'test2'): 10
safe_string_lower_n (['test', 'test2']): 
safe_string_upper_n (['test', 'test2']): 

### {'test': '5', 'test2': ''}

safe_string_lower ('test'): 5
safe_string_upper ('test'): 5
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5
### {'test': '5', 'test2': '10'}

safe_string_lower ('test'): 5
safe_string_upper ('test'): 5
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {'test': 5}

safe_string_lower ('test'): 5
safe_string_upper ('test'): 5
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {'test2': '5'}

safe_string_lower ('test'): None
safe_string_upper ('test'): None
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {'test': ''}

safe_string_lower ('test'): None
safe_string_upper ('test'): None
safe_string_lower_2 ('test', 'test2'): None
safe_string_upper_2 ('test', 'test2'): None
safe_string_lower_n (['test', 'test2']): 
safe_string_upper_n (['test', 'test2']): 

### {'test2': ''}

safe_string_lower ('test'): None
safe_string_upper ('test'): None
safe_string_lower_2 ('test', 'test2'): None
safe_string_upper_2 ('test', 'test2'): None
safe_string_lower_n (['test', 'test2']): 
safe_string_upper_n (['test', 'test2']): 


# PHP

safe_string_lower ('missing_key', 'DEFAULT') *returns default*: default
safe_string_upper ('missing_key', 'default') *returns DEFAULT*: DEFAULT
safe_string_lower_2 ('missing_key', 'missing_key_2', 'DEFAULT') *returns default*: default
safe_string_upper_2 ('missing_key', 'missing_key_2', 'default') *returns DEFAULT*: DEFAULT
safe_string_lower_n (['missing_key', 'missing_key_2'], 'DEFAULT') *returns default*: default
safe_string_upper_n (['missing_key', 'missing_key_2'], 'default') *returns DEFAULT*: DEFAULT

### {"test":"","test2":"10"}

safe_string_lower ('test'): 
safe_string_upper ('test'): 
safe_string_lower_2 ('test', 'test2'): 10
safe_string_upper_2 ('test', 'test2'): 10
safe_string_lower_n (['test', 'test2']): 
safe_string_upper_n (['test', 'test2']): 

### {"test":"5","test2":"10"}

safe_string_lower ('test'): 5
safe_string_upper ('test'): 5
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {"test":"5","test2":"10"}

safe_string_lower ('test'): 5
safe_string_upper ('test'): 5
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {"test":"5"}

safe_string_lower ('test'): 5
safe_string_upper ('test'): 5
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {"test2":"5"}

safe_string_lower ('test'): 
safe_string_upper ('test'): 
safe_string_lower_2 ('test', 'test2'): 5
safe_string_upper_2 ('test', 'test2'): 5
safe_string_lower_n (['test', 'test2']): 5
safe_string_upper_n (['test', 'test2']): 5

### {"test":""}

safe_string_lower ('test'): 
safe_string_upper ('test'): 
safe_string_lower_2 ('test', 'test2'): 
safe_string_upper_2 ('test', 'test2'): 
safe_string_lower_n (['test', 'test2']): 
safe_string_upper_n (['test', 'test2']): 

### {"test2":""}

safe_string_lower ('test'): 
safe_string_upper ('test'): 
safe_string_lower_2 ('test', 'test2'): 
safe_string_upper_2 ('test', 'test2'): 
safe_string_lower_n (['test', 'test2']): 
safe_string_upper_n (['test', 'test2']): 


Problem statement:
Withdrawal from Huobi problem
Hello. I test withdrawals from Huobi to Kucoin and I got some problems.
First, when I try to do this: `huobi.withdraw("USDT", 15, my_address, tag=None, params={"network": "ERC20"})`
I got
`ccxt.base.errors.BadRequest: huobipro {"status":"error","err-code":"base-currency-chain-error","err-msg":"The current currency chain does not exist","data":null}`
Why?
Ok, then I change network name to "ETH" and got this: `ccxt.base.errors.PermissionDenied: huobipro {"status":"error","err-code":"api-not-support-temp-addr","err-msg":"API withdrawal does not support temporary addresses","data":null}`
My questions are:
1) How should the name of the network be given for any exchange? When I withdrawed from MEXC, I filled network params with "ERC20" and "BEP20". It worked. Now on Huobi it doesn't.
2) What temporary addresses mean and how to fix it?

Hints from issue discussion:
And question #3. Now I am trying to withdraw from Kucoin. First, it gives me error Kucoin needs to have a number for withdraw with only 6 decimal numbers after "." Ok, done it, but now with this code:
```
exchangeA = exchangeA
coin = coin
amount = exchangeA.fetch_balance()["free"][coin]
if exchangeA==self.kucoin:
    begining=''
    final = ''
    if '.' in str(amount):
        begining+=str(amount).split(".")[0]+'.'
        if len(str(amount).split(".")[1])>6:
            final+=str(amount).split(".")[1][:5]
        else:
            final+=str(amount).split(".")[1]
    else:
        begining = str(amount)
    amount = float(begining+final)
address = address
exchangeA.withdraw(coin, amount, address, tag=None, params={"network": network})
```
I got this
`ccxt.base.errors.BadRequest: kucoin account.available.Amount`
> And question #3. Now I am trying to withdraw from Kucoin. First, it gives me error Kucoin needs to have a number for withdraw with only 6 decimal numbers after "." Ok, done it, but now with this code:
> 
> ```
> exchangeA = exchangeA
> coin = coin
> amount = exchangeA.fetch_balance()["free"][coin]
> if exchangeA==self.kucoin:
>     begining=''
>     final = ''
>     if '.' in str(amount):
>         begining+=str(amount).split(".")[0]+'.'
>         if len(str(amount).split(".")[1])>6:
>             final+=str(amount).split(".")[1][:5]
>         else:
>             final+=str(amount).split(".")[1]
>     else:
>         begining = str(amount)
>     amount = float(begining+final)
> address = address
> exchangeA.withdraw(coin, amount, address, tag=None, params={"network": network})
> ```
> 
> I got this `ccxt.base.errors.BadRequest: kucoin account.available.Amount`

This error happens when the balance in your account is lower than the amount you are trying to withdraw, note that kucoin has both a main and trading account. The trading account is your spot account

![](https://i.imgur.com/LXMLY4l.png)

> Ok, then I change network name to "ETH" and got this: ccxt.base.errors.PermissionDenied: huobipro {"status":"error","err-code":"api-not-support-temp-addr","err-msg":"API withdrawal does not support temporary addresses","data":null}

I'm not exactly sure what this error means, it's from the huobi exchange, but because it's `PermissionDenied` I think it may be referring to your ip address, so I think you may be trying to withdraw from a non-approved ip address

@ttodua @pcriadoperez @sc0Vu Can one of you try to reproduce this error? The api key I'm using has no withdraw permissions, and I am unable to create a new key or edit my key

> `ccxt.base.errors.BadRequest: huobipro {"status":"error","err-code":"base-currency-chain-error","err-msg":"The current currency chain does not exist","data":null}`

This is a bug, I will push a PR to fix it
@samgermain can you help with Kucoin? I'm trying to withdraw BUSD from Kucoin to MEXC. Give "BEP20" - error `ccxt.base.errors.BadRequest: kucoin Invalid chainId` . 
Give "BSC" - same error `ccxt.base.errors.BadRequest: kucoin Invalid chainId`
Why does it happen? Before it I tested withdraw with USDT TRC20 and everything worked. Kucoin allows to withdraw BUSD by BEP20 and MEXC allows to deposit BUSD with BEP20
@samgermain as I see you have already grasped the problem and as you self-assigned, I suppose that you'll push the PR and after that, I'll test it with real withdrawals. the problem seems just the need of `this.networkCodeToId` base method , with huobi and kucoin too.
> @samgermain as I see you have already grasped the problem and as you self-assigned, I suppose that you'll push the PR and after that, I'll test it with real withdrawals. the problem seems just the need of `this.networkCodeToId` base method , with huobi and kucoin too.

I believe it has to do with the default value of safeStringUpper and safeStringLower not being returned as uppercase and lowercase. See https://github.com/ccxt/ccxt/pull/15812

Commit message: python exchange.get_object_value_from_key_list minor fix

Patch to review:
diff --git a/js/base/functions/type.js b/js/base/functions/type.js
index 4b29f927d3a5f..ed68de1666da9 100644
--- a/js/base/functions/type.js
+++ b/js/base/functions/type.js
@@ -59,8 +59,8 @@ module.exports = {
     , safeTimestamp:      (o, k,          $default, n =   asFloat (prop (o, k))) => (isNumber (n)          ? parseInt (n * 1000)       : $default)
     , safeValue:          (o, k,          $default, x =            prop (o, k))  => (hasProps (x)          ? x                         : $default)
     , safeString:         (o, k,          $default, x =            prop (o, k))  => (isStringCoercible (x) ? String (x)                : $default)
-    , safeStringLower:    (o, k,          $default, x =            prop (o, k))  => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
-    , safeStringUpper:    (o, k,          $default, x =            prop (o, k))  => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
+    , safeStringLower:    (o, k,          $default, x =            prop (o, k))  => (isStringCoercible (x) ? String (x).toLowerCase () : $default ? $default.toLowerCase () : $default)
+    , safeStringUpper:    (o, k,          $default, x =            prop (o, k))  => (isStringCoercible (x) ? String (x).toUpperCase () : $default ? $default.toUpperCase () : $default)
 
     // not using safeFloats with an array argument as we're trying to save some cycles here
     // we're not using safeFloat3 either because those cases are too rare to deserve their own optimization
@@ -71,8 +71,8 @@ module.exports = {
     , safeTimestamp2:      (o, k1, k2,          $default, n =   asFloat (prop2 (o, k1, k2))) => (isNumber (n)          ? parseInt (n * 1000)       : $default)
     , safeValue2:          (o, k1, k2,          $default, x =            prop2 (o, k1, k2))  => (hasProps (x)          ? x                         : $default)
     , safeString2:         (o, k1, k2,          $default, x =            prop2 (o, k1, k2))  => (isStringCoercible (x) ? String (x)                : $default)
-    , safeStringLower2:    (o, k1, k2,          $default, x =            prop2 (o, k1, k2))  => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
-    , safeStringUpper2:    (o, k1, k2,          $default, x =            prop2 (o, k1, k2))  => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
+    , safeStringLower2:    (o, k1, k2,          $default, x =            prop2 (o, k1, k2))  => (isStringCoercible (x) ? String (x).toLowerCase () : $default ? $default.toLowerCase () : $default)
+    , safeStringUpper2:    (o, k1, k2,          $default, x =            prop2 (o, k1, k2))  => (isStringCoercible (x) ? String (x).toUpperCase () : $default ? $default.toUpperCase () : $default)
 
     // safeMethodN
     , safeFloatN:          (o, k,          $default, n =   asFloat (getValueFromKeysInArray (o, k))) => (isNumber (n)          ? n                         : $default)
@@ -81,8 +81,8 @@ module.exports = {
     , safeTimestampN:      (o, k,          $default, n =   asFloat (getValueFromKeysInArray (o, k))) => (isNumber (n)          ? parseInt (n * 1000)       : $default)
     , safeValueN:          (o, k,          $default, x =            getValueFromKeysInArray (o, k))  => (hasProps (x)          ? x                         : $default)
     , safeStringN:         (o, k,          $default, x =            getValueFromKeysInArray (o, k))  => (isStringCoercible (x) ? String (x)                : $default)
-    , safeStringLowerN:    (o, k,          $default, x =            getValueFromKeysInArray (o, k))  => (isStringCoercible (x) ? String (x).toLowerCase () : $default)
-    , safeStringUpperN:    (o, k,          $default, x =            getValueFromKeysInArray (o, k))  => (isStringCoercible (x) ? String (x).toUpperCase () : $default)
+    , safeStringLowerN:    (o, k,          $default, x =            getValueFromKeysInArray (o, k))  => (isStringCoercible (x) ? String (x).toLowerCase () : $default ? $default.toLowerCase () : $default)
+    , safeStringUpperN:    (o, k,          $default, x =            getValueFromKeysInArray (o, k))  => (isStringCoercible (x) ? String (x).toUpperCase () : $default ? $default.toUpperCase () : $default)
 }
 
 /*  ------------------------------------------------------------------------ */
diff --git a/php/Exchange.php b/php/Exchange.php
index 1985d6606aff4..759c3bc086cd2 100644
--- a/php/Exchange.php
+++ b/php/Exchange.php
@@ -456,10 +456,23 @@ public static function safe_string($object, $key, $default_value = null) {
     }
 
     public static function safe_string_lower($object, $key, $default_value = null) {
-        return static::valid_object_value($object, $key) ? strtolower(strval($object[$key])) : $default_value;
+        if (static::valid_object_value($object, $key)) {
+            return strtolower(strval($object[$key]));
+        } else if ($default_value === null) {
+            return $default_value;
+        } else {
+            return strtolower($default_value);
+        }
     }
 
     public static function safe_string_upper($object, $key, $default_value = null) {
+        if (static::valid_object_value($object, $key)) {
+            return strtoupper(strval($object[$key]));
+        } else if ($default_value === null) {
+            return $default_value;
+        } else {
+            return strtoupper($default_value);
+        }
         return static::valid_object_value($object, $key) ? strtoupper(strval($object[$key])) : $default_value;
     }
 
@@ -534,12 +547,24 @@ public static function safe_string_n($object, $array, $default_value = null) {
 
     public static function safe_string_lower_n($object, $array, $default_value = null) {
         $value = static::get_object_value_from_key_array($object, $array);
-        return (static::valid_string($value) && is_scalar($value)) ? strtolower(strval($value)) : $default_value;
+        if (static::valid_string($value) && is_scalar($value)) {
+            return strtolower(strval($value));
+        } else if ($default_value === null) {
+            return $default_value;
+        } else {
+            return strtolower($default_value);
+        }
     }
 
     public static function safe_string_upper_n($object, $array, $default_value = null) {
         $value = static::get_object_value_from_key_array($object, $array);
-        return (static::valid_string($value) && is_scalar($value)) ? strtoupper(strval($value)) : $default_value;
+        if (static::valid_string($value) && is_scalar($value)) {
+            return strtoupper(strval($value));
+        } else if ($default_value === null) {
+            return $default_value;
+        } else {
+            return strtoupper($default_value);
+        }
     }
 
     public static function safe_integer_n($object, $array, $default_value = null) {
@@ -563,7 +588,7 @@ public static function safe_value_n($object, $array, $default_value = null) {
 
     public static function get_object_value_from_key_array($object, $array) {
         foreach($array as $key) {
-            if (isset($object[$key])) {
+            if (isset($object[$key]) && $object[$key] !== '') {
                 return $object[$key];
             }
         }
diff --git a/python/ccxt/base/exchange.py b/python/ccxt/base/exchange.py
index 746c0972e799e..38e1e46f9cbfa 100644
--- a/python/ccxt/base/exchange.py
+++ b/python/ccxt/base/exchange.py
@@ -708,11 +708,17 @@ def safe_string(dictionary, key, default_value=None):
 
     @staticmethod
     def safe_string_lower(dictionary, key, default_value=None):
-        return str(dictionary[key]).lower() if Exchange.key_exists(dictionary, key) else default_value
+        if Exchange.key_exists(dictionary, key):
+            return str(dictionary[key]).lower()
+        else:
+            return default_value.lower() if default_value is not None else default_value
 
     @staticmethod
     def safe_string_upper(dictionary, key, default_value=None):
-        return str(dictionary[key]).upper() if Exchange.key_exists(dictionary, key) else default_value
+        if Exchange.key_exists(dictionary, key):
+            return str(dictionary[key]).upper()
+        else:
+            return default_value.upper() if default_value is not None else default_value
 
     @staticmethod
     def safe_integer(dictionary, key, default_value=None):
@@ -807,12 +813,22 @@ def safe_string_n(dictionary, key_list, default_value=None):
     @staticmethod
     def safe_string_lower_n(dictionary, key_list, default_value=None):
         value = Exchange.get_object_value_from_key_list(dictionary, key_list)
-        return str(value).lower() if value is not None else default_value
+        if value is not None:
+            return str(value).lower()
+        elif default_value is None:
+            return default_value
+        else:
+            return default_value.lower()
 
     @staticmethod
     def safe_string_upper_n(dictionary, key_list, default_value=None):
         value = Exchange.get_object_value_from_key_list(dictionary, key_list)
-        return str(value).upper() if value is not None else default_value
+        if value is not None:
+            return str(value).upper()
+        elif default_value is None:
+            return default_value
+        else:
+            return default_value.upper()
 
     @staticmethod
     def safe_integer_n(dictionary, key_list, default_value=None):
@@ -853,7 +869,7 @@ def safe_value_n(dictionary, key_list, default_value=None):
 
     @staticmethod
     def get_object_value_from_key_list(dictionary, key_list):
-        filtered_list = list(filter(lambda el: el in dictionary, key_list))
+        filtered_list = list(filter(lambda el: el in dictionary and dictionary[el] != '' and dictionary[el] is not None, key_list))
         if (len(filtered_list) == 0):
             return None
         return dictionary[filtered_list[0]]


Code review comments:
diff_hunk: @@ -853,7 +869,7 @@ def safe_value_n(dictionary, key_list, default_value=None):
 
     @staticmethod
     def get_object_value_from_key_list(dictionary, key_list):
-        filtered_list = list(filter(lambda el: el in dictionary, key_list))
+        filtered_list = list(filter(lambda el: el in dictionary and dictionary[el] != '' and dictionary[el] is not None, key_list))
path: python/ccxt/base/exchange.py
text: @user1:
@author why is this needed? can you give us a real-case scenario?








@author:
These are the examples, I'll need to go to a PR I did about 6 months ago to find the real use case, I fixed the problem for `safe_string_2` but not `safe_string_n`

```python
safe_string_lower_n ({'test': '', 'test2': '10'}, ['test', 'test2'])    # should return 10, actually returns ''
safe_string_upper_n ({'test': '', 'test2': '10'}, ['test', 'test2'])    # should return 10, actually returns ''
safe_string_lower_n ({'test': ''}, ['test', 'test2'])                   # should return None, actually returns ''
safe_string_upper_n ({'test': ''}, ['test', 'test2'])                   # should return None, actually returns ''
safe_string_lower_n ({'test2': ''}, ['test', 'test2'])                  # should return None, actually returns ''
safe_string_upper_n ({'test2': ''}, ['test', 'test2'])                  # should return None, actually returns ''
```

@author:
[Here's the PR where it was fixed for `safeString2`](https://github.com/ccxt/ccxt/pull/13438)

It looks like [OKX stop orders were having issues with dictionary/object values being empty strings](https://github.com/ccxt/ccxt/pull/13440)



@user1:
@author Yeah okx might have a weird api but I'm not sure we should do that by default.

For instance:
```Javascript
safe_string_lower_n ({'test': ''}, ['test', 'test2'], "defaultValue") // will return "defaultValue"
```   
⬆️ why should we return the defaultValue here and hide the key existence? 

@author:
> @author Yeah okx might have a weird api but I'm not sure we should do that by default.
> 
> For instance:
> 
> ```js
> safe_string_lower_n ({'test': ''}, ['test', 'test2'], "defaultValue") // will return "defaultValue"
> ```
> 
> ⬆️ why should we return the defaultValue here and hide the key existence?

Because there isn't a value, if we need a number for string math or something, we would need to perform the math on either the value that the exchange gives us, or on the default value, but we don't want to perform it on an empty string, which is the absence of a value


@author:
I moved this change to a separate PR https://github.com/ccxt/ccxt/pull/15854

@user1:
@author thanks we can keep this discussion there then.