SHA256
Добавить promo-регистрацию и оффлайн генератор промокодов
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user