Connector Open-Source Code

Browse connector files locally. Exchange API keys stay on your device.

ui/exchange.php

<?php
// /opt/nuxvision_connector/ui/exchange.php
declare(strict_types=1);

require_once __DIR__ . '/common.php';

/* =========================================================
   CONFIG
   ========================================================= */
const NV_BASE_DEFAULT = 'https://nuxvision.com/api/v1';

/* -------------------- local helpers -------------------- */
function current_instance_id(): int {
    $id = 0;
    if (isset($_GET['instance_id']))  $id = to_int($_GET['instance_id'], 0);
    if (isset($_POST['instance_id'])) $id = to_int($_POST['instance_id'], $id);
    if ($id <= 0) $id = nv_instance_id_from_session();
    return $id > 0 ? $id : 0;
}

function cfg_defaults(): array {
    return [
        'nuxvision' => [
            'base_url' => NV_BASE_DEFAULT,
            'api_key'  => '',
            'timeout'  => 8,
        ],
        'exchange' => [
            // name is auto-detected from NuxVision instance
            'name'       => '',
            'api_key'    => '',
            'api_secret' => '',
        ],
    ];
}

function load_config(int $instanceId): array {
    $d = cfg_defaults();
    if ($instanceId <= 0) return $d;

    $path = nv_instance_config_path($instanceId);
    if (!is_file($path)) return $d;

    $cfg = @require $path;
    if (!is_array($cfg)) return $d;

    foreach (['nuxvision','exchange'] as $k) {
        if (isset($cfg[$k]) && is_array($cfg[$k])) {
            $d[$k] = array_replace($d[$k], $cfg[$k]);
        }
    }

    // lock base_url to prod default
    $d['nuxvision']['base_url'] = NV_BASE_DEFAULT;

    // never use exchange base_url from UI
    if (isset($d['exchange']) && is_array($d['exchange'])) {
        $d['exchange']['base_url'] = '';
    }

    return $d;
}

function save_config(int $instanceId, array $cfg): bool {
    if ($instanceId <= 0) return false;

    $dir = nv_instance_dir($instanceId);
    if (!is_dir($dir)) @mkdir($dir, 0775, true);

    $path = nv_instance_config_path($instanceId);

    // Enforce invariants
    if (!isset($cfg['exchange']) || !is_array($cfg['exchange'])) $cfg['exchange'] = [];
    $cfg['exchange']['base_url'] = '';

    if (!isset($cfg['nuxvision']) || !is_array($cfg['nuxvision'])) $cfg['nuxvision'] = [];
    $cfg['nuxvision']['base_url'] = NV_BASE_DEFAULT;

    $out = [
        'nuxvision' => (array)($cfg['nuxvision'] ?? []),
        'exchange'  => (array)($cfg['exchange'] ?? []),
    ];

    $export = var_export($out, true);
    $php = "<?php\n// generated by connector UI\nreturn {$export};\n";
    return (bool)@file_put_contents($path, $php, LOCK_EX);
}

/* -------------------- temp form persistence (for Test) -------------------- */
function nv_exchange_tmp_key(int $instanceId): string {
    return 'nv_exchange_form_' . $instanceId;
}
function nv_exchange_tmp_set(int $instanceId, string $apiKey, string $apiSecret): void {
    if ($instanceId <= 0) return;
    if (!isset($_SESSION) || !is_array($_SESSION)) return;
    $_SESSION[nv_exchange_tmp_key($instanceId)] = [
        'api_key'    => $apiKey,
        'api_secret' => $apiSecret,
    ];
}
function nv_exchange_tmp_pop(int $instanceId): ?array {
    if ($instanceId <= 0) return null;
    if (!isset($_SESSION) || !is_array($_SESSION)) return null;
    $k = nv_exchange_tmp_key($instanceId);
    if (!isset($_SESSION[$k]) || !is_array($_SESSION[$k])) return null;
    $v = $_SESSION[$k];
    unset($_SESSION[$k]);
    return $v;
}

