import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { Ed25519Program, PublicKey, SYSVAR_INSTRUCTIONS_PUBKEY, SystemProgram, Transaction, } from "@solana/web3.js"; import { createHash } from "crypto"; import { expect } from "chai"; import { Shine } from "../target/types/shine"; const MAGIC = Buffer.from("SHiNE", "utf8"); const FORMAT_MAJOR = 1; const FORMAT_MINOR = 0; const RESERVED = Buffer.from([0, 0, 0, 0, 0]); const ZERO_HASH = Buffer.alloc(32, 0); const KEY_STATUS_CREATED = 0; const LIMIT_STEP = 10_000n; const START_BONUS_LIMIT = 100_000n; const FEE_RECEIVER = new PublicKey("9vXFoN9ngfN1gpqQ3HT5n3y9Wp2r7HnSQckirgwVwWwb"); const USERS_ECONOMY_CONFIG_SEED = "shine_users_v1_economy_config"; type MutableFields = { blockchainKey: PublicKey; deviceKey: PublicKey; chainNumber: number; isServer: boolean; serverKey: PublicKey; serverAddress: string; connectionServers: string[]; trustedCount: number; }; type UnsignedRecord = { createdAtMs: bigint; updatedAtMs: bigint; version: number; prevHash: Buffer; login: string; rootKeyStatus: number; rootKey: PublicKey; blockchainKeyStatus: number; blockchainKey: PublicKey; deviceKeyStatus: number; deviceKey: PublicKey; chainNumber: number; balance: bigint; isServer: boolean; serverKey: PublicKey; serverAddress: string; connectionServers: string[]; trustedCount: number; }; function u16le(v: number): Buffer { const b = Buffer.alloc(2); b.writeUInt16LE(v, 0); return b; } function u32le(v: number): Buffer { const b = Buffer.alloc(4); b.writeUInt32LE(v, 0); return b; } function u64le(v: bigint): Buffer { const b = Buffer.alloc(8); b.writeBigUInt64LE(v, 0); return b; } function serializeUnsignedRecord(r: UnsignedRecord): Buffer { const loginBytes = Buffer.from(r.login, "utf8"); const serverAddressBytes = Buffer.from(r.serverAddress, "utf8"); const out: Buffer[] = []; out.push(MAGIC); out.push(Buffer.from([FORMAT_MAJOR])); out.push(Buffer.from([FORMAT_MINOR])); out.push(Buffer.alloc(2, 0)); // record_len placeholder out.push(u64le(r.createdAtMs)); out.push(u64le(r.updatedAtMs)); out.push(u32le(r.version)); out.push(r.prevHash); out.push(Buffer.from([loginBytes.length])); out.push(loginBytes); out.push(Buffer.from([r.rootKeyStatus])); out.push(r.rootKey.toBuffer()); out.push(Buffer.from([r.blockchainKeyStatus])); out.push(r.blockchainKey.toBuffer()); out.push(Buffer.from([r.deviceKeyStatus])); out.push(r.deviceKey.toBuffer()); out.push(u16le(r.chainNumber)); out.push(u64le(r.balance)); out.push(Buffer.from([r.isServer ? 1 : 0])); if (r.isServer) { out.push(r.serverKey.toBuffer()); out.push(Buffer.from([serverAddressBytes.length])); out.push(serverAddressBytes); } out.push(Buffer.from([r.connectionServers.length])); for (const s of r.connectionServers) { const sb = Buffer.from(s, "utf8"); out.push(Buffer.from([sb.length])); out.push(sb); } out.push(Buffer.from([r.trustedCount])); out.push(RESERVED); const unsigned = Buffer.concat(out); const recordLen = unsigned.length + 64; unsigned.writeUInt16LE(recordLen, 7); return unsigned; } function sha256(buf: Buffer): Buffer { return createHash("sha256").update(buf).digest(); } function extractSigFromEdIx(ixData: Buffer): Buffer { const signatureOffset = ixData.readUInt16LE(2); return ixData.subarray(signatureOffset, signatureOffset + 64); } describe("shine_users e2e", () => { anchor.setProvider(anchor.AnchorProvider.env()); const provider = anchor.getProvider() as anchor.AnchorProvider; const program = anchor.workspace.shine as Program; it("registers user and updates balance/server data", async () => { const login = `u${Date.now().toString().slice(-10)}`; const [userPda] = PublicKey.findProgramAddressSync( [Buffer.from("login="), Buffer.from(login, "utf8")], program.programId ); const [usersEconomyConfigPda] = PublicKey.findProgramAddressSync( [Buffer.from(USERS_ECONOMY_CONFIG_SEED, "utf8")], program.programId ); const economyAi = await provider.connection.getAccountInfo(usersEconomyConfigPda); if (!economyAi) { await program.methods .initUsersEconomyConfig() .accounts({ signer: provider.wallet.publicKey, usersEconomyConfigPda, systemProgram: SystemProgram.programId, }) .rpc(); } const root = anchor.web3.Keypair.generate(); const blockchainKey = anchor.web3.Keypair.generate().publicKey; const deviceKey = anchor.web3.Keypair.generate().publicKey; const serverKey1 = anchor.web3.Keypair.generate().publicKey; const serverKey2 = anchor.web3.Keypair.generate().publicKey; const createdAtMs = BigInt(Date.now()); const additionalLimitCreate = 20_000n; expect(additionalLimitCreate % LIMIT_STEP).eq(0n); const createRecord: UnsignedRecord = { createdAtMs, updatedAtMs: createdAtMs, version: 0, prevHash: ZERO_HASH, login, rootKeyStatus: KEY_STATUS_CREATED, rootKey: root.publicKey, blockchainKeyStatus: KEY_STATUS_CREATED, blockchainKey, deviceKeyStatus: KEY_STATUS_CREATED, deviceKey, chainNumber: 1, balance: START_BONUS_LIMIT + additionalLimitCreate, isServer: true, serverKey: serverKey1, serverAddress: "https://srv-1.local", connectionServers: ["srv_login_1"], trustedCount: 0, }; const createUnsigned = serializeUnsignedRecord(createRecord); const createHash = sha256(createUnsigned); const createEdIx = Ed25519Program.createInstructionWithPrivateKey({ privateKey: root.secretKey, message: createHash, }); const createSig = extractSigFromEdIx(Buffer.from(createEdIx.data)); const createIx = await program.methods .createUserPda({ login, rootKey: root.publicKey, createdAtMs: new anchor.BN(createdAtMs.toString()), additionalLimit: new anchor.BN(additionalLimitCreate.toString()), fields: { blockchainKey, deviceKey, chainNumber: 1, isServer: true, serverKey: serverKey1, serverAddress: "https://srv-1.local", connectionServers: ["srv_login_1"], trustedCount: 0, }, signature: createSig, }) .accounts({ signer: provider.wallet.publicKey, userPda, systemProgram: SystemProgram.programId, feeReceiver: FEE_RECEIVER, instructions: SYSVAR_INSTRUCTIONS_PUBKEY, usersEconomyConfigPda, }) .instruction(); await provider.sendAndConfirm(new Transaction().add(createEdIx, createIx), []); const createAcc = await provider.connection.getAccountInfo(userPda); expect(createAcc).not.eq(null); expect(createAcc!.owner.toBase58()).eq(program.programId.toBase58()); const additionalLimitUpdate = 30_000n; expect(additionalLimitUpdate % LIMIT_STEP).eq(0n); const updateRecord: UnsignedRecord = { createdAtMs, updatedAtMs: createdAtMs + 1_000n, version: 1, prevHash: sha256(createUnsigned), login, rootKeyStatus: KEY_STATUS_CREATED, rootKey: root.publicKey, blockchainKeyStatus: KEY_STATUS_CREATED, blockchainKey: anchor.web3.Keypair.generate().publicKey, deviceKeyStatus: KEY_STATUS_CREATED, deviceKey: anchor.web3.Keypair.generate().publicKey, chainNumber: 1, balance: START_BONUS_LIMIT + additionalLimitCreate + additionalLimitUpdate, isServer: true, serverKey: serverKey2, serverAddress: "https://srv-2.local", connectionServers: ["srv_login_2", "srv_login_3"], trustedCount: 0, }; const updateUnsigned = serializeUnsignedRecord(updateRecord); const updateHash = sha256(updateUnsigned); const updateEdIx = Ed25519Program.createInstructionWithPrivateKey({ privateKey: root.secretKey, message: updateHash, }); const updateSig = extractSigFromEdIx(Buffer.from(updateEdIx.data)); const updateIx = await program.methods .updateUserPda({ login, rootKey: root.publicKey, createdAtMs: new anchor.BN(createdAtMs.toString()), updatedAtMs: new anchor.BN((createdAtMs + 1_000n).toString()), version: 1, prevHash: sha256(createUnsigned), additionalLimit: new anchor.BN(additionalLimitUpdate.toString()), fields: { blockchainKey: updateRecord.blockchainKey, deviceKey: updateRecord.deviceKey, chainNumber: 1, isServer: true, serverKey: serverKey2, serverAddress: "https://srv-2.local", connectionServers: ["srv_login_2", "srv_login_3"], trustedCount: 0, }, signature: updateSig, }) .accounts({ signer: provider.wallet.publicKey, userPda, systemProgram: SystemProgram.programId, feeReceiver: FEE_RECEIVER, instructions: SYSVAR_INSTRUCTIONS_PUBKEY, usersEconomyConfigPda, }) .instruction(); await provider.sendAndConfirm(new Transaction().add(updateEdIx, updateIx), []); const updatedAcc = await provider.connection.getAccountInfo(userPda); expect(updatedAcc).not.eq(null); const data = updatedAcc!.data; expect(data.subarray(0, 5).toString("utf8")).eq("SHiNE"); expect(data[5]).eq(FORMAT_MAJOR); expect(data[6]).eq(FORMAT_MINOR); }); });