SHiNE-server/shine-UI/js/services/shine-user-pda-service.js

972 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { base64ToBytes, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from './device-key-utils.js';
import {
SHINE_LOGIN_GUARD_PROGRAM_ID,
SHINE_PAYMENTS_PROGRAM_ID,
SHINE_USERS_ECONOMY_CONFIG_SEED,
SHINE_USERS_PROGRAM_ID,
} from '../solana-programs.js';
const MAGIC = 'SHiNE';
const LAST_BLOCK_STATE_PREFIX = 'SHiNE_LAST_BLOCK';
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = 'shine_payments_inflow_vault';
const LIMIT_STEP = 10_000n;
const BLOCKCHAIN_TYPE_MAIN_USER = 1;
const CREATE_USER_PDA_DISCRIMINATOR = new Uint8Array([139, 157, 13, 41, 142, 174, 226, 214]);
const UPDATE_USER_PDA_DISCRIMINATOR = new Uint8Array([42, 133, 114, 232, 38, 245, 167, 234]);
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
const BLOCK_TYPE_ROOT_KEY = 1;
const BLOCK_TYPE_DEVICE_KEY = 2;
const BLOCK_TYPE_BLOCKCHAIN_REGISTRY = 3;
const BLOCK_TYPE_SERVER_PROFILE = 30;
const BLOCK_TYPE_ACCESS_SERVERS = 40;
const BLOCK_TYPE_TRUSTED_STATE = 50;
let solanaLibPromise = null;
function loadSolanaLib() {
if (!solanaLibPromise) solanaLibPromise = import('https://esm.sh/@solana/web3.js@1.98.4?bundle');
return solanaLibPromise;
}
function normalizeLogin(login) {
return String(login || '').trim().toLowerCase();
}
function pushU32LE(buf, value) {
const n = Number(value) >>> 0;
buf.push(n & 0xff, (n >>> 8) & 0xff, (n >>> 16) & 0xff, (n >>> 24) & 0xff);
}
function pushU64LE(buf, value) {
const b = BigInt(value);
const lo = Number(b & 0xffffffffn) >>> 0;
const hi = Number((b >> 32n) & 0xffffffffn) >>> 0;
pushU32LE(buf, lo);
pushU32LE(buf, hi);
}
function pushStrU8(buf, value) {
const bytes = new TextEncoder().encode(String(value || ''));
if (bytes.length > 255) throw new Error('Слишком длинная строка для формата U8');
buf.push(bytes.length);
for (const x of bytes) buf.push(x);
}
function pushStrU32(buf, value) {
const bytes = new TextEncoder().encode(String(value || ''));
pushU32LE(buf, bytes.length);
for (const x of bytes) buf.push(x);
}
function pushVecU8(buf, bytes) {
const data = bytes || new Uint8Array();
pushU32LE(buf, data.length);
for (const x of data) buf.push(x);
}
function pushVecStrU32(buf, values) {
const arr = Array.isArray(values) ? values : [];
pushU32LE(buf, arr.length);
for (const value of arr) pushStrU32(buf, value);
}
function makeReader(bytes) {
let offset = 0;
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const ensure = (len) => {
if (offset + len > bytes.length) throw new Error('Повреждённый формат PDA');
};
const readU8 = () => {
ensure(1);
const value = view.getUint8(offset);
offset += 1;
return value;
};
const readU16 = () => {
ensure(2);
const value = view.getUint16(offset, true);
offset += 2;
return value;
};
const readU32 = () => {
ensure(4);
const value = view.getUint32(offset, true);
offset += 4;
return value;
};
const readU64 = () => {
ensure(8);
const value = view.getBigUint64(offset, true);
offset += 8;
return value;
};
const readBytes = (len) => {
ensure(len);
const out = bytes.slice(offset, offset + len);
offset += len;
return out;
};
const readStrU8 = () => {
const len = readU8();
return new TextDecoder().decode(readBytes(len));
};
return {
readU8,
readU16,
readU32,
readU64,
readBytes,
readStrU8,
get offset() {
return offset;
},
};
}
function parseUsersEconomyConfig(dataBytes) {
const view = new DataView(dataBytes.buffer, dataBytes.byteOffset, dataBytes.byteLength);
if (dataBytes.byteLength < 25) throw new Error('Некорректный economy config');
return {
version: view.getUint8(0),
registrationFeeLamports: view.getBigUint64(1, true),
lamportsPerLimitStep: view.getBigUint64(9, true),
startBonusLimit: view.getBigUint64(17, true),
};
}
function buildEd25519IxData(sig64, pubkey32, msgHash32) {
const sigOff = 16;
const pkOff = sigOff + 64;
const msgOff = pkOff + 32;
const data = new Uint8Array(msgOff + 32);
const view = new DataView(data.buffer);
data[0] = 1;
data[1] = 0;
view.setUint16(2, sigOff, true);
view.setUint16(4, 0xffff, true);
view.setUint16(6, pkOff, true);
view.setUint16(8, 0xffff, true);
view.setUint16(10, msgOff, true);
view.setUint16(12, 32, true);
view.setUint16(14, 0xffff, true);
data.set(sig64, sigOff);
data.set(pubkey32, pkOff);
data.set(msgHash32, msgOff);
return data;
}
function serializeCreateUserPdaArgs(args) {
const buf = [];
for (const x of CREATE_USER_PDA_DISCRIMINATOR) buf.push(x);
pushStrU32(buf, args.login);
for (const x of args.rootKey32) buf.push(x);
pushU64LE(buf, args.createdAtMs);
pushU64LE(buf, 0n);
for (const x of args.deviceKey32) buf.push(x);
for (const x of args.blockchainPublicKey32) buf.push(x);
pushStrU32(buf, args.blockchainName);
pushU64LE(buf, args.usedBytes);
pushU32LE(buf, args.lastBlockNumber);
pushVecU8(buf, args.lastBlockHash32);
pushVecU8(buf, args.lastBlockSignature64);
pushStrU32(buf, args.arweaveTxId);
buf.push(args.isServer ? 1 : 0);
buf.push(Number(args.addressFormatType || 0) & 0xff);
buf.push(Number(args.addressFormatVersion || 0) & 0xff);
pushStrU32(buf, args.serverAddress);
pushVecStrU32(buf, args.syncServers);
pushVecStrU32(buf, args.accessServers);
buf.push(Number(args.trustedCount || 0) & 0xff);
pushVecU8(buf, args.rootSignature64);
return new Uint8Array(buf);
}
function serializeUpdateUserPdaArgs(args) {
const buf = [];
for (const x of UPDATE_USER_PDA_DISCRIMINATOR) buf.push(x);
pushStrU32(buf, args.login);
for (const x of args.rootKey32) buf.push(x);
pushU64LE(buf, args.createdAtMs);
pushU64LE(buf, args.updatedAtMs);
pushU32LE(buf, args.version);
pushVecU8(buf, args.prevHash32);
pushU64LE(buf, args.additionalLimitBytes);
for (const x of args.deviceKey32) buf.push(x);
for (const x of args.blockchainPublicKey32) buf.push(x);
pushStrU32(buf, args.blockchainName);
pushU64LE(buf, args.usedBytes);
pushU32LE(buf, args.lastBlockNumber);
pushVecU8(buf, args.lastBlockHash32);
pushVecU8(buf, args.lastBlockSignature64);
pushStrU32(buf, args.arweaveTxId);
buf.push(args.isServer ? 1 : 0);
buf.push(Number(args.addressFormatType || 0) & 0xff);
buf.push(Number(args.addressFormatVersion || 0) & 0xff);
pushStrU32(buf, args.serverAddress);
pushVecStrU32(buf, args.syncServers);
pushVecStrU32(buf, args.accessServers);
buf.push(Number(args.trustedCount || 0) & 0xff);
pushVecU8(buf, args.rootSignature64);
return new Uint8Array(buf);
}
function createBlockchainState({
blockchainName,
blockchainPublicKey,
paidLimitBytes,
usedBytes,
lastBlockNumber,
lastBlockHash,
lastBlockSignature,
arweaveTxId,
}) {
return {
blockchainType: BLOCKCHAIN_TYPE_MAIN_USER,
blockchainName,
blockchainPublicKey,
paidLimitBytes,
usedBytes,
lastBlockNumber,
lastBlockHash,
lastBlockSignature,
arweaveTxId,
};
}
function createPdaState({
login,
createdAtMs,
updatedAtMs,
recordNumber,
prevRecordHash,
rootKey,
deviceKey,
blockchain,
isServer,
addressFormatType,
addressFormatVersion,
serverAddress,
syncServers,
accessServers,
trustedCount,
}) {
const serverProfile = isServer ? {
addressFormatType: Number(addressFormatType || 0),
addressFormatVersion: Number(addressFormatVersion || 0),
serverAddress: String(serverAddress || ''),
syncServers: Array.isArray(syncServers) ? [...syncServers] : [],
} : null;
return {
createdAtMs,
updatedAtMs,
recordNumber,
prevRecordHash,
login,
rootKey,
deviceKey,
blockchain,
isServer: Boolean(isServer),
serverProfile,
serverData: serverProfile,
addressFormatType: serverProfile?.addressFormatType ?? 0,
addressFormatVersion: serverProfile?.addressFormatVersion ?? 0,
serverAddress: serverProfile?.serverAddress ?? '',
syncServers: serverProfile?.syncServers ? [...serverProfile.syncServers] : [],
accessServers: Array.isArray(accessServers) ? [...accessServers] : [],
trustedCount: Number(trustedCount || 0) & 0xff,
};
}
function encodeOptionalArweave(buf, arweaveTxId) {
const value = String(arweaveTxId || '').trim();
if (value) {
buf.push(1);
pushStrU8(buf, value);
} else {
buf.push(0);
}
}
export function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber = 0, lastBlockHash32 = new Uint8Array(32), usedBytes = 0n) {
const buf = [];
for (const x of new TextEncoder().encode(LAST_BLOCK_STATE_PREFIX)) buf.push(x);
pushStrU8(buf, login);
pushStrU8(buf, blockchainName);
pushU32LE(buf, lastBlockNumber);
for (const x of lastBlockHash32) buf.push(x);
pushU64LE(buf, usedBytes);
return new Uint8Array(buf);
}
export function parseShineUserPda(dataBytes) {
const bytes = dataBytes instanceof Uint8Array ? dataBytes : new Uint8Array(dataBytes || []);
const reader = makeReader(bytes);
const magic = new TextDecoder().decode(reader.readBytes(5));
if (magic !== MAGIC) throw new Error('Некорректный формат PDA');
reader.readU8();
reader.readU8();
const recordLen = reader.readU16();
if (recordLen < 9 + 64 || recordLen > bytes.length) throw new Error('Некорректный record_len');
const createdAtMs = reader.readU64();
const updatedAtMs = reader.readU64();
const recordNumber = reader.readU32();
const prevRecordHash = reader.readBytes(32);
const login = reader.readStrU8();
const blocksCount = reader.readU8();
let rootKey = null;
let deviceKey = null;
let blockchain = null;
let isServer = false;
let addressFormatType = 0;
let addressFormatVersion = 0;
let serverAddress = '';
let syncServers = [];
let accessServers = [];
let trustedCount = 0;
for (let i = 0; i < blocksCount; i += 1) {
const blockType = reader.readU8();
reader.readU8();
if (blockType === BLOCK_TYPE_ROOT_KEY) {
rootKey = reader.readBytes(32);
continue;
}
if (blockType === BLOCK_TYPE_DEVICE_KEY) {
deviceKey = reader.readBytes(32);
continue;
}
if (blockType === BLOCK_TYPE_BLOCKCHAIN_REGISTRY) {
const count = reader.readU8();
for (let j = 0; j < count; j += 1) {
const blockchainType = reader.readU8();
const blockchainName = reader.readStrU8();
const blockchainPublicKey = reader.readBytes(32);
const paidLimitBytes = reader.readU64();
const usedBytes = reader.readU64();
const lastBlockNumber = reader.readU32();
const lastBlockHash = reader.readBytes(32);
const lastBlockSignature = reader.readBytes(64);
const arweavePresent = reader.readU8();
const arweaveTxId = arweavePresent === 1 ? reader.readStrU8() : '';
if (!blockchain) {
blockchain = {
blockchainType,
blockchainName,
blockchainPublicKey,
paidLimitBytes,
usedBytes,
lastBlockNumber,
lastBlockHash,
lastBlockSignature,
arweaveTxId,
};
}
}
continue;
}
if (blockType === BLOCK_TYPE_SERVER_PROFILE) {
isServer = reader.readU8() === 1;
if (isServer) {
addressFormatType = reader.readU8();
addressFormatVersion = reader.readU8();
serverAddress = reader.readStrU8();
const syncCount = reader.readU8();
syncServers = [];
for (let j = 0; j < syncCount; j += 1) syncServers.push(reader.readStrU8());
}
continue;
}
if (blockType === BLOCK_TYPE_ACCESS_SERVERS) {
const accessCount = reader.readU8();
accessServers = [];
for (let j = 0; j < accessCount; j += 1) accessServers.push(reader.readStrU8());
continue;
}
if (blockType === BLOCK_TYPE_TRUSTED_STATE) {
trustedCount = reader.readU8();
continue;
}
throw new Error(`Неизвестный блок PDA: ${blockType}`);
}
if (!rootKey || !deviceKey || !blockchain) {
throw new Error('В PDA отсутствуют обязательные блоки');
}
const signature = bytes.slice(reader.offset, reader.offset + 64);
const unsignedBytes = bytes.slice(0, recordLen - 64);
const state = createPdaState({
login,
createdAtMs,
updatedAtMs,
recordNumber,
prevRecordHash,
rootKey,
deviceKey,
blockchain,
isServer,
addressFormatType,
addressFormatVersion,
serverAddress,
syncServers,
accessServers,
trustedCount,
});
return {
...state,
recordLen,
unsignedBytes,
signature,
};
}
export function serializeUnsignedRecordFromState(stateLike) {
const state = createPdaState({
login: stateLike.login,
createdAtMs: stateLike.createdAtMs,
updatedAtMs: stateLike.updatedAtMs,
recordNumber: stateLike.recordNumber,
prevRecordHash: stateLike.prevRecordHash,
rootKey: stateLike.rootKey,
deviceKey: stateLike.deviceKey,
blockchain: stateLike.blockchain,
isServer: stateLike.isServer,
addressFormatType: stateLike.addressFormatType ?? stateLike.serverProfile?.addressFormatType,
addressFormatVersion: stateLike.addressFormatVersion ?? stateLike.serverProfile?.addressFormatVersion,
serverAddress: stateLike.serverAddress ?? stateLike.serverProfile?.serverAddress,
syncServers: stateLike.syncServers ?? stateLike.serverProfile?.syncServers,
accessServers: stateLike.accessServers,
trustedCount: stateLike.trustedCount,
});
const buf = [0x53, 0x48, 0x69, 0x4e, 0x45, 1, 0, 0, 0];
pushU64LE(buf, state.createdAtMs);
pushU64LE(buf, state.updatedAtMs);
pushU32LE(buf, state.recordNumber);
for (const x of state.prevRecordHash) buf.push(x);
pushStrU8(buf, state.login);
buf.push(state.isServer ? 6 : 5);
buf.push(BLOCK_TYPE_ROOT_KEY, 0);
for (const x of state.rootKey) buf.push(x);
buf.push(BLOCK_TYPE_DEVICE_KEY, 0);
for (const x of state.deviceKey) buf.push(x);
buf.push(BLOCK_TYPE_BLOCKCHAIN_REGISTRY, 0, 1, state.blockchain.blockchainType);
pushStrU8(buf, state.blockchain.blockchainName);
for (const x of state.blockchain.blockchainPublicKey) buf.push(x);
pushU64LE(buf, state.blockchain.paidLimitBytes);
pushU64LE(buf, state.blockchain.usedBytes);
pushU32LE(buf, state.blockchain.lastBlockNumber);
for (const x of state.blockchain.lastBlockHash) buf.push(x);
for (const x of state.blockchain.lastBlockSignature) buf.push(x);
encodeOptionalArweave(buf, state.blockchain.arweaveTxId);
if (state.isServer) {
buf.push(BLOCK_TYPE_SERVER_PROFILE, 0, 1);
buf.push(state.addressFormatType & 0xff);
buf.push(state.addressFormatVersion & 0xff);
pushStrU8(buf, state.serverAddress);
buf.push(state.syncServers.length & 0xff);
for (const loginValue of state.syncServers) pushStrU8(buf, loginValue);
}
buf.push(BLOCK_TYPE_ACCESS_SERVERS, 0, state.accessServers.length & 0xff);
for (const loginValue of state.accessServers) pushStrU8(buf, loginValue);
buf.push(BLOCK_TYPE_TRUSTED_STATE, 0, state.trustedCount & 0xff);
const recordLen = buf.length + 64;
buf[7] = recordLen & 0xff;
buf[8] = (recordLen >>> 8) & 0xff;
return new Uint8Array(buf);
}
export async function getShineUsersEconomyConfig({ solanaEndpoint }) {
const endpoint = String(solanaEndpoint || '').trim();
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
const solana = await loadSolanaLib();
const connection = new solana.Connection(endpoint, 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const [economyPda] = solana.PublicKey.findProgramAddressSync(
[new TextEncoder().encode(SHINE_USERS_ECONOMY_CONFIG_SEED)],
usersProgram,
);
const accountInfo = await connection.getAccountInfo(economyPda, 'confirmed');
if (!accountInfo?.data) throw new Error('Economy config PDA не найден');
return { endpoint, economyPda: economyPda.toBase58(), ...parseUsersEconomyConfig(accountInfo.data) };
}
export async function readShineUserPda({ login, solanaEndpoint }) {
const cleanLogin = normalizeLogin(login);
const endpoint = String(solanaEndpoint || '').trim();
if (!cleanLogin) throw new Error('Не указан логин');
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
const solana = await loadSolanaLib();
const connection = new solana.Connection(endpoint, 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const enc = new TextEncoder();
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram);
const accountInfo = await connection.getAccountInfo(userPda, 'confirmed');
if (!accountInfo?.data) throw new Error(`PDA не найдена для логина «${cleanLogin}»`);
return {
...parseShineUserPda(accountInfo.data),
userPda: userPda.toBase58(),
pdaAddress: userPda.toBase58(),
endpoint,
};
}
export async function getShineBlockchainUsage({ login, solanaEndpoint }) {
const parsed = await readShineUserPda({ login, solanaEndpoint });
const bch = parsed.blockchain;
const leftBytes = bch.paidLimitBytes > bch.usedBytes ? (bch.paidLimitBytes - bch.usedBytes) : 0n;
return {
endpoint: parsed.endpoint,
userPda: parsed.userPda,
login: parsed.login,
recordNumber: parsed.recordNumber,
paidLimitBytes: bch.paidLimitBytes,
usedBytes: bch.usedBytes,
leftBytes,
lastBlockNumber: bch.lastBlockNumber,
lastBlockHashHex: Array.from(bch.lastBlockHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
};
}
function parseHex32(value) {
const clean = String(value || '').trim().toLowerCase();
if (!clean) return null;
if (!/^[0-9a-f]+$/.test(clean) || clean.length !== 64) {
throw new Error('last block hash должен быть 32 байта в hex');
}
const out = new Uint8Array(32);
for (let i = 0; i < 32; i += 1) {
out[i] = parseInt(clean.slice(i * 2, (i * 2) + 2), 16);
}
return out;
}
async function buildCreateContext({ login, keyBundle, solanaEndpoint }) {
const cleanLogin = normalizeLogin(login);
const endpoint = String(solanaEndpoint || '').trim();
if (!cleanLogin) throw new Error('Не указан логин');
if (!endpoint) throw new Error('Не указан Solana RPC endpoint');
const solana = await loadSolanaLib();
const connection = new solana.Connection(endpoint, 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
const loginGuardProgram = new solana.PublicKey(SHINE_LOGIN_GUARD_PROGRAM_ID);
const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
const enc = new TextEncoder();
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram);
const [economyConfigPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
const rootKey32 = base64ToBytes(keyBundle.rootPair.publicKeyB64);
const blockchainKey32 = base64ToBytes(keyBundle.blockchainPair.publicKeyB64);
const deviceKey32 = base64ToBytes(keyBundle.devicePair.publicKeyB64);
const rootPrivKey = await importPkcs8Ed25519(keyBundle.rootPair.privatePkcs8B64);
const bchPrivKey = await importPkcs8Ed25519(keyBundle.blockchainPair.privatePkcs8B64);
const deviceSeed32 = extractSeed32FromPkcs8B64(keyBundle.devicePair.privatePkcs8B64);
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
return {
cleanLogin,
endpoint,
solana,
connection,
usersProgram,
paymentsProgram,
loginGuardProgram,
ed25519Program,
sysvarInstructions,
userPda,
economyConfigPda,
inflowVault,
rootKey32,
blockchainKey32,
deviceKey32,
rootPrivKey,
bchPrivKey,
deviceKeypair,
};
}
async function createShineUserPdaOnSolana({
login,
keyBundle,
solanaEndpoint,
isServer = false,
addressFormatType = 0,
addressFormatVersion = 0,
serverAddress = '',
syncServers = [],
accessServers = [],
}) {
const ctx = await buildCreateContext({ login, keyBundle, solanaEndpoint });
const ecoAccount = await ctx.connection.getAccountInfo(ctx.economyConfigPda);
if (!ecoAccount?.data) {
throw new Error('Economy config не инициализирован. Запустите init_users_economy_config.');
}
const cleanLogin = ctx.cleanLogin;
const blockchainName = `${cleanLogin}-001`;
const zeroHash32 = new Uint8Array(32);
const createdAtMs = BigInt(Date.now());
const startBonusLimit = parseUsersEconomyConfig(ecoAccount.data).startBonusLimit;
const lastBlockStateBytes = buildLastBlockStateBytes(cleanLogin, blockchainName, 0, zeroHash32, 0n);
const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);
const lastBlockSig64 = await signBytes(ctx.bchPrivKey, lastBlockStateHash);
const initialState = createPdaState({
login: cleanLogin,
createdAtMs,
updatedAtMs: createdAtMs,
recordNumber: 0,
prevRecordHash: zeroHash32,
rootKey: ctx.rootKey32,
deviceKey: ctx.deviceKey32,
blockchain: createBlockchainState({
blockchainName,
blockchainPublicKey: ctx.blockchainKey32,
paidLimitBytes: startBonusLimit,
usedBytes: 0n,
lastBlockNumber: 0,
lastBlockHash: zeroHash32,
lastBlockSignature: lastBlockSig64,
arweaveTxId: '',
}),
isServer,
addressFormatType,
addressFormatVersion,
serverAddress,
syncServers,
accessServers,
trustedCount: 0,
});
const unsignedRecord = serializeUnsignedRecordFromState(initialState);
const unsignedHash = await sha256Bytes(unsignedRecord);
const rootSig64 = await signBytes(ctx.rootPrivKey, unsignedHash);
const ixData = serializeCreateUserPdaArgs({
login: cleanLogin,
rootKey32: ctx.rootKey32,
createdAtMs,
deviceKey32: ctx.deviceKey32,
blockchainPublicKey32: ctx.blockchainKey32,
blockchainName,
usedBytes: 0n,
lastBlockNumber: 0,
lastBlockHash32: zeroHash32,
lastBlockSignature64: lastBlockSig64,
arweaveTxId: '',
isServer,
addressFormatType: isServer ? addressFormatType : 0,
addressFormatVersion: isServer ? addressFormatVersion : 0,
serverAddress: isServer ? serverAddress : '',
syncServers: isServer ? syncServers : [],
accessServers,
trustedCount: 0,
rootSignature64: rootSig64,
});
const ed25519RootIx = new ctx.solana.TransactionInstruction({
programId: ctx.ed25519Program,
keys: [],
data: buildEd25519IxData(rootSig64, ctx.rootKey32, unsignedHash),
});
const ed25519BchIx = new ctx.solana.TransactionInstruction({
programId: ctx.ed25519Program,
keys: [],
data: buildEd25519IxData(lastBlockSig64, ctx.blockchainKey32, lastBlockStateHash),
});
const createIx = new ctx.solana.TransactionInstruction({
programId: ctx.usersProgram,
keys: [
{ pubkey: ctx.deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: ctx.userPda, isSigner: false, isWritable: true },
{ pubkey: ctx.solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: ctx.inflowVault, isSigner: false, isWritable: true },
{ pubkey: ctx.sysvarInstructions, isSigner: false, isWritable: false },
{ pubkey: ctx.economyConfigPda, isSigner: false, isWritable: false },
{ pubkey: ctx.loginGuardProgram, isSigner: false, isWritable: false },
],
data: ixData,
});
const signature = await ctx.solana.sendAndConfirmTransaction(
ctx.connection,
new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
[ctx.deviceKeypair],
{ commitment: 'confirmed' },
);
return {
signature,
userPda: ctx.userPda.toBase58(),
pdaAddress: ctx.userPda.toBase58(),
blockchainName,
};
}
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint }) {
return createShineUserPdaOnSolana({
login,
keyBundle,
solanaEndpoint,
isServer: false,
accessServers: ['shineup.me'],
});
}
export async function registerServerOnSolana({
login,
keyBundle,
serverAddress,
addressFormatType = 1,
addressFormatVersion = 0,
syncServers = [],
accessServers = [],
solanaEndpoint,
}) {
return createShineUserPdaOnSolana({
login,
keyBundle,
solanaEndpoint,
isServer: true,
addressFormatType,
addressFormatVersion,
serverAddress,
syncServers,
accessServers,
});
}
export async function updateShineUserPdaOnSolana({
login,
solanaEndpoint,
rootPrivatePkcs8B64,
devicePrivatePkcs8B64,
blockchainPrivatePkcs8B64,
additionalLimitBytes = 0n,
nextUsedBytes,
nextLastBlockNumber,
nextLastBlockHashHex,
serverProfile,
accessServers,
trustedCount,
}) {
const current = await readShineUserPda({ login, solanaEndpoint });
const cleanLogin = current.login;
const endpoint = current.endpoint;
const currentBch = current.blockchain;
const addLimit = BigInt(additionalLimitBytes || 0);
if (addLimit < 0n) throw new Error('Нельзя уменьшать лимит');
if (addLimit % LIMIT_STEP !== 0n) throw new Error(`Лимит можно увеличивать только шагом ${LIMIT_STEP}`);
const effectiveUsed = nextUsedBytes == null ? currentBch.usedBytes : BigInt(nextUsedBytes);
const effectiveLastNum = nextLastBlockNumber == null ? currentBch.lastBlockNumber : Number(nextLastBlockNumber);
const effectiveLastHash = parseHex32(nextLastBlockHashHex) || currentBch.lastBlockHash;
if (effectiveLastHash.length !== 32) throw new Error('last block hash должен быть 32 байта');
const rootPriv = await importPkcs8Ed25519(rootPrivatePkcs8B64);
const solana = await loadSolanaLib();
const connection = new solana.Connection(endpoint, 'confirmed');
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const paymentsProgram = new solana.PublicKey(SHINE_PAYMENTS_PROGRAM_ID);
const ed25519Program = new solana.PublicKey(ED25519_PROGRAM_ID);
const sysvarInstructions = new solana.PublicKey(SYSVAR_INSTRUCTIONS_ID);
const enc = new TextEncoder();
const [userPda] = solana.PublicKey.findProgramAddressSync([enc.encode('login='), enc.encode(cleanLogin)], usersProgram);
const [economyPda] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_USERS_ECONOMY_CONFIG_SEED)], usersProgram);
const [inflowVault] = solana.PublicKey.findProgramAddressSync([enc.encode(SHINE_PAYMENTS_INFLOW_VAULT_SEED)], paymentsProgram);
const deviceSeed32 = extractSeed32FromPkcs8B64(devicePrivatePkcs8B64);
const deviceKeypair = solana.Keypair.fromSeed(deviceSeed32);
const lastBlockStateBytes = buildLastBlockStateBytes(
cleanLogin,
currentBch.blockchainName,
effectiveLastNum,
effectiveLastHash,
effectiveUsed,
);
const lastBlockStateHash = await sha256Bytes(lastBlockStateBytes);
const blockchainStateWillChange = (
nextUsedBytes != null
|| nextLastBlockNumber != null
|| nextLastBlockHashHex != null
);
let lastBlockSig64 = currentBch.lastBlockSignature;
if (blockchainPrivatePkcs8B64) {
const bchPriv = await importPkcs8Ed25519(blockchainPrivatePkcs8B64);
lastBlockSig64 = await signBytes(bchPriv, lastBlockStateHash);
} else if (blockchainStateWillChange) {
throw new Error('Для изменения last block state нужен blockchain-ключ');
}
const updatedAtMs = BigInt(Date.now());
const newPaid = currentBch.paidLimitBytes + addLimit;
const newRecordNumber = current.recordNumber + 1;
const prevHash = await sha256Bytes(serializeUnsignedRecordFromState(current));
const nextServerProfile = serverProfile
? {
addressFormatType: Number(serverProfile.addressFormatType ?? current.addressFormatType ?? 0),
addressFormatVersion: Number(serverProfile.addressFormatVersion ?? current.addressFormatVersion ?? 0),
serverAddress: String(serverProfile.serverAddress ?? current.serverAddress ?? ''),
syncServers: Array.isArray(serverProfile.syncServers) ? [...serverProfile.syncServers] : [...current.syncServers],
}
: current.serverProfile;
const nextState = createPdaState({
login: cleanLogin,
createdAtMs: current.createdAtMs,
updatedAtMs,
recordNumber: newRecordNumber,
prevRecordHash: prevHash,
rootKey: current.rootKey,
deviceKey: current.deviceKey,
blockchain: createBlockchainState({
blockchainName: currentBch.blockchainName,
blockchainPublicKey: currentBch.blockchainPublicKey,
paidLimitBytes: newPaid,
usedBytes: effectiveUsed,
lastBlockNumber: effectiveLastNum,
lastBlockHash: effectiveLastHash,
lastBlockSignature: lastBlockSig64,
arweaveTxId: currentBch.arweaveTxId,
}),
isServer: Boolean(nextServerProfile),
addressFormatType: nextServerProfile?.addressFormatType ?? 0,
addressFormatVersion: nextServerProfile?.addressFormatVersion ?? 0,
serverAddress: nextServerProfile?.serverAddress ?? '',
syncServers: nextServerProfile?.syncServers ?? [],
accessServers: accessServers == null ? current.accessServers : accessServers,
trustedCount: trustedCount == null ? current.trustedCount : trustedCount,
});
const unsignedNext = serializeUnsignedRecordFromState(nextState);
const unsignedNextHash = await sha256Bytes(unsignedNext);
const rootSig64 = await signBytes(rootPriv, unsignedNextHash);
const ixData = serializeUpdateUserPdaArgs({
login: cleanLogin,
rootKey32: current.rootKey,
createdAtMs: current.createdAtMs,
updatedAtMs,
version: newRecordNumber,
prevHash32: prevHash,
additionalLimitBytes: addLimit,
deviceKey32: current.deviceKey,
blockchainPublicKey32: currentBch.blockchainPublicKey,
blockchainName: currentBch.blockchainName,
usedBytes: effectiveUsed,
lastBlockNumber: effectiveLastNum,
lastBlockHash32: effectiveLastHash,
lastBlockSignature64: lastBlockSig64,
arweaveTxId: currentBch.arweaveTxId,
isServer: nextState.isServer,
addressFormatType: nextState.addressFormatType,
addressFormatVersion: nextState.addressFormatVersion,
serverAddress: nextState.serverAddress,
syncServers: nextState.syncServers,
accessServers: nextState.accessServers,
trustedCount: nextState.trustedCount,
rootSignature64: rootSig64,
});
const edIxRoot = new solana.TransactionInstruction({
programId: ed25519Program,
keys: [],
data: buildEd25519IxData(rootSig64, current.rootKey, unsignedNextHash),
});
const edIxBch = new solana.TransactionInstruction({
programId: ed25519Program,
keys: [],
data: buildEd25519IxData(lastBlockSig64, currentBch.blockchainPublicKey, lastBlockStateHash),
});
const updateIx = new solana.TransactionInstruction({
programId: usersProgram,
keys: [
{ pubkey: deviceKeypair.publicKey, isSigner: true, isWritable: true },
{ pubkey: userPda, isSigner: false, isWritable: true },
{ pubkey: solana.SystemProgram.programId, isSigner: false, isWritable: false },
{ pubkey: inflowVault, isSigner: false, isWritable: true },
{ pubkey: sysvarInstructions, isSigner: false, isWritable: false },
{ pubkey: economyPda, isSigner: false, isWritable: false },
],
data: ixData,
});
const computeIx = solana.ComputeBudgetProgram.setComputeUnitLimit({ units: 800_000 });
const heapIx = solana.ComputeBudgetProgram.requestHeapFrame({ bytes: 262_144 });
const signature = await solana.sendAndConfirmTransaction(
connection,
new solana.Transaction().add(computeIx, heapIx, edIxRoot, edIxBch, updateIx),
[deviceKeypair],
{ commitment: 'confirmed' },
);
return {
signature,
userPda: userPda.toBase58(),
pdaAddress: userPda.toBase58(),
paidLimitBytes: newPaid,
usedBytes: effectiveUsed,
leftBytes: newPaid > effectiveUsed ? (newPaid - effectiveUsed) : 0n,
lastBlockNumber: effectiveLastNum,
lastBlockHashHex: Array.from(effectiveLastHash).map((x) => x.toString(16).padStart(2, '0')).join(''),
};
}
export async function updateServerOnSolana({
login,
keyBundle,
serverAddress,
addressFormatType,
addressFormatVersion,
syncServers,
solanaEndpoint,
}) {
return updateShineUserPdaOnSolana({
login,
solanaEndpoint,
rootPrivatePkcs8B64: keyBundle.rootPair.privatePkcs8B64,
devicePrivatePkcs8B64: keyBundle.devicePair.privatePkcs8B64,
serverProfile: {
addressFormatType,
addressFormatVersion,
serverAddress,
syncServers,
},
});
}
export function calcLimitTopupPriceLamports(additionalLimitBytes, lamportsPerLimitStep) {
const add = BigInt(additionalLimitBytes || 0);
const pricePerStep = BigInt(lamportsPerLimitStep || 0);
if (add < 0n) throw new Error('Некорректный размер увеличения лимита');
if (add % LIMIT_STEP !== 0n) throw new Error(`Увеличение лимита должно быть кратно ${LIMIT_STEP} байт`);
return (add / LIMIT_STEP) * pricePerStep;
}
export function getLimitStepBytes() {
return LIMIT_STEP;
}