Добавить диагностику server PDA и баланс device (не проверено)
This commit is contained in:
parent
ee3721dfa4
commit
eeb115584d
@ -0,0 +1,17 @@
|
|||||||
|
# Диагностика ключей server PDA и баланс device
|
||||||
|
|
||||||
|
- статус: pending
|
||||||
|
- кратко: на странице обновления server PDA добавлена сверка ожидаемых ключей с уже загруженной PDA, предупреждение о неверном пароле, кнопка показа баланса device-аккаунта и уточнение, что create/update оплачиваются с deviceKey.
|
||||||
|
|
||||||
|
## Что проверять
|
||||||
|
- На `update-server-pda.html` загрузить существующую PDA и убедиться, что видны ожидаемые `root/blockchain/device` public key.
|
||||||
|
- Ввести правильный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи совпадают.
|
||||||
|
- Ввести неверный пароль и нажать `Сгенерировать`: должно появиться сообщение, что ключи не совпали и пароль, вероятно, неверный.
|
||||||
|
- На `create-server-pda.html` и `update-server-pda.html` нажать `Показать / обновить баланс device` и убедиться, что баланс читается по текущему `devPub`.
|
||||||
|
- Повторить `update_user_pda` после увеличения `heap frame` и проверить, ушла ли ошибка `memory allocation failed`.
|
||||||
|
|
||||||
|
## Ожидаемый результат
|
||||||
|
- Пользователь видит, какие именно public key должны получиться для загруженной PDA.
|
||||||
|
- Ошибка неправильного пароля выявляется до отправки транзакции.
|
||||||
|
- Баланс device-кошелька читается прямо со страницы.
|
||||||
|
- Если проблема `OOM` была только в размере heap frame/compute budget клиента, `update_user_pda` начинает проходить.
|
||||||
@ -82,10 +82,17 @@ anchor deploy -p shine_users
|
|||||||
- seed: `shine_users_economy_config`
|
- seed: `shine_users_economy_config`
|
||||||
- program: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
- program: `FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm`
|
||||||
|
|
||||||
|
## Кто оплачивает create/update user_pda
|
||||||
|
|
||||||
|
- И обычная регистрация `create_user_pda`, и последующее `update_user_pda` оплачиваются с `deviceKey`.
|
||||||
|
- В UI это означает, что Solana fee payer всегда берётся из `device`-ключа пользователя/сервера.
|
||||||
|
- `rootKey` нужен для подписи unsigned PDA-записи, но не оплачивает транзакцию.
|
||||||
|
- Для server UI это особенно важно: перед `create` и `update` нужно пополнять именно Solana-адрес `deviceKey`.
|
||||||
|
|
||||||
## Важно
|
## Важно
|
||||||
|
|
||||||
- `init_users_economy_config` выполняется один раз на программу.
|
- `init_users_economy_config` выполняется один раз на программу.
|
||||||
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
|
Если PDA уже создан, повторный вызов вернёт ошибку "already initialized" (это нормальное поведение).
|
||||||
- Серверные приватные ключи для Solana не используются: подписание делается кошельком пользователя в UI.
|
- Серверные приватные ключи для Solana не используются как отдельный backend-wallet: в UI/server UI транзакцию оплачивает именно `deviceKey`, а содержимое записи подписывает `rootKey`.
|
||||||
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
|
- `shine_users` внутри `create_user_pda` требует корректный адрес `shine_login_guard` для CPI-классификации логина.
|
||||||
Несовпадение адреса приведёт к ошибке регистрации.
|
Несовпадение адреса приведёт к ошибке регистрации.
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.120
|
client.version=1.2.121
|
||||||
server.version=1.2.112
|
server.version=1.2.113
|
||||||
|
|||||||
@ -913,8 +913,8 @@ export async function updateShineUserPdaOnSolana({
|
|||||||
],
|
],
|
||||||
data: ixData,
|
data: ixData,
|
||||||
});
|
});
|
||||||
const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 });
|
const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 });
|
||||||
const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 131_072 });
|
const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 });
|
||||||
|
|
||||||
const signature = await solana.sendAndConfirmTransaction(
|
const signature = await solana.sendAndConfirmTransaction(
|
||||||
connection,
|
connection,
|
||||||
|
|||||||
@ -29,6 +29,8 @@
|
|||||||
.sol-adr { font-family:monospace; font-size:12px; word-break:break-all; margin-top:4px; }
|
.sol-adr { font-family:monospace; font-size:12px; word-break:break-all; margin-top:4px; }
|
||||||
.sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; }
|
.sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; }
|
||||||
.sol-topup-btn { margin-top:10px; width:100%; }
|
.sol-topup-btn { margin-top:10px; width:100%; }
|
||||||
|
.sol-balance-btn { margin-top:10px; width:100%; }
|
||||||
|
.sol-balance { font-size:11px; color:var(--text-muted); margin-top:8px; word-break:break-all; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -112,8 +114,10 @@
|
|||||||
<div class="sol-box" id="solBox">
|
<div class="sol-box" id="solBox">
|
||||||
<div class="sol-ttl">Положите SOL на этот адрес перед регистрацией:</div>
|
<div class="sol-ttl">Положите SOL на этот адрес перед регистрацией:</div>
|
||||||
<div class="sol-adr" id="solAdr"></div>
|
<div class="sol-adr" id="solAdr"></div>
|
||||||
<div class="sol-ht">Это Solana-адрес device-ключа (public key в base58 = Solana-адрес). С него оплачивается создание PDA.</div>
|
<div class="sol-ht">Это Solana-адрес device-ключа (public key в base58 = Solana-адрес). С него оплачиваются и создание, и обновление PDA.</div>
|
||||||
<button class="btn-secondary sol-topup-btn" id="btnTopupDevnet" type="button">Открыть пополнение DEVNET</button>
|
<button class="btn-secondary sol-topup-btn" id="btnTopupDevnet" type="button">Открыть пополнение DEVNET</button>
|
||||||
|
<button class="btn-secondary sol-balance-btn" id="btnRefreshBalance" type="button">Показать / обновить баланс device</button>
|
||||||
|
<div class="sol-balance" id="deviceBalance">Баланс device ещё не запрашивался.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
deriveKeyBundleFromPassword,
|
deriveKeyBundleFromPassword,
|
||||||
fillKeyFields,
|
fillKeyFields,
|
||||||
parseLoginList,
|
parseLoginList,
|
||||||
|
refreshDeviceBalance,
|
||||||
setGenMessage,
|
setGenMessage,
|
||||||
setStatus,
|
setStatus,
|
||||||
setupPasswordEye,
|
setupPasswordEye,
|
||||||
@ -40,6 +41,21 @@ $('btnTopupDevnet').addEventListener('click', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('btnRefreshBalance').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
$('btnRefreshBalance').disabled = true;
|
||||||
|
await refreshDeviceBalance({
|
||||||
|
endpoint: $('endpoint').value,
|
||||||
|
deviceAddress: $('devPub').value,
|
||||||
|
targetNode: $('deviceBalance'),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setStatus($('status'), error?.message || String(error), 'error');
|
||||||
|
} finally {
|
||||||
|
$('btnRefreshBalance').disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('btnGen').addEventListener('click', async () => {
|
$('btnGen').addEventListener('click', async () => {
|
||||||
clearGenMessage($('genMsg'));
|
clearGenMessage($('genMsg'));
|
||||||
clearStatus($('status'));
|
clearStatus($('status'));
|
||||||
@ -53,7 +69,8 @@ $('btnGen').addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
fillKeyFields(fieldMap, keyBundle, masterSecret32);
|
fillKeyFields(fieldMap, keyBundle, masterSecret32);
|
||||||
updateSolAddress(fieldMap);
|
updateSolAddress(fieldMap);
|
||||||
setGenMessage($('genMsg'), 'Ключи и master secret сгенерированы из логина и пароля.', 'ok');
|
$('deviceBalance').textContent = 'Теперь можно проверить баланс device-аккаунта.';
|
||||||
|
setGenMessage($('genMsg'), 'Ключи и master secret сгенерированы из логина и пароля. Транзакцию оплатит device-ключ.', 'ok');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setGenMessage($('genMsg'), error?.message || String(error), 'err');
|
setGenMessage($('genMsg'), error?.message || String(error), 'err');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
deriveMasterSecretFromPassword,
|
deriveMasterSecretFromPassword,
|
||||||
publicKeyB64FromPkcs8Ed25519,
|
publicKeyB64FromPkcs8Ed25519,
|
||||||
} from '../../js/services/crypto-utils.js';
|
} from '../../js/services/crypto-utils.js';
|
||||||
|
import { formatSol, getBalanceSol } from '../../js/services/solana-wallet-service.js';
|
||||||
|
|
||||||
const LOGIN_RE = /^[a-z0-9_]{1,20}$/;
|
const LOGIN_RE = /^[a-z0-9_]{1,20}$/;
|
||||||
const ED25519_PKCS8_PREFIX = new Uint8Array([
|
const ED25519_PKCS8_PREFIX = new Uint8Array([
|
||||||
@ -189,12 +190,56 @@ export function updateSolAddress(fieldMap) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setText(idOrNode, value) {
|
||||||
|
const node = typeof idOrNode === 'string' ? $(idOrNode) : idOrNode;
|
||||||
|
if (node) node.textContent = String(value || '');
|
||||||
|
}
|
||||||
|
|
||||||
export function wireDeviceAddressPreview(fieldMap) {
|
export function wireDeviceAddressPreview(fieldMap) {
|
||||||
const update = () => updateSolAddress(fieldMap);
|
const update = () => updateSolAddress(fieldMap);
|
||||||
$(fieldMap.devPub).addEventListener('input', update);
|
$(fieldMap.devPub).addEventListener('input', update);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function publicKeyBytesToBase58(value) {
|
||||||
|
return bytesToBase58(value instanceof Uint8Array ? value : new Uint8Array(value || []));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareExpectedPublicKeys(expected, actual) {
|
||||||
|
const exp = String(expected || '').trim();
|
||||||
|
const act = String(actual || '').trim();
|
||||||
|
return {
|
||||||
|
matches: Boolean(exp) && Boolean(act) && exp === act,
|
||||||
|
expected: exp,
|
||||||
|
actual: act,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function summarizeKeyComparison(resultMap) {
|
||||||
|
const labels = {
|
||||||
|
root: 'root',
|
||||||
|
blockchain: 'blockchain',
|
||||||
|
device: 'device',
|
||||||
|
};
|
||||||
|
const mismatches = Object.entries(resultMap)
|
||||||
|
.filter(([, result]) => !result.matches)
|
||||||
|
.map(([key]) => labels[key] || key);
|
||||||
|
return {
|
||||||
|
allMatch: mismatches.length === 0,
|
||||||
|
mismatches,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshDeviceBalance({ endpoint, deviceAddress, targetNode }) {
|
||||||
|
const cleanEndpoint = String(endpoint || '').trim();
|
||||||
|
const cleanAddress = String(deviceAddress || '').trim();
|
||||||
|
if (!cleanEndpoint) throw new Error('Укажите Solana endpoint');
|
||||||
|
if (!cleanAddress) throw new Error('Сначала укажите device-адрес');
|
||||||
|
const balance = await getBalanceSol({ endpoint: cleanEndpoint, address: cleanAddress });
|
||||||
|
setText(targetNode, `Баланс device: ${formatSol(balance.sol, 6)} SOL (${balance.lamports} lamports)`);
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildDevnetTopupUrl(walletAddress) {
|
export function buildDevnetTopupUrl(walletAddress) {
|
||||||
const cleanWallet = String(walletAddress || '').trim();
|
const cleanWallet = String(walletAddress || '').trim();
|
||||||
const url = new URL('../devnet-topup-view', window.location.href);
|
const url = new URL('../devnet-topup-view', window.location.href);
|
||||||
|
|||||||
@ -4,15 +4,20 @@ import {
|
|||||||
buildKeyBundleFromForm,
|
buildKeyBundleFromForm,
|
||||||
clearGenMessage,
|
clearGenMessage,
|
||||||
clearStatus,
|
clearStatus,
|
||||||
|
compareExpectedPublicKeys,
|
||||||
deriveKeyBundleFromPassword,
|
deriveKeyBundleFromPassword,
|
||||||
fillKeyFields,
|
fillKeyFields,
|
||||||
formatBigInt,
|
formatBigInt,
|
||||||
formatTimestamp,
|
formatTimestamp,
|
||||||
openDevnetTopup,
|
openDevnetTopup,
|
||||||
parseLoginList,
|
parseLoginList,
|
||||||
|
publicKeyBytesToBase58,
|
||||||
|
refreshDeviceBalance,
|
||||||
setGenMessage,
|
setGenMessage,
|
||||||
setStatus,
|
setStatus,
|
||||||
|
setText,
|
||||||
setupPasswordEye,
|
setupPasswordEye,
|
||||||
|
summarizeKeyComparison,
|
||||||
updateSolAddress,
|
updateSolAddress,
|
||||||
validateLoginOrThrow,
|
validateLoginOrThrow,
|
||||||
wireDeviceAddressPreview,
|
wireDeviceAddressPreview,
|
||||||
@ -32,9 +37,62 @@ const fieldMap = {
|
|||||||
|
|
||||||
let currentPda = null;
|
let currentPda = null;
|
||||||
|
|
||||||
|
function resetExpectedKeysUi() {
|
||||||
|
$('expectedKeysBox').style.display = 'none';
|
||||||
|
setText('expectedRootPub', '');
|
||||||
|
setText('expectedBchPub', '');
|
||||||
|
setText('expectedDevPub', '');
|
||||||
|
clearGenMessage($('expectedKeysStatus'));
|
||||||
|
setText('deviceBalance', 'Баланс device ещё не запрашивался.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExpectedKeys(parsed) {
|
||||||
|
$('expectedKeysBox').style.display = 'block';
|
||||||
|
setText('expectedRootPub', publicKeyBytesToBase58(parsed.rootKey));
|
||||||
|
setText('expectedBchPub', publicKeyBytesToBase58(parsed.blockchain.blockchainPublicKey));
|
||||||
|
setText('expectedDevPub', publicKeyBytesToBase58(parsed.deviceKey));
|
||||||
|
setGenMessage($('expectedKeysStatus'), 'После генерации ключей этот блок покажет, совпадают ли они с уже записанной PDA.', 'warn');
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareCurrentFormKeysWithPda() {
|
||||||
|
if (!currentPda) throw new Error('Сначала загрузите PDA');
|
||||||
|
const blockchainActual = String($('bchPub').value || '').trim();
|
||||||
|
return {
|
||||||
|
resultMap: {
|
||||||
|
root: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.rootKey), $('rootPub').value),
|
||||||
|
blockchain: blockchainActual
|
||||||
|
? compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), blockchainActual)
|
||||||
|
: { matches: true, expected: publicKeyBytesToBase58(currentPda.blockchain.blockchainPublicKey), actual: '' },
|
||||||
|
device: compareExpectedPublicKeys(publicKeyBytesToBase58(currentPda.deviceKey), $('devPub').value),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComparisonStatus() {
|
||||||
|
if (!currentPda) return false;
|
||||||
|
try {
|
||||||
|
const { resultMap } = compareCurrentFormKeysWithPda();
|
||||||
|
const summary = summarizeKeyComparison(resultMap);
|
||||||
|
if (summary.allMatch) {
|
||||||
|
setGenMessage($('expectedKeysStatus'), 'Ключи совпадают с загруженной PDA. Похоже, пароль верный.', 'ok');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
setGenMessage(
|
||||||
|
$('expectedKeysStatus'),
|
||||||
|
`Не совпали ключи: ${summary.mismatches.join(', ')}. Похоже, пароль неверный или введены не те ключи.`,
|
||||||
|
'err',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
setGenMessage($('expectedKeysStatus'), error?.message || String(error), 'err');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupPasswordEye($('btnEye'), $('password'));
|
setupPasswordEye($('btnEye'), $('password'));
|
||||||
wireDeviceAddressPreview(fieldMap);
|
wireDeviceAddressPreview(fieldMap);
|
||||||
$('password').value = '';
|
$('password').value = '';
|
||||||
|
resetExpectedKeysUi();
|
||||||
|
|
||||||
$('btnTopupDevnet').addEventListener('click', () => {
|
$('btnTopupDevnet').addEventListener('click', () => {
|
||||||
try {
|
try {
|
||||||
@ -44,6 +102,21 @@ $('btnTopupDevnet').addEventListener('click', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('btnRefreshBalance').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
$('btnRefreshBalance').disabled = true;
|
||||||
|
await refreshDeviceBalance({
|
||||||
|
endpoint: $('endpoint').value,
|
||||||
|
deviceAddress: $('devPub').value,
|
||||||
|
targetNode: $('deviceBalance'),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setStatus($('status'), error?.message || String(error), 'error');
|
||||||
|
} finally {
|
||||||
|
$('btnRefreshBalance').disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$('btnGen').addEventListener('click', async () => {
|
$('btnGen').addEventListener('click', async () => {
|
||||||
clearGenMessage($('genMsg'));
|
clearGenMessage($('genMsg'));
|
||||||
clearStatus($('status'));
|
clearStatus($('status'));
|
||||||
@ -54,7 +127,15 @@ $('btnGen').addEventListener('click', async () => {
|
|||||||
const { masterSecret32, keyBundle } = await deriveKeyBundleFromPassword({ login, password });
|
const { masterSecret32, keyBundle } = await deriveKeyBundleFromPassword({ login, password });
|
||||||
fillKeyFields(fieldMap, keyBundle, masterSecret32);
|
fillKeyFields(fieldMap, keyBundle, masterSecret32);
|
||||||
updateSolAddress(fieldMap);
|
updateSolAddress(fieldMap);
|
||||||
setGenMessage($('genMsg'), 'Ключи и master secret сгенерированы из логина и пароля.', 'ok');
|
$('deviceBalance').textContent = 'Теперь можно проверить баланс device-аккаунта.';
|
||||||
|
const keysOk = renderComparisonStatus();
|
||||||
|
setGenMessage(
|
||||||
|
$('genMsg'),
|
||||||
|
keysOk
|
||||||
|
? 'Ключи и master secret сгенерированы из логина и пароля. Device-ключ оплатит транзакцию.'
|
||||||
|
: 'Ключи сгенерированы, но не совпали с уже загруженной PDA. Скорее всего, пароль неверный.',
|
||||||
|
keysOk ? 'ok' : 'err',
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setGenMessage($('genMsg'), error?.message || String(error), 'err');
|
setGenMessage($('genMsg'), error?.message || String(error), 'err');
|
||||||
} finally {
|
} finally {
|
||||||
@ -69,6 +150,7 @@ $('btnLoad').addEventListener('click', async () => {
|
|||||||
currentPda = null;
|
currentPda = null;
|
||||||
$('pdaInfo').style.display = 'none';
|
$('pdaInfo').style.display = 'none';
|
||||||
$('updateForm').style.display = 'none';
|
$('updateForm').style.display = 'none';
|
||||||
|
resetExpectedKeysUi();
|
||||||
try {
|
try {
|
||||||
const login = validateLoginOrThrow($('login').value);
|
const login = validateLoginOrThrow($('login').value);
|
||||||
const endpoint = String($('endpoint').value || '').trim();
|
const endpoint = String($('endpoint').value || '').trim();
|
||||||
@ -93,6 +175,7 @@ $('btnLoad').addEventListener('click', async () => {
|
|||||||
$('syncServers').value = parsed.syncServers.join('\n');
|
$('syncServers').value = parsed.syncServers.join('\n');
|
||||||
$('pdaInfo').style.display = 'block';
|
$('pdaInfo').style.display = 'block';
|
||||||
$('updateForm').style.display = 'block';
|
$('updateForm').style.display = 'block';
|
||||||
|
renderExpectedKeys(parsed);
|
||||||
setStatus($('status'), 'PDA загружена. Можно менять адрес или sync_servers.', 'success');
|
setStatus($('status'), 'PDA загружена. Можно менять адрес или sync_servers.', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus($('status'), error?.message || String(error), 'error');
|
setStatus($('status'), error?.message || String(error), 'error');
|
||||||
@ -121,6 +204,9 @@ $('btnUpdate').addEventListener('click', async () => {
|
|||||||
$('devPub').value = normalized.devPubB58;
|
$('devPub').value = normalized.devPubB58;
|
||||||
$('devPriv').value = normalized.devPrivB58;
|
$('devPriv').value = normalized.devPrivB58;
|
||||||
updateSolAddress(fieldMap);
|
updateSolAddress(fieldMap);
|
||||||
|
if (!renderComparisonStatus()) {
|
||||||
|
throw new Error('Ключи не совпадают с загруженной PDA. Проверьте пароль или введённые ключи.');
|
||||||
|
}
|
||||||
|
|
||||||
setStatus($('status'), 'Отправка update_user_pda в Solana...', 'info');
|
setStatus($('status'), 'Отправка update_user_pda в Solana...', 'info');
|
||||||
const result = await updateServerOnSolana({
|
const result = await updateServerOnSolana({
|
||||||
|
|||||||
@ -30,6 +30,15 @@
|
|||||||
.sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; }
|
.sol-ht { font-size:11px; color:var(--text-muted); margin-top:4px; }
|
||||||
.muted { font-size:12px; color:var(--text-muted); margin-bottom:14px; line-height:1.6; }
|
.muted { font-size:12px; color:var(--text-muted); margin-bottom:14px; line-height:1.6; }
|
||||||
.sol-topup-btn { margin-top:10px; width:100%; }
|
.sol-topup-btn { margin-top:10px; width:100%; }
|
||||||
|
.sol-balance-btn { margin-top:10px; width:100%; }
|
||||||
|
.sol-balance { font-size:11px; color:var(--text-muted); margin-top:8px; word-break:break-all; }
|
||||||
|
.expected-card { margin-top:14px; background:#111520; border:1px solid #243147; border-radius:var(--radius); padding:12px 14px; }
|
||||||
|
.expected-ttl { font-size:12px; font-weight:600; color:#9dc4ff; margin-bottom:8px; }
|
||||||
|
.expected-row { margin-bottom:8px; }
|
||||||
|
.expected-row:last-child { margin-bottom:0; }
|
||||||
|
.expected-lbl { font-size:11px; color:var(--text-muted); margin-bottom:4px; }
|
||||||
|
.expected-val { font-family:monospace; font-size:11px; word-break:break-all; }
|
||||||
|
.gen-msg.warn { display:block; background:#2f2614; border:1px solid #5f4b22; color:#ffd37a; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -128,8 +137,27 @@
|
|||||||
<div class="sol-adr" id="solAdr"></div>
|
<div class="sol-adr" id="solAdr"></div>
|
||||||
<div class="sol-ht">Это Solana-адрес (base58) device-ключа. С него оплачивается транзакция.</div>
|
<div class="sol-ht">Это Solana-адрес (base58) device-ключа. С него оплачивается транзакция.</div>
|
||||||
<button class="btn-secondary sol-topup-btn" id="btnTopupDevnet" type="button">Открыть пополнение DEVNET</button>
|
<button class="btn-secondary sol-topup-btn" id="btnTopupDevnet" type="button">Открыть пополнение DEVNET</button>
|
||||||
|
<button class="btn-secondary sol-balance-btn" id="btnRefreshBalance" type="button">Показать / обновить баланс device</button>
|
||||||
|
<div class="sol-balance" id="deviceBalance">Баланс device ещё не запрашивался.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="expected-card" id="expectedKeysBox" style="display:none">
|
||||||
|
<div class="expected-ttl">Какие ключи ожидаются по уже загруженной PDA</div>
|
||||||
|
<div class="expected-row">
|
||||||
|
<div class="expected-lbl">Ожидаемый root public key</div>
|
||||||
|
<div class="expected-val" id="expectedRootPub"></div>
|
||||||
|
</div>
|
||||||
|
<div class="expected-row">
|
||||||
|
<div class="expected-lbl">Ожидаемый blockchain public key</div>
|
||||||
|
<div class="expected-val" id="expectedBchPub"></div>
|
||||||
|
</div>
|
||||||
|
<div class="expected-row">
|
||||||
|
<div class="expected-lbl">Ожидаемый device public key</div>
|
||||||
|
<div class="expected-val" id="expectedDevPub"></div>
|
||||||
|
</div>
|
||||||
|
<div class="gen-msg" id="expectedKeysStatus"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
|
|||||||
@ -32,6 +32,12 @@
|
|||||||
|
|
||||||
Один логин соответствует одной `user_pda`.
|
Один логин соответствует одной `user_pda`.
|
||||||
|
|
||||||
|
## 2.1. Кто оплачивает create/update PDA
|
||||||
|
|
||||||
|
- Инструкции `create_user_pda` и `update_user_pda` оплачиваются с `device_key`.
|
||||||
|
- `root_key` используется для подписи unsigned части записи через Ed25519 instruction и не является fee payer.
|
||||||
|
- Для server PDA это правило то же самое: пополнять SOL нужно на адрес `device_key`.
|
||||||
|
|
||||||
## 3. Общие правила кодирования
|
## 3. Общие правила кодирования
|
||||||
|
|
||||||
- Числа кодируются в Little Endian.
|
- Числа кодируются в Little Endian.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user