/* -------------------- NV helpers -------------------- */
function nv_detect_exchange_name(string $apiKey, int $instanceId): array {
    $apiKey = trim($apiKey);
    if ($apiKey === '' || $instanceId <= 0) {
        return ['ok' => false, 'name' => '', 'err' => 'missing_params', 'http' => 0, 'raw' => ''];
    }

    $url = rtrim(NV_BASE_DEFAULT, '/') . '/instance_list.php';

    [$code, $curlErr, $body, $json] = curl_json($url, [
        'Accept'    => 'application/json',
        'X-API-KEY' => $apiKey,
    ], 8);

    if ($curlErr !== '') {
        return ['ok' => false, 'name' => '', 'err' => $curlErr, 'http' => (int)$code, 'raw' => is_string($body) ? substr($body, 0, 200) : ''];
    }
    if ($code < 200 || $code >= 400 || !is_array($json) || empty($json['ok']) || !isset($json['instances']) || !is_array($json['instances'])) {
        return ['ok' => false, 'name' => '', 'err' => 'http_error', 'http' => (int)$code, 'raw' => is_string($body) ? substr($body, 0, 200) : ''];
    }

    foreach ($json['instances'] as $row) {
        if (!is_array($row)) continue;
        $id = (int)($row['id'] ?? 0);
        if ($id !== $instanceId) continue;

        $ex = strtolower(trim((string)($row['exchange'] ?? '')));
        if ($ex === '') {
            return ['ok' => false, 'name' => '', 'err' => 'missing_exchange', 'http' => (int)$code, 'raw' => ''];
        }
        return ['ok' => true, 'name' => $ex, 'err' => '', 'http' => (int)$code, 'raw' => ''];
    }

    return ['ok' => false, 'name' => '', 'err' => 'instance_not_found', 'http' => (int)$code, 'raw' => ''];
}

/* -------------------- Exchange adapter test -------------------- */
function exchange_adapter_load(string $name): array {
    $name = strtolower(trim($name));
    if ($name === '') return ['ok' => false, 'err' => 'missing_exchange_name', 'adapter' => null];

    // UI dir = /opt/nuxvision_connector/ui, adapters = /opt/nuxvision_connector/exchanges
    $path = realpath(__DIR__ . '/../exchanges/' . $name . '.php');
    if (!$path || !is_file($path)) {
        return ['ok' => false, 'err' => 'adapter_not_found', 'adapter' => null];
    }

    $adapter = @require $path;
    if (!is_array($adapter)) {
        return ['ok' => false, 'err' => 'adapter_invalid', 'adapter' => null];
    }

    return ['ok' => true, 'err' => '', 'adapter' => $adapter];
}

function exchange_test_keys(string $exchangeName, array $cfgExchange): array {
    $load = exchange_adapter_load($exchangeName);
    if (empty($load['ok'])) {
        return ['ok' => false, 'err' => (string)($load['err'] ?? 'adapter_load_failed')];
    }

    $ad = $load['adapter'];
    if (!is_array($ad) || !isset($ad['wallet']) || !is_callable($ad['wallet'])) {
        return ['ok' => false, 'err' => 'wallet_not_supported'];
    }

    // Never force base_url from UI
    $cfgExchange['base_url'] = '';

    $r = $ad['wallet']($cfgExchange);

    // Expect standard structure from your adapters: ['ok'=>bool,'code'=>int,'json'=>..., 'raw'=>...]
    $okHttp = !empty($r['ok']);
    $code   = (int)($r['code'] ?? 0);
    $json   = $r['json'] ?? null;

    // Bitkub-specific: if JSON has "error" and it's not 0, treat as failure
    if (is_array($json) && array_key_exists('error', $json)) {
        $e = (int)$json['error'];
        if ($e !== 0) {
            return ['ok' => false, 'err' => 'exchange_error_' . $e, 'code' => $code];
        }
    }

    if (!$okHttp) {
        return ['ok' => false, 'err' => 'http_or_auth_failed', 'code' => $code];
    }

    return ['ok' => true, 'err' => '', 'code' => $code];
}

/* =========================================================
   PAGE LOGIC
   ========================================================= */
$instanceId = current_instance_id();
if ($instanceId <= 0) {
    nv_flash_set('err', 'Missing instance_id. Please complete Step 1 first.');
    header('Location: ./nuxvision.php');
    exit;
}
nv_set_instance_in_session($instanceId);

$cfg = load_config($instanceId);

// If we just came back from "Test", restore the typed values (without saving to disk)
$tmp = nv_exchange_tmp_pop($instanceId);
if (is_array($tmp)) {
    if (array_key_exists('api_key', $tmp))    $cfg['exchange']['api_key'] = (string)$tmp['api_key'];
    if (array_key_exists('api_secret', $tmp)) $cfg['exchange']['api_secret'] = (string)$tmp['api_secret'];
    $cfg['exchange']['base_url'] = '';
}

// Ensure Step 1 exists
$nvKey = trim((string)($cfg['nuxvision']['api_key'] ?? ''));
if ($nvKey === '') {
    nv_flash_set('err', 'NuxVision config is missing. Please complete Step 1 first.');
    header('Location: ./nuxvision.php?instance_id=' . urlencode((string)$instanceId));
    exit;
}

// Auto-detect exchange name from NuxVision instance
$det = nv_detect_exchange_name($nvKey, $instanceId);
if (!empty($det['ok'])) {
    $cfg['exchange']['name'] = (string)$det['name'];
} else {
    if (trim((string)($cfg['exchange']['name'] ?? '')) === '') {
        nv_flash_set('warn', 'Could not detect exchange name from NuxVision (you can still save credentials).');
    }
}

