shine-solana/shine/tests/shine.ts

310 lines
9.4 KiB
TypeScript

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<Shine>;
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);
});
});