Добавить promo-регистрацию и оффлайн генератор промокодов

This commit is contained in:
AidarKC
2026-06-30 17:39:40 +04:00
parent b0887648f7
commit 0a416a2f5c
12 changed files with 1154 additions and 42 deletions
+80 -3
View File
@@ -1,4 +1,4 @@
import { base64ToBytes, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
import { base64ToBytes, bytesToBase58, importPkcs8Ed25519, sha256Bytes, signBytes } from './crypto-utils.js';
import { extractSeed32FromPkcs8B64 } from './client-key-utils.js';
import {
SHINE_LOGIN_GUARD_PROGRAM_ID,
@@ -12,10 +12,12 @@ const MAGIC = 'SHiNE';
const LAST_BLOCK_STATE_PREFIX = 'SHiNE_LAST_BLOCK';
const SHINE_PAYMENTS_INFLOW_VAULT_SEED = 'shine_payments_inflow_vault';
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
const SHINE_USERS_PROMO_SELLER_PDA_SEED_PREFIX = 'promo_seller=';
const LIMIT_STEP = 10_000n;
const BLOCKCHAIN_TYPE_MAIN_USER = 1;
const ED25519_PROGRAM_ID = 'Ed25519SigVerify111111111111111111111111111';
const SYSVAR_INSTRUCTIONS_ID = 'Sysvar1nstructions1111111111111111111111111';
const PROMO_SIGN_PREFIX = 'shine_promo_v1:';
const BLOCK_TYPE_RECOVERY_KEY = 0;
const BLOCK_TYPE_ROOT_KEY = 1;
@@ -214,6 +216,10 @@ function serializeCreateUserPdaArgs(args) {
}
buf.push(Number(args.trustedCount || 0) & 0xff);
for (const x of args.rootSignature64) buf.push(x);
const promoCode = String(args.promoCode || '').trim();
if (promoCode) {
pushStrU8(buf, promoCode);
}
return new Uint8Array(buf);
}
@@ -363,6 +369,37 @@ export function buildLastBlockStateBytes(login, blockchainName, lastBlockNumber
return new Uint8Array(buf);
}
export function buildPromoMessageBytes(login) {
return new TextEncoder().encode(`${PROMO_SIGN_PREFIX}${normalizeLogin(login)}`);
}
export function buildPromoCodeString(sellerLogin, signatureBytes) {
const cleanSellerLogin = normalizeLogin(sellerLogin);
if (!cleanSellerLogin) {
throw new Error('Не указан логин продавца');
}
const sig = signatureBytes instanceof Uint8Array ? signatureBytes : new Uint8Array(signatureBytes || []);
if (sig.length !== 64) {
throw new Error('Подпись промокода должна быть ровно 64 байта');
}
return `1${cleanSellerLogin}-${bytesToBase58(sig)}`;
}
export async function findPromoSellerPda({ sellerLogin }) {
const cleanSellerLogin = normalizeLogin(sellerLogin);
if (!cleanSellerLogin) throw new Error('Не указан логин продавца');
const solana = await loadSolanaLib();
const usersProgram = new solana.PublicKey(SHINE_USERS_PROGRAM_ID);
const [promoSellerPda] = solana.PublicKey.findProgramAddressSync(
[new TextEncoder().encode(SHINE_USERS_PROMO_SELLER_PDA_SEED_PREFIX), new TextEncoder().encode(cleanSellerLogin)],
usersProgram,
);
return {
sellerLogin: cleanSellerLogin,
promoSellerPda: promoSellerPda.toBase58(),
};
}
export function parseShineUserPda(dataBytes) {
const bytes = dataBytes instanceof Uint8Array ? dataBytes : new Uint8Array(dataBytes || []);
const reader = makeReader(bytes);
@@ -770,6 +807,7 @@ async function createShineUserPdaOnSolana({
serverAddress = '',
syncServers = [],
accessServers = [],
promoCode = '',
}) {
const ctx = await buildCreateContext({ login, keyBundle, solanaEndpoint });
const ecoAccount = await ctx.connection.getAccountInfo(ctx.economyConfigPda);
@@ -778,6 +816,7 @@ async function createShineUserPdaOnSolana({
}
const cleanLogin = ctx.cleanLogin;
const cleanPromoCode = String(promoCode || '').trim();
const blockchainName = `${cleanLogin}-001`;
const zeroHash32 = new Uint8Array(32);
const createdAtMs = BigInt(Date.now());
@@ -845,8 +884,41 @@ async function createShineUserPdaOnSolana({
sessions: [],
trustedCount: 0,
rootSignature64: rootSig64,
promoCode: cleanPromoCode,
});
let promoSellerPda = null;
let promoEd25519Ix = null;
if (cleanPromoCode) {
const dashPos = cleanPromoCode.indexOf('-');
if (!cleanPromoCode.startsWith('1') || dashPos <= 1 || dashPos === cleanPromoCode.length - 1) {
throw new Error('Некорректный формат промокода');
}
const sellerLogin = normalizeLogin(cleanPromoCode.slice(1, dashPos));
const signatureBase58 = cleanPromoCode.slice(dashPos + 1);
const [promoSellerAddress] = ctx.solana.PublicKey.findProgramAddressSync(
[new TextEncoder().encode(SHINE_USERS_PROMO_SELLER_PDA_SEED_PREFIX), new TextEncoder().encode(sellerLogin)],
ctx.usersProgram,
);
promoSellerPda = promoSellerAddress;
const promoMessage = buildPromoMessageBytes(cleanLogin);
const signatureBytes = ctx.solana.bs58.decode(signatureBase58);
if (signatureBytes.length !== 64) {
throw new Error('Подпись промокода должна быть 64 байта в Base58');
}
const sellerAccount = await ctx.connection.getAccountInfo(promoSellerAddress, 'confirmed');
if (!sellerAccount?.data || sellerAccount.data.length < 42) {
throw new Error('Promo seller PDA не найдена или повреждена');
}
const signerPubkey = new Uint8Array(sellerAccount.data.slice(10, 42));
promoEd25519Ix = new ctx.solana.TransactionInstruction({
programId: ctx.ed25519Program,
keys: [],
data: buildEd25519IxData(signatureBytes, signerPubkey, promoMessage),
});
}
const ed25519RootIx = new ctx.solana.TransactionInstruction({
programId: ctx.ed25519Program,
keys: [],
@@ -867,15 +939,19 @@ async function createShineUserPdaOnSolana({
{ pubkey: ctx.sysvarInstructions, isSigner: false, isWritable: false },
{ pubkey: ctx.economyConfigPda, isSigner: false, isWritable: false },
{ pubkey: ctx.loginGuardProgram, isSigner: false, isWritable: false },
...(promoSellerPda ? [{ pubkey: promoSellerPda, isSigner: false, isWritable: true }] : []),
],
data: ixData,
});
let signature;
try {
const tx = new ctx.solana.Transaction();
if (promoEd25519Ix) tx.add(promoEd25519Ix);
tx.add(ed25519RootIx, ed25519BchIx, createIx);
signature = await ctx.solana.sendAndConfirmTransaction(
ctx.connection,
new ctx.solana.Transaction().add(ed25519RootIx, ed25519BchIx, createIx),
tx,
[ctx.clientKeypair],
{ commitment: 'confirmed' },
);
@@ -891,13 +967,14 @@ async function createShineUserPdaOnSolana({
};
}
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers = [] }) {
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers = [], promoCode = '' }) {
return createShineUserPdaOnSolana({
login,
keyBundle,
solanaEndpoint,
isServer: false,
accessServers: Array.isArray(accessServers) ? accessServers : [],
promoCode,
});
}
@@ -139,6 +139,6 @@ export async function checkLoginExistsOnSolana({ login, solanaEndpoint }) {
return { exists: !!ai, userPda: userPda.toBase58() };
}
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers }) {
return registerUserOnSolanaShared({ login, keyBundle, solanaEndpoint, accessServers });
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers, promoCode = '' }) {
return registerUserOnSolanaShared({ login, keyBundle, solanaEndpoint, accessServers, promoCode });
}