// Handle POST actions
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    $do = (string)($_POST['do'] ?? 'save'); // default to save if missing

    $apiKey    = trim((string)($_POST['api_key'] ?? ''));
    $apiSecret = trim((string)($_POST['api_secret'] ?? ''));

    // Update in-memory cfg from form
    $cfg['exchange']['api_key']    = $apiKey;
    $cfg['exchange']['api_secret'] = $apiSecret;
    $cfg['exchange']['base_url']   = '';

    // Refresh exchange name on any action (best-effort)
    $det2 = nv_detect_exchange_name($nvKey, $instanceId);
    if (!empty($det2['ok'])) {
        $cfg['exchange']['name'] = (string)$det2['name'];
    }

    $exchangeName = strtolower(trim((string)($cfg['exchange']['name'] ?? '')));

    if ($do === 'test') {
        // Persist typed values for the redirect so the form doesn't "lose" them
        nv_exchange_tmp_set($instanceId, $apiKey, $apiSecret);

        if ($apiKey === '' || $apiSecret === '') {
            nv_flash_set('err', 'Please enter both API key and API secret before testing.');
        } elseif ($exchangeName === '') {
            nv_flash_set('err', 'Exchange name is missing (cannot load adapter).');
        } else {
            $t = exchange_test_keys($exchangeName, [
                'api_key'    => $apiKey,
                'api_secret' => $apiSecret,
                'base_url'   => '',
            ]);

            if (!empty($t['ok'])) {
                nv_flash_set('ok', 'Test successful: exchange API credentials are valid.');
            } else {
                $code = (int)($t['code'] ?? 0);
                $msg = 'Test failed';
                if ($exchangeName !== '') $msg .= ' (' . strtoupper($exchangeName) . ')';
                if ($code > 0) $msg .= ' • HTTP ' . $code;
                if (!empty($t['err'])) $msg .= ' • ' . (string)$t['err'];
                nv_flash_set('err', $msg);
            }
        }

        header('Location: ./exchange.php?instance_id=' . urlencode((string)$instanceId));
        exit;
    }

    // Default: save
    if ($apiKey === '' || $apiSecret === '') {
        nv_flash_set('err', 'Please enter both API key and API secret.');
    } else {
        if (save_config($instanceId, $cfg)) {
            nv_flash_set('ok', 'Saved.');
            header('Location: ./finish.php?instance_id=' . urlencode((string)$instanceId));
            exit;
        }
        nv_flash_set('err', 'Failed to write config.php (permissions).');
    }
}

/* =========================================================
   RENDER
   ========================================================= */
render_header('Exchange', 'Step 2 — Exchange');
render_flash();

$exName = strtoupper(trim((string)($cfg['exchange']['name'] ?? '')));
if ($exName === '') $exName = 'UNKNOWN';
?>
<div class="nv-card">
<div class="nv-titlebar">
    <div class="d-flex align-items-center gap-2">
        <strong>Exchange credentials</strong>
        <span class="nv-muted">Instance <?=h((string)$instanceId)?> • <?=h($exName)?></span>
    </div>

    <a class="btn btn-soft btn-sm" href="./index.php" title="Home">
        <i class="bi bi-house me-1"></i>Home
    </a>
</div>
    <div class="nv-pad">
        <form method="post" action="./exchange.php?instance_id=<?=h((string)$instanceId)?>">
            <input type="hidden" name="instance_id" value="<?=h((string)$instanceId)?>">
            <input type="hidden" name="do" value="save">

            <div class="row g-3">
                <div class="col-12">
                    <label class="form-label">Exchange</label>
                    <input class="form-control" value="<?=h($exName)?>" readonly>
                    <div class="nv-muted small mt-1">Detected from your NuxVision instance.</div>
                </div>

                <div class="col-12 col-md-6">
                    <label class="form-label">API key</label>
                    <input class="form-control" name="api_key" value="<?=h((string)($cfg['exchange']['api_key'] ?? ''))?>">
                </div>

                <div class="col-12 col-md-6">
                    <label class="form-label">API secret</label>
                    <input class="form-control" name="api_secret" value="<?=h((string)($cfg['exchange']['api_secret'] ?? ''))?>">
                </div>
            </div>

            <div class="d-flex flex-wrap gap-2 justify-content-between mt-4">
                <a class="btn btn-soft" href="./nuxvision.php?instance_id=<?=h((string)$instanceId)?>">
                    <i class="bi bi-arrow-left me-1"></i>Back
                </a>

                <div class="d-flex gap-2">
                    <button class="btn btn-soft" type="submit" onclick="this.form.do.value='test'">
                        <i class="bi bi-shield-check me-1"></i>Test API keys
                    </button>

                    <button class="btn btn-accent" type="submit" onclick="this.form.do.value='save'">
                        <i class="bi bi-check2-circle me-1"></i>Continue to Finish
                    </button>
                </div>
            </div>
        </form>
    </div>
</div>
<?php render_footer(); ?>