From a99195b4c8b391c8b51d5ab7d0bf182fea204a5f Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:49:29 +0700 Subject: [PATCH 01/49] fix(crypto): harden Antelope key and signature handling --- packages/crypto/src/index.ts | 265 ++++++++++++++++++++++++++++------- 1 file changed, 214 insertions(+), 51 deletions(-) diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index 2902e04..c163baf 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -4,23 +4,62 @@ * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI * SPDX-License-Identifier: MIT */ -import { secp256k1 } from "@noble/curves/secp256k1.js"; import { p256 } from "@noble/curves/nist.js"; -import { sha256 } from "@noble/hashes/sha2.js"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; import { ripemd160 } from "@noble/hashes/legacy.js"; +import { sha256 } from "@noble/hashes/sha2.js"; export type KeyType = "K1" | "R1"; + const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; const encoder = new TextEncoder(); +function curveFor(type: KeyType) { + return type === "K1" ? secp256k1 : p256; +} + function assertBytes(value: Uint8Array, length: number, label: string): void { - if (!(value instanceof Uint8Array) || value.length !== length) throw new TypeError(`${label} must be ${length} bytes`); + if (!(value instanceof Uint8Array) || value.length !== length) { + throw new TypeError(`${label} must be ${length} bytes`); + } +} + +function equalBytes(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let index = 0; index < a.length; index += 1) { + diff |= a[index]! ^ b[index]!; + } + return diff === 0; +} + +function uint32Bytes(value: number): Uint8Array { + const bytes = new Uint8Array(4); + new DataView(bytes.buffer).setUint32(0, value >>> 0, true); + return bytes; +} + +function isCanonicalCompact(compact: Uint8Array): boolean { + if (compact.length !== 64) return false; + const r0 = compact[0]!; + const r1 = compact[1]!; + const s0 = compact[32]!; + const s1 = compact[33]!; + return ( + (r0 & 0x80) === 0 && + !(r0 === 0 && (r1 & 0x80) === 0) && + (s0 & 0x80) === 0 && + !(s0 === 0 && (s1 & 0x80) === 0) + ); } export function concatBytes(...parts: Uint8Array[]): Uint8Array { const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0)); let offset = 0; - for (const part of parts) { out.set(part, offset); offset += part.length; } + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } return out; } @@ -30,39 +69,50 @@ export function bytesToHex(bytes: Uint8Array): string { export function hexToBytes(hex: string): Uint8Array { const normalized = hex.startsWith("0x") ? hex.slice(2) : hex; - if (!/^[0-9a-f]*$/i.test(normalized) || normalized.length % 2 !== 0) throw new TypeError("Invalid hexadecimal value"); + if (!/^[0-9a-f]*$/i.test(normalized) || normalized.length % 2 !== 0) { + throw new TypeError("Invalid hexadecimal value"); + } return Uint8Array.from(normalized.match(/.{2}/g)?.map((pair) => Number.parseInt(pair, 16)) ?? []); } -export function sha256Digest(data: Uint8Array): Uint8Array { return sha256(data); } - -function equalBytes(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) return false; - let diff = 0; - for (let i = 0; i < a.length; i += 1) diff |= a[i]! ^ b[i]!; - return diff === 0; +export function sha256Digest(data: Uint8Array): Uint8Array { + return sha256(data); } function base58Encode(bytes: Uint8Array): string { + if (bytes.length === 0) return ""; let value = 0n; for (const byte of bytes) value = (value << 8n) | BigInt(byte); let out = ""; - while (value > 0n) { const mod = Number(value % 58n); out = BASE58[mod]! + out; value /= 58n; } - for (const byte of bytes) { if (byte !== 0) break; out = "1" + out; } - return out || "1"; + while (value > 0n) { + const mod = Number(value % 58n); + out = BASE58[mod]! + out; + value /= 58n; + } + for (const byte of bytes) { + if (byte !== 0) break; + out = `1${out}`; + } + return out; } function base58Decode(value: string): Uint8Array { if (!value) throw new TypeError("Base58 value cannot be empty"); - let num = 0n; + let number = 0n; for (const char of value) { const index = BASE58.indexOf(char); if (index < 0) throw new TypeError(`Invalid base58 character: ${char}`); - num = num * 58n + BigInt(index); + number = number * 58n + BigInt(index); } const bytes: number[] = []; - while (num > 0n) { bytes.unshift(Number(num & 0xffn)); num >>= 8n; } - for (const char of value) { if (char !== "1") break; bytes.unshift(0); } + while (number > 0n) { + bytes.unshift(Number(number & 0xffn)); + number >>= 8n; + } + for (const char of value) { + if (char !== "1") break; + bytes.unshift(0); + } return Uint8Array.from(bytes); } @@ -79,96 +129,209 @@ function decodeModern(value: string, type: KeyType): Uint8Array { const decoded = base58Decode(value); if (decoded.length < 5) throw new TypeError("Invalid Antelope encoded value"); const data = decoded.slice(0, -4); - if (!equalBytes(decoded.slice(-4), ripemdChecksum(data, type))) throw new TypeError("Antelope checksum mismatch"); + const checksum = decoded.slice(-4); + if (!equalBytes(checksum, ripemdChecksum(data, type))) { + throw new TypeError("Antelope checksum mismatch"); + } return data; } -function curveFor(type: KeyType) { return type === "K1" ? secp256k1 : p256; } - export class PublicKey { readonly type: KeyType; readonly #data: Uint8Array; - private constructor(type: KeyType, data: Uint8Array) { assertBytes(data, 33, "Public key"); this.type = type; this.#data = data.slice(); } - static fromBytes(type: KeyType, data: Uint8Array): PublicKey { return new PublicKey(type, data); } + + private constructor(type: KeyType, data: Uint8Array) { + assertBytes(data, 33, "Public key"); + if (!curveFor(type).utils.isValidPublicKey(data)) { + throw new TypeError(`Invalid ${type} public key`); + } + this.type = type; + this.#data = data.slice(); + } + + static fromBytes(type: KeyType, data: Uint8Array): PublicKey { + return new PublicKey(type, data); + } + static fromString(value: string): PublicKey { const modern = /^PUB_(K1|R1)_(.+)$/.exec(value); - if (modern) return new PublicKey(modern[1] as KeyType, decodeModern(modern[2]!, modern[1] as KeyType)); + if (modern) { + const type = modern[1] as KeyType; + return new PublicKey(type, decodeModern(modern[2]!, type)); + } if (value.startsWith("EOS")) { const decoded = base58Decode(value.slice(3)); + if (decoded.length !== 37) throw new TypeError("Invalid legacy EOS public key length"); const data = decoded.slice(0, -4); - if (!equalBytes(decoded.slice(-4), ripemdChecksum(data))) throw new TypeError("Legacy EOS public-key checksum mismatch"); + if (!equalBytes(decoded.slice(-4), ripemdChecksum(data))) { + throw new TypeError("Legacy EOS public-key checksum mismatch"); + } return new PublicKey("K1", data); } throw new TypeError("Unsupported Antelope public-key format"); } - toBytes(): Uint8Array { return this.#data.slice(); } - toString(): string { return `PUB_${this.type}_${encodeModern(this.#data, this.type)}`; } + + toBytes(): Uint8Array { + return this.#data.slice(); + } + + toString(): string { + return `PUB_${this.type}_${encodeModern(this.#data, this.type)}`; + } + toLegacyString(): string { if (this.type !== "K1") throw new TypeError("Legacy EOS public keys only support K1"); return `EOS${base58Encode(concatBytes(this.#data, ripemdChecksum(this.#data)))}`; } - equals(other: PublicKey): boolean { return this.type === other.type && equalBytes(this.#data, other.#data); } - verifyDigest(digest: Uint8Array, signature: Signature): boolean { return signature.verifyDigest(digest, this); } + + equals(other: PublicKey): boolean { + return this.type === other.type && equalBytes(this.#data, other.#data); + } + + verifyDigest(digest: Uint8Array, signature: Signature): boolean { + return signature.verifyDigest(digest, this); + } } export class Signature { readonly type: KeyType; readonly #data: Uint8Array; + private constructor(type: KeyType, data: Uint8Array) { assertBytes(data, 65, "Signature"); - if (data[0]! < 31 || data[0]! > 34) throw new TypeError("Invalid Antelope recovery header"); - this.type = type; this.#data = data.slice(); + if (data[0]! < 31 || data[0]! > 34) { + throw new TypeError("Invalid Antelope recovery header"); + } + this.type = type; + this.#data = data.slice(); + } + + static fromBytes(type: KeyType, data: Uint8Array): Signature { + return new Signature(type, data); } - static fromBytes(type: KeyType, data: Uint8Array): Signature { return new Signature(type, data); } + static fromString(value: string): Signature { const match = /^SIG_(K1|R1)_(.+)$/.exec(value); if (!match) throw new TypeError("Unsupported Antelope signature format"); - return new Signature(match[1] as KeyType, decodeModern(match[2]!, match[1] as KeyType)); + const type = match[1] as KeyType; + return new Signature(type, decodeModern(match[2]!, type)); + } + + toBytes(): Uint8Array { + return this.#data.slice(); } - toBytes(): Uint8Array { return this.#data.slice(); } - toString(): string { return `SIG_${this.type}_${encodeModern(this.#data, this.type)}`; } + + toString(): string { + return `SIG_${this.type}_${encodeModern(this.#data, this.type)}`; + } + + isCanonical(): boolean { + return this.type !== "K1" || isCanonicalCompact(this.#data.slice(1)); + } + verifyDigest(digest: Uint8Array, publicKey: PublicKey): boolean { assertBytes(digest, 32, "Digest"); if (publicKey.type !== this.type) return false; - return curveFor(this.type).verify(this.#data.slice(1), digest, publicKey.toBytes(), { prehash: false, lowS: true, format: "compact" }); + try { + return curveFor(this.type).verify(this.#data.slice(1), digest, publicKey.toBytes(), { + prehash: false, + lowS: true, + format: "compact", + }); + } catch { + return false; + } } + recoverDigest(digest: Uint8Array): PublicKey { assertBytes(digest, 32, "Digest"); - const recovered = this.#data.slice(); recovered[0] = recovered[0]! - 31; - return PublicKey.fromBytes(this.type, curveFor(this.type).recoverPublicKey(recovered, digest, { prehash: false })); + const recovered = this.#data.slice(); + recovered[0] = recovered[0]! - 31; + return PublicKey.fromBytes( + this.type, + curveFor(this.type).recoverPublicKey(recovered, digest, { prehash: false }), + ); } } export class PrivateKey { readonly type: KeyType; readonly #data: Uint8Array; - private constructor(type: KeyType, data: Uint8Array) { assertBytes(data, 32, "Private key"); this.type = type; this.#data = data.slice(); } - static fromBytes(type: KeyType, data: Uint8Array): PrivateKey { return new PrivateKey(type, data); } - static generate(type: KeyType = "K1"): PrivateKey { return new PrivateKey(type, curveFor(type).keygen().secretKey); } + + private constructor(type: KeyType, data: Uint8Array) { + assertBytes(data, 32, "Private key"); + if (!curveFor(type).utils.isValidSecretKey(data)) { + throw new TypeError(`Invalid ${type} private key`); + } + this.type = type; + this.#data = data.slice(); + } + + static fromBytes(type: KeyType, data: Uint8Array): PrivateKey { + return new PrivateKey(type, data); + } + + static generate(type: KeyType = "K1"): PrivateKey { + return new PrivateKey(type, curveFor(type).keygen().secretKey); + } + static fromString(value: string): PrivateKey { const modern = /^PVT_(K1|R1)_(.+)$/.exec(value); - if (modern) return new PrivateKey(modern[1] as KeyType, decodeModern(modern[2]!, modern[1] as KeyType)); + if (modern) { + const type = modern[1] as KeyType; + return new PrivateKey(type, decodeModern(modern[2]!, type)); + } + const decoded = base58Decode(value); if (decoded.length === 37 && decoded[0] === 0x80) { const payload = decoded.slice(0, -4); - const check = sha256(sha256(payload)).slice(0, 4); - if (!equalBytes(decoded.slice(-4), check)) throw new TypeError("Legacy WIF checksum mismatch"); + const checksum = sha256(sha256(payload)).slice(0, 4); + if (!equalBytes(decoded.slice(-4), checksum)) { + throw new TypeError("Legacy WIF checksum mismatch"); + } return new PrivateKey("K1", payload.slice(1)); } throw new TypeError("Unsupported Antelope private-key format"); } - toBytes(): Uint8Array { return this.#data.slice(); } - toString(): string { return `PVT_${this.type}_${encodeModern(this.#data, this.type)}`; } + + toBytes(): Uint8Array { + return this.#data.slice(); + } + + toString(): string { + return `PVT_${this.type}_${encodeModern(this.#data, this.type)}`; + } + toWif(): string { if (this.type !== "K1") throw new TypeError("Legacy WIF only supports K1"); const payload = concatBytes(Uint8Array.of(0x80), this.#data); return base58Encode(concatBytes(payload, sha256(sha256(payload)).slice(0, 4))); } - toPublicKey(): PublicKey { return PublicKey.fromBytes(this.type, curveFor(this.type).getPublicKey(this.#data, true)); } + + toPublicKey(): PublicKey { + return PublicKey.fromBytes(this.type, curveFor(this.type).getPublicKey(this.#data, true)); + } + signDigest(digest: Uint8Array): Signature { assertBytes(digest, 32, "Digest"); - const recovered = curveFor(this.type).sign(digest, this.#data, { prehash: false, lowS: true, format: "recovered" }); - const antelope = recovered.slice(); antelope[0] = antelope[0]! + 31; - return Signature.fromBytes(this.type, antelope); + const curve = curveFor(this.type); + + for (let attempt = 0; attempt < 1024; attempt += 1) { + const options = { + prehash: false, + lowS: true, + format: "recovered" as const, + ...(attempt === 0 + ? {} + : { extraEntropy: sha256(concatBytes(digest, uint32Bytes(attempt))) }), + }; + const recovered = curve.sign(digest, this.#data, options); + if (this.type === "K1" && !isCanonicalCompact(recovered.slice(1))) continue; + const antelope = recovered.slice(); + antelope[0] = antelope[0]! + 31; + return Signature.fromBytes(this.type, antelope); + } + + throw new Error("Unable to produce a canonical Antelope signature"); } } From 2d874430dd8e96a964a4cda6abd0255cf8ca9420 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:50:41 +0700 Subject: [PATCH 02/49] fix(abi): complete core Antelope ABI primitives --- packages/abi/src/index.ts | 815 +++++++++++++++++++++++++++++++++----- 1 file changed, 717 insertions(+), 98 deletions(-) diff --git a/packages/abi/src/index.ts b/packages/abi/src/index.ts index 19b543f..a51b3f8 100644 --- a/packages/abi/src/index.ts +++ b/packages/abi/src/index.ts @@ -9,84 +9,328 @@ import { PublicKey, Signature } from "@windstack/crypto"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const NAME_CHARS = ".12345abcdefghijklmnopqrstuvwxyz"; +const BLOCK_TIMESTAMP_EPOCH_MS = Date.UTC(2000, 0, 1); export type AbiField = { name: string; type: string }; -export type AbiStruct = { name: string; base: string; fields: AbiField[] }; +export type AbiStruct = { name: string; base?: string; fields: AbiField[] }; export type AbiTypeDef = { new_type_name: string; type: string }; export type AbiAction = { name: string; type: string; ricardian_contract?: string }; export type AbiVariant = { name: string; types: string[] }; +export type AbiTable = { + name: string; + index_type: string; + key_names?: string[]; + key_types?: string[]; + type: string; +}; export type Abi = { version: string; types?: AbiTypeDef[]; structs?: AbiStruct[]; actions?: AbiAction[]; - tables?: Array<{ name: string; index_type: string; key_names?: string[]; key_types?: string[]; type: string }>; + tables?: AbiTable[]; variants?: AbiVariant[]; + [key: string]: unknown; }; -export function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); } +function assertInteger(value: number, min: number, max: number, label: string): number { + if (!Number.isInteger(value) || value < min || value > max) { + throw new RangeError(`${label} must be an integer between ${min} and ${max}`); + } + return value; +} + +function assertBigIntRange(value: bigint, bits: number, signed: boolean, label: string): bigint { + const width = BigInt(bits); + const min = signed ? -(1n << (width - 1n)) : 0n; + const max = signed ? (1n << (width - 1n)) - 1n : (1n << width) - 1n; + if (value < min || value > max) { + throw new RangeError(`${label} is outside the ${signed ? "signed" : "unsigned"} ${bits}-bit range`); + } + return value; +} + +function toBigInt(value: unknown, label: string): bigint { + try { + return BigInt(value as string | number | bigint); + } catch { + throw new TypeError(`${label} expects an integer-compatible value`); + } +} + +function toNumber(value: unknown, label: string): number { + const number = Number(value); + if (!Number.isFinite(number)) throw new TypeError(`${label} expects a finite number`); + return number; +} + +function assertHexBytes(value: unknown, bytes: number, label: string): Uint8Array { + const data = typeof value === "string" ? hexToBytes(value) : value; + if (!(data instanceof Uint8Array) || data.length !== bytes) { + throw new TypeError(`${label} must be exactly ${bytes} bytes`); + } + return data; +} + +export function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + export function hexToBytes(hex: string): Uint8Array { const value = hex.startsWith("0x") ? hex.slice(2) : hex; - if (!/^[0-9a-f]*$/i.test(value) || value.length % 2) throw new TypeError("Invalid hex value"); + if (!/^[0-9a-f]*$/i.test(value) || value.length % 2 !== 0) { + throw new TypeError("Invalid hexadecimal value"); + } return Uint8Array.from(value.match(/.{2}/g)?.map((pair) => Number.parseInt(pair, 16)) ?? []); } export function nameToBigInt(name: string): bigint { - if (name.length > 13) throw new RangeError("Antelope name cannot exceed 13 characters"); + if (typeof name !== "string" || name.length > 13) { + throw new RangeError("Antelope name cannot exceed 13 characters"); + } let value = 0n; - for (let i = 0; i < 13; i += 1) { - const char = i < name.length ? NAME_CHARS.indexOf(name[i]!) : 0; - if (char < 0) throw new TypeError(`Invalid Antelope name character: ${name[i]}`); - if (i === 12) { - if (char > 15) throw new TypeError("The 13th Antelope name character must be .12345abcdefghij"); + for (let index = 0; index < 13; index += 1) { + const char = index < name.length ? NAME_CHARS.indexOf(name[index]!) : 0; + if (char < 0) throw new TypeError(`Invalid Antelope name character: ${name[index]}`); + if (index === 12) { + if (char > 15) { + throw new TypeError("The 13th Antelope name character must be .12345abcdefghij"); + } value |= BigInt(char); } else { - value |= BigInt(char) << BigInt(64 - 5 * (i + 1)); + value |= BigInt(char) << BigInt(64 - 5 * (index + 1)); } } return value; } export function bigIntToName(value: bigint): string { - let tmp = BigInt.asUintN(64, value); + let current = BigInt.asUintN(64, value); const chars = new Array(13).fill("."); - for (let i = 0; i <= 12; i += 1) { - const index = i === 0 ? Number(tmp & 0x0fn) : Number(tmp & 0x1fn); - chars[12 - i] = NAME_CHARS[index]!; - tmp >>= i === 0 ? 4n : 5n; + for (let index = 0; index <= 12; index += 1) { + const charIndex = index === 0 ? Number(current & 0x0fn) : Number(current & 0x1fn); + chars[12 - index] = NAME_CHARS[charIndex]!; + current >>= index === 0 ? 4n : 5n; } return chars.join("").replace(/\.+$/, ""); } export class BinaryWriter { #bytes: number[] = []; - writeByte(value: number): void { this.#bytes.push(value & 0xff); } - writeUint16(value: number): void { this.writeByte(value); this.writeByte(value >>> 8); } - writeUint32(value: number): void { for (let i = 0; i < 4; i += 1) this.writeByte(value >>> (8 * i)); } - writeUint64(value: bigint): void { let v = BigInt.asUintN(64, value); for (let i = 0; i < 8; i += 1) { this.writeByte(Number(v & 0xffn)); v >>= 8n; } } - writeInt64(value: bigint): void { this.writeUint64(BigInt.asUintN(64, value)); } - writeVarUint(value: number): void { let v = value >>> 0; while (true) { if (v >>> 7) { this.writeByte(0x80 | (v & 0x7f)); v >>>= 7; } else { this.writeByte(v); break; } } } - writeBytes(value: Uint8Array): void { for (const byte of value) this.writeByte(byte); } - writeVarBytes(value: Uint8Array): void { this.writeVarUint(value.length); this.writeBytes(value); } - writeString(value: string): void { this.writeVarBytes(encoder.encode(value)); } - writeName(value: string): void { this.writeUint64(nameToBigInt(value)); } - toBytes(): Uint8Array { return Uint8Array.from(this.#bytes); } + + writeByte(value: number): void { + this.#bytes.push(assertInteger(value, 0, 0xff, "byte")); + } + + writeUint16(value: number): void { + const checked = assertInteger(value, 0, 0xffff, "uint16"); + this.writeByte(checked & 0xff); + this.writeByte((checked >>> 8) & 0xff); + } + + writeInt16(value: number): void { + const checked = assertInteger(value, -0x8000, 0x7fff, "int16"); + this.writeUint16(checked & 0xffff); + } + + writeUint32(value: number): void { + const checked = assertInteger(value, 0, 0xffffffff, "uint32"); + for (let index = 0; index < 4; index += 1) this.writeByte((checked >>> (8 * index)) & 0xff); + } + + writeInt32(value: number): void { + const checked = assertInteger(value, -0x80000000, 0x7fffffff, "int32"); + this.writeUint32(checked >>> 0); + } + + writeUint64(value: bigint): void { + let current = assertBigIntRange(value, 64, false, "uint64"); + for (let index = 0; index < 8; index += 1) { + this.writeByte(Number(current & 0xffn)); + current >>= 8n; + } + } + + writeInt64(value: bigint): void { + const checked = assertBigIntRange(value, 64, true, "int64"); + this.writeUint64(BigInt.asUintN(64, checked)); + } + + writeUint128(value: bigint): void { + let current = assertBigIntRange(value, 128, false, "uint128"); + for (let index = 0; index < 16; index += 1) { + this.writeByte(Number(current & 0xffn)); + current >>= 8n; + } + } + + writeInt128(value: bigint): void { + const checked = assertBigIntRange(value, 128, true, "int128"); + this.writeUint128(BigInt.asUintN(128, checked)); + } + + writeFloat32(value: number): void { + const bytes = new Uint8Array(4); + new DataView(bytes.buffer).setFloat32(0, value, true); + this.writeBytes(bytes); + } + + writeFloat64(value: number): void { + const bytes = new Uint8Array(8); + new DataView(bytes.buffer).setFloat64(0, value, true); + this.writeBytes(bytes); + } + + writeVarUint(value: number): void { + let current = assertInteger(value, 0, 0xffffffff, "varuint32") >>> 0; + while (true) { + if (current >>> 7) { + this.writeByte(0x80 | (current & 0x7f)); + current >>>= 7; + } else { + this.writeByte(current); + return; + } + } + } + + writeVarInt(value: number): void { + const checked = assertInteger(value, -0x80000000, 0x7fffffff, "varint32"); + this.writeVarUint(((checked << 1) ^ (checked >> 31)) >>> 0); + } + + writeBytes(value: Uint8Array): void { + if (!(value instanceof Uint8Array)) throw new TypeError("Expected Uint8Array"); + for (const byte of value) this.#bytes.push(byte); + } + + writeVarBytes(value: Uint8Array): void { + this.writeVarUint(value.length); + this.writeBytes(value); + } + + writeString(value: string): void { + this.writeVarBytes(encoder.encode(value)); + } + + writeName(value: string): void { + this.writeUint64(nameToBigInt(value)); + } + + toBytes(): Uint8Array { + return Uint8Array.from(this.#bytes); + } } export class BinaryReader { - readonly #bytes: Uint8Array; #offset = 0; - constructor(bytes: Uint8Array) { this.#bytes = bytes; } - get remaining(): number { return this.#bytes.length - this.#offset; } - readByte(): number { if (this.remaining < 1) throw new RangeError("Unexpected end of ABI data"); return this.#bytes[this.#offset++]!; } - readUint16(): number { return this.readByte() | (this.readByte() << 8); } - readUint32(): number { return (this.readByte() | (this.readByte() << 8) | (this.readByte() << 16) | (this.readByte() << 24)) >>> 0; } - readUint64(): bigint { let value = 0n; for (let i = 0n; i < 8n; i += 1n) value |= BigInt(this.readByte()) << (8n * i); return value; } - readInt64(): bigint { return BigInt.asIntN(64, this.readUint64()); } - readVarUint(): number { let value = 0; let bit = 0; while (true) { const byte = this.readByte(); value |= (byte & 0x7f) << bit; if (!(byte & 0x80)) return value >>> 0; bit += 7; if (bit > 35) throw new RangeError("varuint32 overflow"); } } - readBytes(length: number): Uint8Array { if (this.remaining < length) throw new RangeError("Unexpected end of ABI data"); const out = this.#bytes.slice(this.#offset, this.#offset + length); this.#offset += length; return out; } - readVarBytes(): Uint8Array { return this.readBytes(this.readVarUint()); } - readString(): string { return decoder.decode(this.readVarBytes()); } - readName(): string { return bigIntToName(this.readUint64()); } + readonly #bytes: Uint8Array; + #offset = 0; + + constructor(bytes: Uint8Array) { + if (!(bytes instanceof Uint8Array)) throw new TypeError("BinaryReader expects Uint8Array"); + this.#bytes = bytes; + } + + get remaining(): number { + return this.#bytes.length - this.#offset; + } + + readByte(): number { + if (this.remaining < 1) throw new RangeError("Unexpected end of ABI data"); + return this.#bytes[this.#offset++]!; + } + + readUint16(): number { + return this.readByte() | (this.readByte() << 8); + } + + readInt16(): number { + return (this.readUint16() << 16) >> 16; + } + + readUint32(): number { + return ( + this.readByte() | + (this.readByte() << 8) | + (this.readByte() << 16) | + (this.readByte() << 24) + ) >>> 0; + } + + readInt32(): number { + return this.readUint32() | 0; + } + + readUint64(): bigint { + let value = 0n; + for (let index = 0n; index < 8n; index += 1n) { + value |= BigInt(this.readByte()) << (8n * index); + } + return value; + } + + readInt64(): bigint { + return BigInt.asIntN(64, this.readUint64()); + } + + readUint128(): bigint { + let value = 0n; + for (let index = 0n; index < 16n; index += 1n) { + value |= BigInt(this.readByte()) << (8n * index); + } + return value; + } + + readInt128(): bigint { + return BigInt.asIntN(128, this.readUint128()); + } + + readFloat32(): number { + const bytes = this.readBytes(4); + return new DataView(bytes.buffer, bytes.byteOffset, 4).getFloat32(0, true); + } + + readFloat64(): number { + const bytes = this.readBytes(8); + return new DataView(bytes.buffer, bytes.byteOffset, 8).getFloat64(0, true); + } + + readVarUint(): number { + let value = 0; + let bit = 0; + while (true) { + const byte = this.readByte(); + if (bit === 28 && (byte & 0xf0) !== 0) throw new RangeError("varuint32 overflow"); + value |= (byte & 0x7f) << bit; + if ((byte & 0x80) === 0) return value >>> 0; + bit += 7; + if (bit > 28) throw new RangeError("varuint32 overflow"); + } + } + + readVarInt(): number { + const value = this.readVarUint(); + return (value >>> 1) ^ -(value & 1); + } + + readBytes(length: number): Uint8Array { + assertInteger(length, 0, this.remaining, "byte length"); + const out = this.#bytes.slice(this.#offset, this.#offset + length); + this.#offset += length; + return out; + } + + readVarBytes(): Uint8Array { + return this.readBytes(this.readVarUint()); + } + + readString(): string { + return decoder.decode(this.readVarBytes()); + } + + readName(): string { + return bigIntToName(this.readUint64()); + } } function parseAsset(value: string): { amount: bigint; precision: number; symbol: string } { @@ -94,90 +338,465 @@ function parseAsset(value: string): { amount: bigint; precision: number; symbol: if (!match) throw new TypeError(`Invalid asset: ${value}`); const fraction = match[3] ?? ""; const amount = BigInt(`${match[1]}${match[2]}${fraction}`); + assertBigIntRange(amount, 64, true, "asset amount"); return { amount, precision: fraction.length, symbol: match[4]! }; } -function symbolToBigInt(symbol: string, precision: number): bigint { let value = BigInt(precision); for (let i = 0; i < symbol.length; i += 1) value |= BigInt(symbol.charCodeAt(i)) << BigInt(8 * (i + 1)); return value; } -function symbolFromBigInt(raw: bigint): { precision: number; symbol: string } { const precision = Number(raw & 0xffn); let value = raw >> 8n; let symbol = ""; while (value > 0n) { symbol += String.fromCharCode(Number(value & 0xffn)); value >>= 8n; } return { precision, symbol }; } + +function validateSymbol(symbol: string): string { + if (!/^[A-Z]{1,7}$/.test(symbol)) throw new TypeError(`Invalid Antelope symbol: ${symbol}`); + return symbol; +} + +function symbolToBigInt(symbol: string, precision: number): bigint { + validateSymbol(symbol); + assertInteger(precision, 0, 18, "symbol precision"); + let value = BigInt(precision); + for (let index = 0; index < symbol.length; index += 1) { + value |= BigInt(symbol.charCodeAt(index)) << BigInt(8 * (index + 1)); + } + return value; +} + +function symbolFromBigInt(raw: bigint): { precision: number; symbol: string } { + const precision = Number(raw & 0xffn); + let value = raw >> 8n; + let symbol = ""; + while (value > 0n) { + const code = Number(value & 0xffn); + if (code === 0 || code < 65 || code > 90) throw new TypeError("Invalid encoded Antelope symbol"); + symbol += String.fromCharCode(code); + value >>= 8n; + } + validateSymbol(symbol); + return { precision, symbol }; +} + +function timePointToMicros(value: unknown): bigint { + if (typeof value === "bigint" || typeof value === "number") { + return assertBigIntRange(toBigInt(value, "time_point"), 64, true, "time_point"); + } + if (typeof value !== "string") throw new TypeError("time_point expects an ISO timestamp or microseconds"); + if (/^-?\d+$/.test(value)) return assertBigIntRange(BigInt(value), 64, true, "time_point"); + const match = /^(.+T\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})?$/.exec(value); + if (!match) throw new TypeError(`Invalid time_point: ${value}`); + const baseMs = Date.parse(`${match[1]}${match[3] ?? "Z"}`); + if (!Number.isFinite(baseMs)) throw new TypeError(`Invalid time_point: ${value}`); + const micros = BigInt((match[2] ?? "").padEnd(6, "0") || "0"); + return BigInt(baseMs) * 1000n + micros; +} + +function microsToTimePoint(value: bigint): string { + const milliseconds = value / 1000n; + const remainder = value % 1000n; + const date = new Date(Number(milliseconds)); + if (!Number.isFinite(date.getTime())) return value.toString(); + const iso = date.toISOString(); + const millisecondFraction = BigInt(iso.slice(20, 23)) * 1000n + remainder; + return iso.replace(/\.\d{3}Z$/, `.${millisecondFraction.toString().padStart(6, "0")}Z`); +} + +function blockTimestampToSlot(value: unknown): number { + if (typeof value === "number") return assertInteger(value, 0, 0xffffffff, "block_timestamp_type"); + if (typeof value !== "string") throw new TypeError("block_timestamp_type expects an ISO timestamp or slot number"); + if (/^\d+$/.test(value)) return assertInteger(Number(value), 0, 0xffffffff, "block_timestamp_type"); + const timestamp = Date.parse(/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`); + if (!Number.isFinite(timestamp) || timestamp < BLOCK_TIMESTAMP_EPOCH_MS) { + throw new TypeError(`Invalid block_timestamp_type: ${value}`); + } + return assertInteger(Math.floor((timestamp - BLOCK_TIMESTAMP_EPOCH_MS) / 500), 0, 0xffffffff, "block_timestamp_type"); +} + +function decodeAsset(reader: BinaryReader): string { + const amount = reader.readInt64(); + const { precision, symbol } = symbolFromBigInt(reader.readUint64()); + const negative = amount < 0n; + const digits = (negative ? -amount : amount).toString().padStart(precision + 1, "0"); + const formatted = precision + ? `${digits.slice(0, -precision)}.${digits.slice(-precision)}` + : digits; + return `${negative ? "-" : ""}${formatted} ${symbol}`; +} + +function encodeExtendedAsset(writer: BinaryWriter, value: unknown): void { + let quantity: unknown; + let contract: unknown; + if (typeof value === "string") { + const separator = value.lastIndexOf("@"); + if (separator < 1) throw new TypeError("extended_asset expects quantity@contract"); + quantity = value.slice(0, separator); + contract = value.slice(separator + 1); + } else if (typeof value === "object" && value !== null && !Array.isArray(value)) { + const record = value as Record; + quantity = record.quantity; + contract = record.contract; + } else { + throw new TypeError("extended_asset expects { quantity, contract } or quantity@contract"); + } + const asset = parseAsset(String(quantity)); + writer.writeInt64(asset.amount); + writer.writeUint64(symbolToBigInt(asset.symbol, asset.precision)); + writer.writeName(String(contract)); +} export class AbiSerializer { readonly abi: Abi; readonly #aliases = new Map(); readonly #structs = new Map(); readonly #variants = new Map(); + constructor(abi: Abi) { + if (!abi || typeof abi !== "object" || typeof abi.version !== "string") { + throw new TypeError("A valid Antelope ABI is required"); + } this.abi = abi; for (const type of abi.types ?? []) this.#aliases.set(type.new_type_name, type.type); for (const struct of abi.structs ?? []) this.#structs.set(struct.name, struct); for (const variant of abi.variants ?? []) this.#variants.set(variant.name, variant); } - resolveType(type: string): string { let current = type; const seen = new Set(); while (this.#aliases.has(current)) { if (seen.has(current)) throw new TypeError(`Cyclic ABI alias: ${type}`); seen.add(current); current = this.#aliases.get(current)!; } return current; } - encode(type: string, value: unknown): Uint8Array { const writer = new BinaryWriter(); this.#encodeType(writer, type, value); return writer.toBytes(); } - decode(type: string, bytes: Uint8Array): unknown { const reader = new BinaryReader(bytes); const value = this.#decodeType(reader, type); if (reader.remaining !== 0) throw new TypeError(`Unused ABI bytes: ${reader.remaining}`); return value; } - encodeAction(name: string, value: unknown): Uint8Array { const action = this.abi.actions?.find((item) => item.name === name); if (!action) throw new TypeError(`Unknown ABI action: ${name}`); return this.encode(action.type, value); } - decodeAction(name: string, bytes: Uint8Array): unknown { const action = this.abi.actions?.find((item) => item.name === name); if (!action) throw new TypeError(`Unknown ABI action: ${name}`); return this.decode(action.type, bytes); } + + resolveType(type: string): string { + let current = type; + const seen = new Set(); + while (this.#aliases.has(current)) { + if (seen.has(current)) throw new TypeError(`Cyclic ABI alias: ${type}`); + seen.add(current); + current = this.#aliases.get(current)!; + } + return current; + } + + getActionType(name: string): string { + const action = this.abi.actions?.find((item) => item.name === name); + if (!action) throw new TypeError(`Unknown ABI action: ${name}`); + return action.type; + } + + getTableType(name: string): string { + const table = this.abi.tables?.find((item) => item.name === name); + if (!table) throw new TypeError(`Unknown ABI table: ${name}`); + return table.type; + } + + encode(type: string, value: unknown): Uint8Array { + const writer = new BinaryWriter(); + this.#encodeType(writer, type, value); + return writer.toBytes(); + } + + decode(type: string, bytes: Uint8Array): unknown { + const reader = new BinaryReader(bytes); + const value = this.#decodeType(reader, type); + if (reader.remaining !== 0) throw new TypeError(`Unused ABI bytes: ${reader.remaining}`); + return value; + } + + encodeAction(name: string, value: unknown): Uint8Array { + return this.encode(this.getActionType(name), value); + } + + decodeAction(name: string, bytes: Uint8Array): unknown { + return this.decode(this.getActionType(name), bytes); + } + #encodeType(writer: BinaryWriter, rawType: string, value: unknown): void { - if (rawType.endsWith("[]")) { if (!Array.isArray(value)) throw new TypeError(`${rawType} expects an array`); writer.writeVarUint(value.length); for (const item of value) this.#encodeType(writer, rawType.slice(0, -2), item); return; } - if (rawType.endsWith("?")) { if (value === null || value === undefined) { writer.writeByte(0); return; } writer.writeByte(1); this.#encodeType(writer, rawType.slice(0, -1), value); return; } - if (rawType.endsWith("$")) { if (value !== null && value !== undefined) this.#encodeType(writer, rawType.slice(0, -1), value); return; } + if (rawType.endsWith("[]")) { + if (!Array.isArray(value)) throw new TypeError(`${rawType} expects an array`); + writer.writeVarUint(value.length); + for (const item of value) this.#encodeType(writer, rawType.slice(0, -2), item); + return; + } + if (rawType.endsWith("?")) { + if (value === null || value === undefined) { + writer.writeByte(0); + return; + } + writer.writeByte(1); + this.#encodeType(writer, rawType.slice(0, -1), value); + return; + } + if (rawType.endsWith("$")) { + if (value !== null && value !== undefined) this.#encodeType(writer, rawType.slice(0, -1), value); + return; + } + const type = this.resolveType(rawType); const struct = this.#structs.get(type); - if (struct) { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${type} expects an object`); if (struct.base) this.#encodeType(writer, struct.base, value); const record = value as Record; for (const field of struct.fields) this.#encodeType(writer, field.type, record[field.name]); return; } + if (struct) { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new TypeError(`${type} expects an object`); + } + if (struct.base) this.#encodeType(writer, struct.base, value); + const record = value as Record; + for (const field of struct.fields) this.#encodeType(writer, field.type, record[field.name]); + return; + } + const variant = this.#variants.get(type); - if (variant) { const pair = Array.isArray(value) ? value : [String((value as { type: string }).type), (value as { value: unknown }).value]; const index = variant.types.indexOf(String(pair[0])); if (index < 0) throw new TypeError(`Unknown ${type} variant: ${String(pair[0])}`); writer.writeVarUint(index); this.#encodeType(writer, variant.types[index]!, pair[1]); return; } + if (variant) { + if (value === null || value === undefined) throw new TypeError(`${type} expects a variant value`); + const pair = Array.isArray(value) + ? value + : [String((value as { type: string }).type), (value as { value: unknown }).value]; + const index = variant.types.indexOf(String(pair[0])); + if (index < 0) throw new TypeError(`Unknown ${type} variant: ${String(pair[0])}`); + writer.writeVarUint(index); + this.#encodeType(writer, variant.types[index]!, pair[1]); + return; + } + switch (type) { - case "bool": writer.writeByte(value ? 1 : 0); return; - case "uint8": writer.writeByte(Number(value)); return; - case "int8": writer.writeByte(Number(value)); return; - case "uint16": case "int16": writer.writeUint16(Number(value)); return; - case "uint32": case "int32": case "time_point_sec": writer.writeUint32(typeof value === "string" ? Math.floor(new Date(value).getTime() / 1000) : Number(value)); return; - case "uint64": writer.writeUint64(BigInt(value as string | number | bigint)); return; - case "int64": writer.writeInt64(BigInt(value as string | number | bigint)); return; - case "varuint32": writer.writeVarUint(Number(value)); return; - case "varint32": { const n = Number(value); writer.writeVarUint(((n << 1) ^ (n >> 31)) >>> 0); return; } - case "name": writer.writeName(String(value)); return; - case "string": writer.writeString(String(value)); return; - case "bytes": writer.writeVarBytes(typeof value === "string" ? hexToBytes(value) : value as Uint8Array); return; - case "checksum256": writer.writeBytes(typeof value === "string" ? hexToBytes(value) : value as Uint8Array); return; - case "asset": { const asset = parseAsset(String(value)); writer.writeInt64(asset.amount); writer.writeUint64(symbolToBigInt(asset.symbol, asset.precision)); return; } - case "symbol": { const match = /^(\d+),([A-Z]{1,7})$/.exec(String(value)); if (!match) throw new TypeError("symbol expects precision,CODE"); writer.writeUint64(symbolToBigInt(match[2]!, Number(match[1]))); return; } - case "symbol_code": { let raw = 0n; const code = String(value); for (let i = 0; i < code.length; i += 1) raw |= BigInt(code.charCodeAt(i)) << BigInt(8 * i); writer.writeUint64(raw); return; } - case "public_key": { const key = value instanceof PublicKey ? value : PublicKey.fromString(String(value)); writer.writeByte(key.type === "K1" ? 0 : 1); writer.writeBytes(key.toBytes()); return; } - case "signature": { const sig = value instanceof Signature ? value : Signature.fromString(String(value)); writer.writeByte(sig.type === "K1" ? 0 : 1); writer.writeBytes(sig.toBytes()); return; } - default: throw new TypeError(`Unsupported ABI type: ${type}`); + case "bool": + if (typeof value !== "boolean") throw new TypeError("bool expects true or false"); + writer.writeByte(value ? 1 : 0); + return; + case "uint8": + writer.writeByte(assertInteger(toNumber(value, type), 0, 0xff, type)); + return; + case "int8": { + const checked = assertInteger(toNumber(value, type), -0x80, 0x7f, type); + writer.writeByte(checked & 0xff); + return; + } + case "uint16": + writer.writeUint16(assertInteger(toNumber(value, type), 0, 0xffff, type)); + return; + case "int16": + writer.writeInt16(assertInteger(toNumber(value, type), -0x8000, 0x7fff, type)); + return; + case "uint32": + writer.writeUint32(assertInteger(toNumber(value, type), 0, 0xffffffff, type)); + return; + case "int32": + writer.writeInt32(assertInteger(toNumber(value, type), -0x80000000, 0x7fffffff, type)); + return; + case "uint64": + writer.writeUint64(assertBigIntRange(toBigInt(value, type), 64, false, type)); + return; + case "int64": + writer.writeInt64(assertBigIntRange(toBigInt(value, type), 64, true, type)); + return; + case "uint128": + writer.writeUint128(assertBigIntRange(toBigInt(value, type), 128, false, type)); + return; + case "int128": + writer.writeInt128(assertBigIntRange(toBigInt(value, type), 128, true, type)); + return; + case "varuint32": + case "varuint": + writer.writeVarUint(toNumber(value, type)); + return; + case "varint32": + case "varint": + writer.writeVarInt(toNumber(value, type)); + return; + case "float32": + writer.writeFloat32(toNumber(value, type)); + return; + case "float64": + writer.writeFloat64(toNumber(value, type)); + return; + case "float128": + writer.writeBytes(assertHexBytes(value, 16, type)); + return; + case "name": + writer.writeName(String(value)); + return; + case "string": + if (typeof value !== "string") throw new TypeError("string expects a string value"); + writer.writeString(value); + return; + case "bytes": + writer.writeVarBytes(typeof value === "string" ? hexToBytes(value) : assertHexBytes(value, (value as Uint8Array).length, type)); + return; + case "checksum160": + writer.writeBytes(assertHexBytes(value, 20, type)); + return; + case "checksum256": + writer.writeBytes(assertHexBytes(value, 32, type)); + return; + case "checksum512": + writer.writeBytes(assertHexBytes(value, 64, type)); + return; + case "asset": { + const asset = parseAsset(String(value)); + writer.writeInt64(asset.amount); + writer.writeUint64(symbolToBigInt(asset.symbol, asset.precision)); + return; + } + case "extended_asset": + encodeExtendedAsset(writer, value); + return; + case "symbol": { + const match = /^(\d+),([A-Z]{1,7})$/.exec(String(value)); + if (!match) throw new TypeError("symbol expects precision,CODE"); + writer.writeUint64(symbolToBigInt(match[2]!, Number(match[1]))); + return; + } + case "symbol_code": { + const code = validateSymbol(String(value)); + let raw = 0n; + for (let index = 0; index < code.length; index += 1) { + raw |= BigInt(code.charCodeAt(index)) << BigInt(8 * index); + } + writer.writeUint64(raw); + return; + } + case "time_point": + writer.writeInt64(timePointToMicros(value)); + return; + case "time_point_sec": { + const seconds = typeof value === "string" + ? Math.floor(Date.parse(/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`) / 1000) + : toNumber(value, type); + writer.writeUint32(assertInteger(seconds, 0, 0xffffffff, type)); + return; + } + case "block_timestamp_type": + writer.writeUint32(blockTimestampToSlot(value)); + return; + case "public_key": + case "publickey": { + const key = value instanceof PublicKey ? value : PublicKey.fromString(String(value)); + writer.writeByte(key.type === "K1" ? 0 : 1); + writer.writeBytes(key.toBytes()); + return; + } + case "signature": { + const signature = value instanceof Signature ? value : Signature.fromString(String(value)); + writer.writeByte(signature.type === "K1" ? 0 : 1); + writer.writeBytes(signature.toBytes()); + return; + } + default: + throw new TypeError(`Unsupported ABI type: ${type}`); } } + #decodeType(reader: BinaryReader, rawType: string): unknown { - if (rawType.endsWith("[]")) { const count = reader.readVarUint(); return Array.from({ length: count }, () => this.#decodeType(reader, rawType.slice(0, -2))); } - if (rawType.endsWith("?")) return reader.readByte() ? this.#decodeType(reader, rawType.slice(0, -1)) : null; - if (rawType.endsWith("$")) return reader.remaining ? this.#decodeType(reader, rawType.slice(0, -1)) : undefined; + if (rawType.endsWith("[]")) { + const count = reader.readVarUint(); + return Array.from({ length: count }, () => this.#decodeType(reader, rawType.slice(0, -2))); + } + if (rawType.endsWith("?")) { + const present = reader.readByte(); + if (present !== 0 && present !== 1) throw new TypeError(`Invalid optional marker: ${present}`); + return present ? this.#decodeType(reader, rawType.slice(0, -1)) : null; + } + if (rawType.endsWith("$")) { + return reader.remaining ? this.#decodeType(reader, rawType.slice(0, -1)) : undefined; + } + const type = this.resolveType(rawType); const struct = this.#structs.get(type); - if (struct) { const out: Record = {}; if (struct.base) Object.assign(out, this.#decodeType(reader, struct.base)); for (const field of struct.fields) out[field.name] = this.#decodeType(reader, field.type); return out; } + if (struct) { + const out: Record = {}; + if (struct.base) { + const base = this.#decodeType(reader, struct.base); + if (typeof base !== "object" || base === null || Array.isArray(base)) { + throw new TypeError(`ABI base struct ${struct.base} did not decode to an object`); + } + Object.assign(out, base); + } + for (const field of struct.fields) out[field.name] = this.#decodeType(reader, field.type); + return out; + } + const variant = this.#variants.get(type); - if (variant) { const index = reader.readVarUint(); const selected = variant.types[index]; if (!selected) throw new TypeError(`Invalid ${type} variant index: ${index}`); return { type: selected, value: this.#decodeType(reader, selected) }; } + if (variant) { + const index = reader.readVarUint(); + const selected = variant.types[index]; + if (!selected) throw new TypeError(`Invalid ${type} variant index: ${index}`); + return { type: selected, value: this.#decodeType(reader, selected) }; + } + switch (type) { - case "bool": return reader.readByte() !== 0; - case "uint8": return reader.readByte(); - case "int8": return (reader.readByte() << 24) >> 24; - case "uint16": return reader.readUint16(); - case "int16": return (reader.readUint16() << 16) >> 16; - case "uint32": return reader.readUint32(); - case "int32": return reader.readUint32() | 0; - case "time_point_sec": return new Date(reader.readUint32() * 1000).toISOString(); - case "uint64": return reader.readUint64(); - case "int64": return reader.readInt64(); - case "varuint32": return reader.readVarUint(); - case "varint32": { const n = reader.readVarUint(); return (n >>> 1) ^ -(n & 1); } - case "name": return reader.readName(); - case "string": return reader.readString(); - case "bytes": return bytesToHex(reader.readVarBytes()); - case "checksum256": return bytesToHex(reader.readBytes(32)); - case "asset": { const amount = reader.readInt64(); const { precision, symbol } = symbolFromBigInt(reader.readUint64()); const negative = amount < 0n; const digits = (negative ? -amount : amount).toString().padStart(precision + 1, "0"); const formatted = precision ? `${digits.slice(0, -precision)}.${digits.slice(-precision)}` : digits; return `${negative ? "-" : ""}${formatted} ${symbol}`; } - case "symbol": { const { precision, symbol } = symbolFromBigInt(reader.readUint64()); return `${precision},${symbol}`; } - case "symbol_code": return symbolFromBigInt(reader.readUint64() << 8n).symbol; - case "public_key": { const index = reader.readByte(); return PublicKey.fromBytes(index === 0 ? "K1" : "R1", reader.readBytes(33)).toString(); } - case "signature": { const index = reader.readByte(); return Signature.fromBytes(index === 0 ? "K1" : "R1", reader.readBytes(65)).toString(); } - default: throw new TypeError(`Unsupported ABI type: ${type}`); + case "bool": { + const value = reader.readByte(); + if (value !== 0 && value !== 1) throw new TypeError(`Invalid bool value: ${value}`); + return value === 1; + } + case "uint8": + return reader.readByte(); + case "int8": + return (reader.readByte() << 24) >> 24; + case "uint16": + return reader.readUint16(); + case "int16": + return reader.readInt16(); + case "uint32": + return reader.readUint32(); + case "int32": + return reader.readInt32(); + case "uint64": + return reader.readUint64(); + case "int64": + return reader.readInt64(); + case "uint128": + return reader.readUint128(); + case "int128": + return reader.readInt128(); + case "varuint32": + case "varuint": + return reader.readVarUint(); + case "varint32": + case "varint": + return reader.readVarInt(); + case "float32": + return reader.readFloat32(); + case "float64": + return reader.readFloat64(); + case "float128": + return bytesToHex(reader.readBytes(16)); + case "name": + return reader.readName(); + case "string": + return reader.readString(); + case "bytes": + return bytesToHex(reader.readVarBytes()); + case "checksum160": + return bytesToHex(reader.readBytes(20)); + case "checksum256": + return bytesToHex(reader.readBytes(32)); + case "checksum512": + return bytesToHex(reader.readBytes(64)); + case "asset": + return decodeAsset(reader); + case "extended_asset": + return { quantity: decodeAsset(reader), contract: reader.readName() }; + case "symbol": { + const { precision, symbol } = symbolFromBigInt(reader.readUint64()); + return `${precision},${symbol}`; + } + case "symbol_code": { + let raw = reader.readUint64(); + let code = ""; + while (raw > 0n) { + const char = Number(raw & 0xffn); + if (char < 65 || char > 90) throw new TypeError("Invalid encoded symbol_code"); + code += String.fromCharCode(char); + raw >>= 8n; + } + return validateSymbol(code); + } + case "time_point": + return microsToTimePoint(reader.readInt64()); + case "time_point_sec": + return new Date(reader.readUint32() * 1000).toISOString(); + case "block_timestamp_type": + return new Date(BLOCK_TIMESTAMP_EPOCH_MS + reader.readUint32() * 500).toISOString(); + case "public_key": + case "publickey": { + const keyType = reader.readByte(); + if (keyType !== 0 && keyType !== 1) throw new TypeError(`Unsupported public-key type: ${keyType}`); + return PublicKey.fromBytes(keyType === 0 ? "K1" : "R1", reader.readBytes(33)).toString(); + } + case "signature": { + const keyType = reader.readByte(); + if (keyType !== 0 && keyType !== 1) throw new TypeError(`Unsupported signature type: ${keyType}`); + return Signature.fromBytes(keyType === 0 ? "K1" : "R1", reader.readBytes(65)).toString(); + } + default: + throw new TypeError(`Unsupported ABI type: ${type}`); } } } From 686d1edde41cc9b9c45a91eda709e1608dd8fa35 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:51:19 +0700 Subject: [PATCH 03/49] fix(rpc): harden retries timeouts and chain errors --- packages/rpc/src/index.ts | 299 +++++++++++++++++++++++++++++++++----- 1 file changed, 266 insertions(+), 33 deletions(-) diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts index a1a7ed4..28bb9b7 100644 --- a/packages/rpc/src/index.ts +++ b/packages/rpc/src/index.ts @@ -5,65 +5,298 @@ * SPDX-License-Identifier: MIT */ export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise; -export type RpcClientOptions = { endpoints: string | string[]; fetch?: FetchLike; timeoutMs?: number; retries?: number }; -export type GetInfoResponse = { server_version?: string; chain_id: string; head_block_num: number; last_irreversible_block_num: number; head_block_id: string; head_block_time: string; head_block_producer?: string; [key: string]: unknown }; -export type GetBlockResponse = { timestamp: string; producer?: string; confirmed?: number; previous?: string; transaction_mroot?: string; action_mroot?: string; schedule_version?: number; producer_signature?: string; id: string; block_num: number; ref_block_prefix?: number; [key: string]: unknown }; -export type TableRowsRequest = { code: string; scope: string; table: string; json?: boolean; lower_bound?: string | number; upper_bound?: string | number; limit?: number; key_type?: string; index_position?: string | number; reverse?: boolean; show_payer?: boolean }; +export type RpcClientOptions = { + endpoints: string | string[]; + fetch?: FetchLike; + timeoutMs?: number; + retries?: number; +}; +export type RpcRequestOptions = { retries?: number }; +export type GetInfoResponse = { + server_version?: string; + chain_id: string; + head_block_num: number; + last_irreversible_block_num: number; + head_block_id: string; + head_block_time: string; + head_block_producer?: string; + [key: string]: unknown; +}; +export type GetBlockResponse = { + timestamp: string; + producer?: string; + confirmed?: number; + previous?: string; + transaction_mroot?: string; + action_mroot?: string; + schedule_version?: number; + producer_signature?: string; + id: string; + block_num: number; + ref_block_prefix?: number; + [key: string]: unknown; +}; +export type TableRowsRequest = { + code: string; + scope: string; + table: string; + json?: boolean; + lower_bound?: string | number; + upper_bound?: string | number; + limit?: number; + key_type?: string; + index_position?: string | number; + reverse?: boolean; + show_payer?: boolean; +}; export type TableRowsResponse = { rows: T[]; more: boolean | string; next_key?: string }; +export type TableByScopeRequest = { + code: string; + table?: string; + lower_bound?: string; + upper_bound?: string; + limit?: number; + reverse?: boolean; +}; +export type TableByScopeRow = { + code: string; + scope: string; + table: string; + payer: string; + count: number; +}; export class RpcError extends Error { - readonly status: number; readonly endpoint: string; readonly payload: unknown; - constructor(message: string, status: number, endpoint: string, payload?: unknown) { super(message); this.name = "RpcError"; this.status = status; this.endpoint = endpoint; this.payload = payload; } + readonly status: number; + readonly endpoint: string; + readonly payload: unknown; + + constructor(message: string, status: number, endpoint: string, payload?: unknown) { + super(message); + this.name = "RpcError"; + this.status = status; + this.endpoint = endpoint; + this.payload = payload; + } +} + +export class RpcTimeoutError extends Error { + readonly endpoint: string; + readonly timeoutMs: number; + + constructor(endpoint: string, timeoutMs: number) { + super(`RPC request to ${endpoint} timed out after ${timeoutMs}ms`); + this.name = "RpcTimeoutError"; + this.endpoint = endpoint; + this.timeoutMs = timeoutMs; + } +} + +function rpcMessage(payload: unknown, fallback: string): string { + if (!payload || typeof payload !== "object") return fallback; + const record = payload as Record; + if (typeof record.message === "string" && record.message) return record.message; + if (record.error && typeof record.error === "object") { + const error = record.error as Record; + if (typeof error.what === "string" && error.what) return error.what; + if (Array.isArray(error.details)) { + const detail = error.details.find( + (item) => item && typeof item === "object" && typeof (item as Record).message === "string", + ) as Record | undefined; + if (detail?.message) return String(detail.message); + } + } + return fallback; +} + +function isRetriable(error: unknown): boolean { + if (error instanceof RpcTimeoutError) return true; + if (error instanceof RpcError) { + return error.status === 408 || error.status === 425 || error.status === 429 || error.status >= 500; + } + return error instanceof TypeError || (error instanceof Error && error.name === "AbortError"); } export class RpcClient { readonly endpoints: readonly string[]; - readonly #fetch: FetchLike; readonly #timeoutMs: number; readonly #retries: number; + readonly #fetch: FetchLike; + readonly #timeoutMs: number; + readonly #retries: number; #cursor = 0; + constructor(options: RpcClientOptions) { - const endpoints = (Array.isArray(options.endpoints) ? options.endpoints : [options.endpoints]).map((endpoint) => endpoint.replace(/\/+$/, "")); - if (!endpoints.length || endpoints.some((endpoint) => !/^https?:\/\//.test(endpoint))) throw new TypeError("At least one http(s) RPC endpoint is required"); + const endpoints = (Array.isArray(options.endpoints) ? options.endpoints : [options.endpoints]) + .map((endpoint) => endpoint.trim().replace(/\/+$/, "")) + .filter(Boolean); + if (!endpoints.length || endpoints.some((endpoint) => !/^https?:\/\//.test(endpoint))) { + throw new TypeError("At least one http(s) RPC endpoint is required"); + } + const fetchImplementation = options.fetch ?? globalThis.fetch; + if (typeof fetchImplementation !== "function") { + throw new TypeError("A fetch implementation is required in this runtime"); + } + const timeoutMs = options.timeoutMs ?? 10_000; + const retries = options.retries ?? Math.max(0, endpoints.length - 1); + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new RangeError("timeoutMs must be greater than zero"); + if (!Number.isInteger(retries) || retries < 0) throw new RangeError("retries must be a non-negative integer"); + this.endpoints = Object.freeze([...new Set(endpoints)]); - this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis); - this.#timeoutMs = options.timeoutMs ?? 10_000; - this.#retries = Math.max(0, options.retries ?? Math.max(1, this.endpoints.length - 1)); + this.#fetch = fetchImplementation.bind(globalThis); + this.#timeoutMs = timeoutMs; + this.#retries = retries; } - async request(path: string, body: unknown = {}, signal?: AbortSignal): Promise { + + async request( + path: string, + body: unknown = {}, + signal?: AbortSignal, + options: RpcRequestOptions = {}, + ): Promise { + if (!path.startsWith("/")) throw new TypeError("RPC path must start with /"); + if (signal?.aborted) { + throw signal.reason instanceof Error ? signal.reason : new Error("RPC request aborted"); + } + + const retries = options.retries ?? this.#retries; + const attempts = retries + 1; let lastError: unknown; - const attempts = Math.min(this.#retries + 1, Math.max(this.endpoints.length, 1) + this.#retries); + for (let attempt = 0; attempt < attempts; attempt += 1) { - const endpoint = this.endpoints[(this.#cursor + attempt) % this.endpoints.length]!; + const endpointIndex = (this.#cursor + attempt) % this.endpoints.length; + const endpoint = this.endpoints[endpointIndex]!; const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(new DOMException("RPC request timed out", "TimeoutError")), this.#timeoutMs); - const onAbort = () => controller.abort(signal?.reason); - if (signal) { if (signal.aborted) controller.abort(signal.reason); else signal.addEventListener("abort", onAbort, { once: true }); } + let timedOut = false; + const timeout = setTimeout(() => { + timedOut = true; + controller.abort(); + }, this.#timeoutMs); + const onAbort = () => controller.abort(); + signal?.addEventListener("abort", onAbort, { once: true }); + try { - const response = await this.#fetch(`${endpoint}${path}`, { method: "POST", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(body), signal: controller.signal }); + const response = await this.#fetch(`${endpoint}${path}`, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify(body), + signal: controller.signal, + }); const text = await response.text(); let payload: unknown = null; - try { payload = text ? JSON.parse(text) : null; } catch { payload = text; } + try { + payload = text ? JSON.parse(text) : null; + } catch { + payload = text; + } if (!response.ok) { - const detail = typeof payload === "object" && payload && "message" in payload ? String((payload as { message: unknown }).message) : response.statusText; - throw new RpcError(detail || `RPC HTTP ${response.status}`, response.status, endpoint, payload); + throw new RpcError( + rpcMessage(payload, response.statusText || `RPC HTTP ${response.status}`), + response.status, + endpoint, + payload, + ); } - this.#cursor = (this.#cursor + attempt) % this.endpoints.length; + this.#cursor = endpointIndex; return payload as T; } catch (error) { - lastError = error; - if (signal?.aborted) throw error; + if (signal?.aborted) { + throw signal.reason instanceof Error ? signal.reason : error; + } + lastError = timedOut ? new RpcTimeoutError(endpoint, this.#timeoutMs) : error; + if (!isRetriable(lastError) || attempt === attempts - 1) break; } finally { clearTimeout(timeout); signal?.removeEventListener("abort", onAbort); } } + throw lastError instanceof Error ? lastError : new Error("Antelope RPC request failed"); } - getInfo(signal?: AbortSignal): Promise { return this.request("/v1/chain/get_info", {}, signal); } - getBlock(blockNumOrId: number | string, signal?: AbortSignal): Promise { return this.request("/v1/chain/get_block", { block_num_or_id: blockNumOrId }, signal); } - getAccount>(accountName: string, signal?: AbortSignal): Promise { return this.request("/v1/chain/get_account", { account_name: accountName }, signal); } - getAbi(accountName: string, signal?: AbortSignal): Promise<{ account_name: string; abi: unknown }> { return this.request("/v1/chain/get_abi", { account_name: accountName }, signal); } - getTableRows>(args: TableRowsRequest, signal?: AbortSignal): Promise> { return this.request("/v1/chain/get_table_rows", { json: true, ...args }, signal); } - getCurrencyBalance(code: string, account: string, symbol?: string, signal?: AbortSignal): Promise { return this.request("/v1/chain/get_currency_balance", { code, account, ...(symbol ? { symbol } : {}) }, signal); } - getRequiredKeys(transaction: unknown, availableKeys: string[], signal?: AbortSignal): Promise<{ required_keys: string[] }> { return this.request("/v1/chain/get_required_keys", { transaction, available_keys: availableKeys }, signal); } - pushTransaction>(transaction: { signatures: string[]; compression?: number; packed_context_free_data?: string; packed_trx: string }, signal?: AbortSignal): Promise { return this.request("/v1/chain/push_transaction", { compression: 0, packed_context_free_data: "", ...transaction }, signal); } + + getInfo(signal?: AbortSignal): Promise { + return this.request("/v1/chain/get_info", {}, signal); + } + + getBlock(blockNumOrId: number | string, signal?: AbortSignal): Promise { + return this.request("/v1/chain/get_block", { block_num_or_id: blockNumOrId }, signal); + } + + getAccount>(accountName: string, signal?: AbortSignal): Promise { + return this.request("/v1/chain/get_account", { account_name: accountName }, signal); + } + + getAbi(accountName: string, signal?: AbortSignal): Promise<{ account_name: string; abi: unknown }> { + return this.request("/v1/chain/get_abi", { account_name: accountName }, signal); + } + + getRawAbi>(accountName: string, signal?: AbortSignal): Promise { + return this.request("/v1/chain/get_raw_abi", { account_name: accountName }, signal); + } + + getCodeHash>(accountName: string, signal?: AbortSignal): Promise { + return this.request("/v1/chain/get_code_hash", { account_name: accountName }, signal); + } + + getTableRows>( + args: TableRowsRequest, + signal?: AbortSignal, + ): Promise> { + return this.request("/v1/chain/get_table_rows", { json: true, ...args }, signal); + } + + getTableByScope( + args: TableByScopeRequest, + signal?: AbortSignal, + ): Promise<{ rows: TableByScopeRow[]; more: string }> { + return this.request("/v1/chain/get_table_by_scope", args, signal); + } + + getCurrencyBalance( + code: string, + account: string, + symbol?: string, + signal?: AbortSignal, + ): Promise { + return this.request( + "/v1/chain/get_currency_balance", + { code, account, ...(symbol ? { symbol } : {}) }, + signal, + ); + } + + getCurrencyStats>( + code: string, + symbol: string, + signal?: AbortSignal, + ): Promise { + return this.request("/v1/chain/get_currency_stats", { code, symbol }, signal); + } + + getRequiredKeys( + transaction: unknown, + availableKeys: string[], + signal?: AbortSignal, + ): Promise<{ required_keys: string[] }> { + return this.request( + "/v1/chain/get_required_keys", + { transaction, available_keys: availableKeys }, + signal, + ); + } + + pushTransaction>( + transaction: { + signatures: string[]; + compression?: number; + packed_context_free_data?: string; + packed_trx: string; + }, + signal?: AbortSignal, + ): Promise { + return this.request( + "/v1/chain/push_transaction", + { compression: 0, packed_context_free_data: "", ...transaction }, + signal, + { retries: 0 }, + ); + } } From 8bc3846af2a7d557d61bcdaa63d852cc8a15b208 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:51:43 +0700 Subject: [PATCH 04/49] fix(contract): validate names and share ABI loading --- packages/contract/src/index.ts | 146 +++++++++++++++++++++++++++++---- 1 file changed, 129 insertions(+), 17 deletions(-) diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index 3a3cf46..449c7ee 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -4,40 +4,152 @@ * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI * SPDX-License-Identifier: MIT */ -import { AbiSerializer, bytesToHex, type Abi } from "@windstack/abi"; +import { AbiSerializer, bytesToHex, nameToBigInt, type Abi } from "@windstack/abi"; import { RpcClient, type TableRowsRequest, type TableRowsResponse } from "@windstack/rpc"; export type PermissionLevel = { actor: string; permission: string }; -export type ContractAction = { account: string; name: string; authorization: PermissionLevel[]; data: string }; +export type ContractAction = { + account: string; + name: string; + authorization: PermissionLevel[]; + data: string; +}; export type AuthorizationInput = PermissionLevel | `${string}@${string}`; +function validateName(value: string, label: string): string { + if (!value) throw new TypeError(`${label} is required`); + nameToBigInt(value); + return value; +} + export class AbiCache { readonly #entries = new Map(); - constructor(readonly ttlMs = 5 * 60_000) {} - get(account: string): Abi | undefined { const entry = this.#entries.get(account); if (!entry || entry.expiresAt < Date.now()) { this.#entries.delete(account); return undefined; } return entry.abi; } - set(account: string, abi: Abi): void { this.#entries.set(account, { abi, expiresAt: Date.now() + this.ttlMs }); } - delete(account: string): void { this.#entries.delete(account); } - clear(): void { this.#entries.clear(); } + + constructor(readonly ttlMs = 5 * 60_000) { + if (!Number.isFinite(ttlMs) || ttlMs < 0) throw new RangeError("ABI cache TTL must be non-negative"); + } + + get(account: string): Abi | undefined { + const entry = this.#entries.get(account); + if (!entry || entry.expiresAt < Date.now()) { + this.#entries.delete(account); + return undefined; + } + return entry.abi; + } + + set(account: string, abi: Abi): void { + this.#entries.set(account, { abi, expiresAt: Date.now() + this.ttlMs }); + } + + delete(account: string): void { + this.#entries.delete(account); + } + + clear(): void { + this.#entries.clear(); + } } function normalizeAuthorization(input: AuthorizationInput[]): PermissionLevel[] { - return input.map((item) => typeof item === "string" ? (() => { const [actor, permission = "active"] = item.split("@"); if (!actor) throw new TypeError("Authorization actor is required"); return { actor, permission }; })() : item); + return input.map((item) => { + const value = + typeof item === "string" + ? (() => { + const separator = item.indexOf("@"); + const actor = separator >= 0 ? item.slice(0, separator) : item; + const permission = separator >= 0 ? item.slice(separator + 1) : "active"; + return { actor, permission }; + })() + : item; + return { + actor: validateName(value.actor, "Authorization actor"), + permission: validateName(value.permission, "Authorization permission"), + }; + }); } export class Contract { - readonly account: string; readonly rpc: RpcClient; readonly abiCache: AbiCache; - constructor(account: string, rpc: RpcClient, abiCache = new AbiCache()) { this.account = account; this.rpc = rpc; this.abiCache = abiCache; } + readonly account: string; + readonly rpc: RpcClient; + readonly abiCache: AbiCache; + #pendingAbi: Promise | null = null; + + constructor(account: string, rpc: RpcClient, abiCache = new AbiCache()) { + this.account = validateName(account, "Contract account"); + this.rpc = rpc; + this.abiCache = abiCache; + } + async getAbi(force = false, signal?: AbortSignal): Promise { - if (!force) { const cached = this.abiCache.get(this.account); if (cached) return cached; } - const result = await this.rpc.getAbi(this.account, signal); - if (!result.abi || typeof result.abi !== "object") throw new TypeError(`RPC returned no ABI for ${this.account}`); - const abi = result.abi as Abi; this.abiCache.set(this.account, abi); return abi; + if (!force) { + const cached = this.abiCache.get(this.account); + if (cached) return cached; + if (this.#pendingAbi) return this.#pendingAbi; + } + + const load = async (): Promise => { + const result = await this.rpc.getAbi(this.account, signal); + if (!result.abi || typeof result.abi !== "object") { + throw new TypeError(`RPC returned no ABI for ${this.account}`); + } + const abi = result.abi as Abi; + new AbiSerializer(abi); + this.abiCache.set(this.account, abi); + return abi; + }; + + if (force) return load(); + this.#pendingAbi = load().finally(() => { + this.#pendingAbi = null; + }); + return this.#pendingAbi; } - async action(name: string, data: unknown, authorization: AuthorizationInput[] = [], signal?: AbortSignal): Promise { + + async action( + name: string, + data: unknown, + authorization: AuthorizationInput[] = [], + signal?: AbortSignal, + ): Promise { + validateName(name, "Action name"); const serializer = new AbiSerializer(await this.getAbi(false, signal)); - return { account: this.account, name, authorization: normalizeAuthorization(authorization), data: bytesToHex(serializer.encodeAction(name, data)) }; + return { + account: this.account, + name, + authorization: normalizeAuthorization(authorization), + data: bytesToHex(serializer.encodeAction(name, data)), + }; } - tableRows>(table: string, scope: string = this.account, options: Omit = {}, signal?: AbortSignal): Promise> { + + tableRows>( + table: string, + scope: string = this.account, + options: Omit = {}, + signal?: AbortSignal, + ): Promise> { + validateName(table, "Table name"); + validateName(scope, "Table scope"); return this.rpc.getTableRows({ code: this.account, scope, table, ...options }, signal); } } + +export class ContractKit { + readonly rpc: RpcClient; + readonly abiCache: AbiCache; + + constructor(rpc: RpcClient, options: { abiCache?: AbiCache } = {}) { + this.rpc = rpc; + this.abiCache = options.abiCache ?? new AbiCache(); + } + + contract(account: string): Contract { + return new Contract(account, this.rpc, this.abiCache); + } + + async load(account: string, signal?: AbortSignal): Promise { + const contract = this.contract(account); + await contract.getAbi(false, signal); + return contract; + } +} From d1157f4108d2f66bf20235045fbdf1dcaadc9afc Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:52:36 +0700 Subject: [PATCH 05/49] fix(account): require explicit chain contracts --- packages/account/src/index.ts | 181 +++++++++++++++++++++++++++++++--- 1 file changed, 167 insertions(+), 14 deletions(-) diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 312c0b9..dbb1df8 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -4,22 +4,175 @@ * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI * SPDX-License-Identifier: MIT */ +import { nameToBigInt } from "@windstack/abi"; import { AbiCache, Contract, type ContractAction } from "@windstack/contract"; import { RpcClient } from "@windstack/rpc"; +export type AccountClientOptions = { + tokenContract?: string; + systemContract?: string; +}; + +function validateName(value: string, label: string): string { + if (!value) throw new TypeError(`${label} is required`); + nameToBigInt(value); + return value; +} + export class AccountClient { - readonly name: string; readonly rpc: RpcClient; readonly abiCache: AbiCache; - constructor(name: string, rpc: RpcClient, abiCache = new AbiCache()) { this.name = name; this.rpc = rpc; this.abiCache = abiCache; } - get>(signal?: AbortSignal): Promise { return this.rpc.getAccount(this.name, signal); } - balance(tokenContract = "eosio.token", symbol?: string, signal?: AbortSignal): Promise { return this.rpc.getCurrencyBalance(tokenContract, this.name, symbol, signal); } - contract(account: string): Contract { return new Contract(account, this.rpc, this.abiCache); } - async transfer(to: string, quantity: string, memo = "", options: { tokenContract?: string; permission?: string } = {}, signal?: AbortSignal): Promise { - const token = this.contract(options.tokenContract ?? "eosio.token"); - return token.action("transfer", { from: this.name, to, quantity, memo }, [`${this.name}@${options.permission ?? "active"}`], signal); - } - systemAction(name: string, data: unknown, permission = "active", signal?: AbortSignal): Promise { return this.contract("eosio").action(name, data, [`${this.name}@${permission}`], signal); } - delegate(receiver: string, stakeNetQuantity: string, stakeCpuQuantity: string, transfer = false, signal?: AbortSignal): Promise { return this.systemAction("delegatebw", { from: this.name, receiver, stake_net_quantity: stakeNetQuantity, stake_cpu_quantity: stakeCpuQuantity, transfer }, "active", signal); } - undelegate(receiver: string, unstakeNetQuantity: string, unstakeCpuQuantity: string, signal?: AbortSignal): Promise { return this.systemAction("undelegatebw", { from: this.name, receiver, unstake_net_quantity: unstakeNetQuantity, unstake_cpu_quantity: unstakeCpuQuantity }, "active", signal); } - buyRamBytes(receiver: string, bytes: number, signal?: AbortSignal): Promise { return this.systemAction("buyrambytes", { payer: this.name, receiver, bytes }, "active", signal); } - sellRam(bytes: number, signal?: AbortSignal): Promise { return this.systemAction("sellram", { account: this.name, bytes }, "active", signal); } + readonly name: string; + readonly rpc: RpcClient; + readonly abiCache: AbiCache; + readonly tokenContract?: string; + readonly systemContract?: string; + + constructor( + name: string, + rpc: RpcClient, + abiCache = new AbiCache(), + options: AccountClientOptions = {}, + ) { + this.name = validateName(name, "Account name"); + this.rpc = rpc; + this.abiCache = abiCache; + this.tokenContract = options.tokenContract + ? validateName(options.tokenContract, "Token contract") + : undefined; + this.systemContract = options.systemContract + ? validateName(options.systemContract, "System contract") + : undefined; + } + + get>(signal?: AbortSignal): Promise { + return this.rpc.getAccount(this.name, signal); + } + + balance( + tokenContract = this.tokenContract, + symbol?: string, + signal?: AbortSignal, + ): Promise { + if (!tokenContract) { + throw new TypeError("Token contract is required; configure it on the client or pass it explicitly"); + } + return this.rpc.getCurrencyBalance( + validateName(tokenContract, "Token contract"), + this.name, + symbol, + signal, + ); + } + + contract(account: string): Contract { + return new Contract(account, this.rpc, this.abiCache); + } + + async transfer( + to: string, + quantity: string, + memo = "", + options: { tokenContract?: string; permission?: string } = {}, + signal?: AbortSignal, + ): Promise { + const tokenContract = options.tokenContract ?? this.tokenContract; + if (!tokenContract) { + throw new TypeError("Token contract is required; configure it on the client or pass tokenContract"); + } + const permission = validateName(options.permission ?? "active", "Permission"); + return this.contract(tokenContract).action( + "transfer", + { from: this.name, to: validateName(to, "Transfer recipient"), quantity, memo }, + [`${this.name}@${permission}`], + signal, + ); + } + + systemAction( + name: string, + data: unknown, + permission = "active", + signal?: AbortSignal, + ): Promise { + if (!this.systemContract) { + throw new TypeError("System contract is required; configure it on the client before using system actions"); + } + return this.contract(this.systemContract).action( + name, + data, + [`${this.name}@${validateName(permission, "Permission")}`], + signal, + ); + } + + delegate( + receiver: string, + stakeNetQuantity: string, + stakeCpuQuantity: string, + transfer = false, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "delegatebw", + { + from: this.name, + receiver: validateName(receiver, "Receiver"), + stake_net_quantity: stakeNetQuantity, + stake_cpu_quantity: stakeCpuQuantity, + transfer, + }, + "active", + signal, + ); + } + + undelegate( + receiver: string, + unstakeNetQuantity: string, + unstakeCpuQuantity: string, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "undelegatebw", + { + from: this.name, + receiver: validateName(receiver, "Receiver"), + unstake_net_quantity: unstakeNetQuantity, + unstake_cpu_quantity: unstakeCpuQuantity, + }, + "active", + signal, + ); + } + + buyRam(receiver: string, quantity: string, signal?: AbortSignal): Promise { + return this.systemAction( + "buyram", + { payer: this.name, receiver: validateName(receiver, "Receiver"), quant: quantity }, + "active", + signal, + ); + } + + buyRamBytes(receiver: string, bytes: number, signal?: AbortSignal): Promise { + if (!Number.isSafeInteger(bytes) || bytes <= 0) { + throw new RangeError("RAM bytes must be a positive safe integer"); + } + return this.systemAction( + "buyrambytes", + { payer: this.name, receiver: validateName(receiver, "Receiver"), bytes }, + "active", + signal, + ); + } + + sellRam(bytes: number, signal?: AbortSignal): Promise { + if (!Number.isSafeInteger(bytes) || bytes <= 0) { + throw new RangeError("RAM bytes must be a positive safe integer"); + } + return this.systemAction("sellram", { account: this.name, bytes }, "active", signal); + } + + refund(signal?: AbortSignal): Promise { + return this.systemAction("refund", { owner: this.name }, "active", signal); + } } From 7db2179a69af00703ed749a35adecded5bb5b546 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:53:14 +0700 Subject: [PATCH 06/49] fix(antelope): harden signing TAPOS and chain validation --- packages/antelope/src/index.ts | 276 ++++++++++++++++++++++++++++----- 1 file changed, 239 insertions(+), 37 deletions(-) diff --git a/packages/antelope/src/index.ts b/packages/antelope/src/index.ts index bcfbad6..c8dffc0 100644 --- a/packages/antelope/src/index.ts +++ b/packages/antelope/src/index.ts @@ -4,10 +4,17 @@ * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI * SPDX-License-Identifier: MIT */ -import { AccountClient } from "@windstack/account"; -import { BinaryWriter, hexToBytes, bytesToHex } from "@windstack/abi"; +import { AccountClient, type AccountClientOptions } from "@windstack/account"; +import { BinaryWriter, bytesToHex, hexToBytes } from "@windstack/abi"; import { AbiCache, Contract, type ContractAction } from "@windstack/contract"; -import { PrivateKey, PublicKey, Signature, concatBytes, hexToBytes as cryptoHexToBytes, sha256Digest } from "@windstack/crypto"; +import { + PrivateKey, + PublicKey, + Signature, + concatBytes, + hexToBytes as cryptoHexToBytes, + sha256Digest, +} from "@windstack/crypto"; import { RpcClient, type GetBlockResponse, type RpcClientOptions } from "@windstack/rpc"; export * from "@windstack/account"; @@ -18,6 +25,7 @@ export type { KeyType } from "@windstack/crypto"; export * from "@windstack/rpc"; export type Action = ContractAction; +export type TransactionExtension = [number, string]; export type Transaction = { expiration: string; ref_block_num: number; @@ -27,11 +35,39 @@ export type Transaction = { delay_sec: number; context_free_actions: Action[]; actions: Action[]; - transaction_extensions: Array<[number, string]>; + transaction_extensions: TransactionExtension[]; +}; +export type SignRequest = { + chainId: string; + transaction: Transaction; + serializedTransaction: Uint8Array; + digest: Uint8Array; + requiredKeys: string[]; +}; +export interface Signer { + getAvailableKeys(): Promise; + sign(request: SignRequest): Promise>; +} +export type TransactArgs = { + actions: Action[]; + signer: Signer; + contextFreeActions?: Action[]; + contextFreeData?: Uint8Array; + transactionExtensions?: TransactionExtension[]; + broadcast?: boolean; + expireSeconds?: number; + signal?: AbortSignal; +}; +export type TransactResult> = { + transaction: Transaction; + serializedTransaction: Uint8Array; + signatures: string[]; + response?: T; +}; +export type ChainContracts = { + token?: string; + system?: string; }; -export type Signer = { getAvailableKeys(): Promise; signDigest(digest: Uint8Array, requiredKeys: string[]): Promise> }; -export type TransactArgs = { actions: Action[]; signer: Signer; broadcast?: boolean; expireSeconds?: number; signal?: AbortSignal }; -export type TransactResult> = { transaction: Transaction; serializedTransaction: Uint8Array; signatures: string[]; response?: T }; function blockPrefix(block: GetBlockResponse): number { if (typeof block.ref_block_prefix === "number") return block.ref_block_prefix >>> 0; @@ -39,58 +75,224 @@ function blockPrefix(block: GetBlockResponse): number { if (bytes.length !== 32) throw new TypeError("Invalid block id"); return new DataView(bytes.buffer, bytes.byteOffset + 8, 4).getUint32(0, true); } -function timestampSeconds(value: string): number { const normalized = /(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`; const ms = Date.parse(normalized); if (!Number.isFinite(ms)) throw new TypeError(`Invalid block timestamp: ${value}`); return Math.floor(ms / 1000); } -function transactionForRpc(transaction: Transaction): Record { return { ...transaction, context_free_actions: transaction.context_free_actions, actions: transaction.actions, transaction_extensions: transaction.transaction_extensions }; } + +function timestampSeconds(value: string): number { + const normalized = /(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`; + const milliseconds = Date.parse(normalized); + if (!Number.isFinite(milliseconds)) throw new TypeError(`Invalid block timestamp: ${value}`); + return Math.floor(milliseconds / 1000); +} + +function assertUint(value: number, max: number, label: string): number { + if (!Number.isInteger(value) || value < 0 || value > max) { + throw new RangeError(`${label} must be an integer between 0 and ${max}`); + } + return value; +} + +function transactionForRpc(transaction: Transaction): Record { + return { + expiration: transaction.expiration, + ref_block_num: transaction.ref_block_num, + ref_block_prefix: transaction.ref_block_prefix, + max_net_usage_words: transaction.max_net_usage_words, + max_cpu_usage_ms: transaction.max_cpu_usage_ms, + delay_sec: transaction.delay_sec, + context_free_actions: transaction.context_free_actions, + actions: transaction.actions, + transaction_extensions: transaction.transaction_extensions, + }; +} + function writeAction(writer: BinaryWriter, action: Action): void { - writer.writeName(action.account); writer.writeName(action.name); writer.writeVarUint(action.authorization.length); - for (const permission of action.authorization) { writer.writeName(permission.actor); writer.writeName(permission.permission); } + writer.writeName(action.account); + writer.writeName(action.name); + writer.writeVarUint(action.authorization.length); + for (const permission of action.authorization) { + writer.writeName(permission.actor); + writer.writeName(permission.permission); + } writer.writeVarBytes(hexToBytes(action.data)); } + export function serializeTransaction(transaction: Transaction): Uint8Array { + const expirationSeconds = timestampSeconds(transaction.expiration); const writer = new BinaryWriter(); - writer.writeUint32(Math.floor(Date.parse(transaction.expiration.endsWith("Z") ? transaction.expiration : `${transaction.expiration}Z`) / 1000)); - writer.writeUint16(transaction.ref_block_num); writer.writeUint32(transaction.ref_block_prefix); writer.writeVarUint(transaction.max_net_usage_words); writer.writeByte(transaction.max_cpu_usage_ms); writer.writeVarUint(transaction.delay_sec); - writer.writeVarUint(transaction.context_free_actions.length); for (const action of transaction.context_free_actions) writeAction(writer, action); - writer.writeVarUint(transaction.actions.length); for (const action of transaction.actions) writeAction(writer, action); - writer.writeVarUint(transaction.transaction_extensions.length); for (const [type, data] of transaction.transaction_extensions) { writer.writeUint16(type); writer.writeVarBytes(hexToBytes(data)); } + writer.writeUint32(assertUint(expirationSeconds, 0xffffffff, "expiration")); + writer.writeUint16(assertUint(transaction.ref_block_num, 0xffff, "ref_block_num")); + writer.writeUint32(assertUint(transaction.ref_block_prefix, 0xffffffff, "ref_block_prefix")); + writer.writeVarUint(assertUint(transaction.max_net_usage_words, 0xffffffff, "max_net_usage_words")); + writer.writeByte(assertUint(transaction.max_cpu_usage_ms, 0xff, "max_cpu_usage_ms")); + writer.writeVarUint(assertUint(transaction.delay_sec, 0xffffffff, "delay_sec")); + writer.writeVarUint(transaction.context_free_actions.length); + for (const action of transaction.context_free_actions) writeAction(writer, action); + writer.writeVarUint(transaction.actions.length); + for (const action of transaction.actions) writeAction(writer, action); + writer.writeVarUint(transaction.transaction_extensions.length); + for (const [type, data] of transaction.transaction_extensions) { + writer.writeUint16(assertUint(type, 0xffff, "transaction extension type")); + writer.writeVarBytes(hexToBytes(data)); + } return writer.toBytes(); } -export function transactionDigest(chainId: string, serializedTransaction: Uint8Array, contextFreeDataHash = new Uint8Array(32)): Uint8Array { - const id = cryptoHexToBytes(chainId); if (id.length !== 32) throw new TypeError("Antelope chain id must be 32 bytes"); + +export function transactionDigest( + chainId: string, + serializedTransaction: Uint8Array, + contextFreeDataHash = new Uint8Array(32), +): Uint8Array { + const id = cryptoHexToBytes(chainId); + if (id.length !== 32) throw new TypeError("Antelope chain id must be 32 bytes"); + if (!(contextFreeDataHash instanceof Uint8Array) || contextFreeDataHash.length !== 32) { + throw new TypeError("Context-free data hash must be 32 bytes"); + } return sha256Digest(concatBytes(id, serializedTransaction, contextFreeDataHash)); } +export type PrivateKeySignerOptions = { + k1PublicKeyFormat?: "legacy" | "modern"; +}; + export class PrivateKeySigner implements Signer { readonly #keys: PrivateKey[]; - constructor(keys: PrivateKey[]) { if (!keys.length) throw new TypeError("At least one private key is required"); this.#keys = [...keys]; } - async getAvailableKeys(): Promise { return this.#keys.map((key) => key.toPublicKey().toString()); } - async signDigest(digest: Uint8Array, requiredKeys: string[]): Promise { - const wanted = new Set(requiredKeys.map((key) => PublicKey.fromString(key).toString())); - return this.#keys.filter((key) => wanted.has(key.toPublicKey().toString())).map((key) => key.signDigest(digest)); + readonly #k1PublicKeyFormat: "legacy" | "modern"; + + constructor(keys: PrivateKey[], options: PrivateKeySignerOptions = {}) { + if (!keys.length) throw new TypeError("At least one private key is required"); + this.#keys = [...keys]; + this.#k1PublicKeyFormat = options.k1PublicKeyFormat ?? "legacy"; + } + + async getAvailableKeys(): Promise { + return this.#keys.map((key) => { + const publicKey = key.toPublicKey(); + return key.type === "K1" && this.#k1PublicKeyFormat === "legacy" + ? publicKey.toLegacyString() + : publicKey.toString(); + }); + } + + async sign(request: SignRequest): Promise { + const keyMap = new Map( + this.#keys.map((key) => [key.toPublicKey().toString(), key] as const), + ); + return request.requiredKeys.map((requiredKey) => { + const normalized = PublicKey.fromString(requiredKey).toString(); + const key = keyMap.get(normalized); + if (!key) throw new Error(`No private key available for required key ${requiredKey}`); + return key.signDigest(request.digest); + }); } } -export type AntelopeClientOptions = RpcClientOptions & { abiCache?: AbiCache }; +export type AntelopeClientOptions = RpcClientOptions & { + abiCache?: AbiCache; + chainId?: string; + contracts?: ChainContracts; +}; + export class AntelopeClient { - readonly rpc: RpcClient; readonly abiCache: AbiCache; - constructor(options: AntelopeClientOptions) { this.rpc = new RpcClient(options); this.abiCache = options.abiCache ?? new AbiCache(); } - contract(account: string): Contract { return new Contract(account, this.rpc, this.abiCache); } - account(name: string): AccountClient { return new AccountClient(name, this.rpc, this.abiCache); } + readonly rpc: RpcClient; + readonly abiCache: AbiCache; + readonly chainId?: string; + readonly contracts: Readonly; + + constructor(options: AntelopeClientOptions) { + if (options.chainId) { + const chainId = cryptoHexToBytes(options.chainId); + if (chainId.length !== 32) throw new TypeError("Configured Antelope chain id must be 32 bytes"); + } + this.rpc = new RpcClient(options); + this.abiCache = options.abiCache ?? new AbiCache(); + this.chainId = options.chainId?.toLowerCase(); + this.contracts = Object.freeze({ ...(options.contracts ?? {}) }); + } + + contract(account: string): Contract { + return new Contract(account, this.rpc, this.abiCache); + } + + account(name: string): AccountClient { + const options: AccountClientOptions = { + tokenContract: this.contracts.token, + systemContract: this.contracts.system, + }; + return new AccountClient(name, this.rpc, this.abiCache, options); + } + async transact>(args: TransactArgs): Promise> { if (!args.actions.length) throw new TypeError("Transaction must include at least one action"); + const expireSeconds = args.expireSeconds ?? 120; + if (!Number.isInteger(expireSeconds) || expireSeconds < 1 || expireSeconds > 3600) { + throw new RangeError("expireSeconds must be an integer between 1 and 3600"); + } + const info = await this.rpc.getInfo(args.signal); + const actualChainId = info.chain_id.toLowerCase(); + if (!/^[0-9a-f]{64}$/.test(actualChainId)) { + throw new TypeError("RPC returned an invalid Antelope chain id"); + } + if (this.chainId && actualChainId !== this.chainId) { + throw new Error(`RPC chain mismatch: expected ${this.chainId}, received ${actualChainId}`); + } + const block = await this.rpc.getBlock(info.last_irreversible_block_num, args.signal); - const expiration = new Date((timestampSeconds(block.timestamp) + (args.expireSeconds ?? 120)) * 1000).toISOString().replace(/\.000Z$/, ""); - const transaction: Transaction = { expiration, ref_block_num: block.block_num & 0xffff, ref_block_prefix: blockPrefix(block), max_net_usage_words: 0, max_cpu_usage_ms: 0, delay_sec: 0, context_free_actions: [], actions: args.actions, transaction_extensions: [] }; + const expiration = new Date((timestampSeconds(info.head_block_time) + expireSeconds) * 1000) + .toISOString() + .replace(/\.000Z$/, ""); + const transaction: Transaction = { + expiration, + ref_block_num: block.block_num & 0xffff, + ref_block_prefix: blockPrefix(block), + max_net_usage_words: 0, + max_cpu_usage_ms: 0, + delay_sec: 0, + context_free_actions: args.contextFreeActions ?? [], + actions: args.actions, + transaction_extensions: args.transactionExtensions ?? [], + }; + const serializedTransaction = serializeTransaction(transaction); + const contextFreeDataHash = args.contextFreeData?.length + ? sha256Digest(args.contextFreeData) + : new Uint8Array(32); + const digest = transactionDigest(actualChainId, serializedTransaction, contextFreeDataHash); const availableKeys = await args.signer.getAvailableKeys(); - const { required_keys: requiredKeys } = await this.rpc.getRequiredKeys(transactionForRpc(transaction), availableKeys, args.signal); - const digest = transactionDigest(info.chain_id, serializedTransaction); - const signed = await args.signer.signDigest(digest, requiredKeys); - const signatures = signed.map((signature) => typeof signature === "string" ? signature : signature.toString()); - if (signatures.length !== requiredKeys.length) throw new Error(`Signer returned ${signatures.length} signatures for ${requiredKeys.length} required keys`); - if (args.broadcast === false) return { transaction, serializedTransaction, signatures }; - const response = await this.rpc.pushTransaction({ signatures, compression: 0, packed_context_free_data: "", packed_trx: bytesToHex(serializedTransaction) }, args.signal); + if (!availableKeys.length) throw new Error("Signer returned no available keys"); + const { required_keys: requiredKeys } = await this.rpc.getRequiredKeys( + transactionForRpc(transaction), + availableKeys, + args.signal, + ); + const signed = await args.signer.sign({ + chainId: actualChainId, + transaction, + serializedTransaction, + digest, + requiredKeys, + }); + const signatures = signed.map((signature) => + typeof signature === "string" ? Signature.fromString(signature).toString() : signature.toString(), + ); + if (signatures.length !== requiredKeys.length) { + throw new Error( + `Signer returned ${signatures.length} signatures for ${requiredKeys.length} required keys`, + ); + } + + if (args.broadcast === false) { + return { transaction, serializedTransaction, signatures }; + } + + const response = await this.rpc.pushTransaction( + { + signatures, + compression: 0, + packed_context_free_data: args.contextFreeData ? bytesToHex(args.contextFreeData) : "", + packed_trx: bytesToHex(serializedTransaction), + }, + args.signal, + ); return { transaction, serializedTransaction, signatures, response }; } } From 1f3326f4719c519e69c155e68fa9ef6a06a1704a Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:53:52 +0700 Subject: [PATCH 07/49] fix(session): validate restore and bind sessions to chain --- packages/session/src/native.ts | 262 +++++++++++++++++++++++++++++---- 1 file changed, 231 insertions(+), 31 deletions(-) diff --git a/packages/session/src/native.ts b/packages/session/src/native.ts index 9623ab8..db432df 100644 --- a/packages/session/src/native.ts +++ b/packages/session/src/native.ts @@ -4,55 +4,255 @@ * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI * SPDX-License-Identifier: MIT */ -import { AntelopeClient, type Action, type Signer, type TransactResult } from "@windstack/antelope"; +import { + AntelopeClient, + nameToBigInt, + type Action, + type ChainContracts, + type Signer, + type TransactResult, +} from "@windstack/antelope"; -export type SessionChain = { id: string; url: string }; +export type SessionChain = { + id: string; + url: string | string[]; + contracts?: ChainContracts; +}; export type SessionIdentity = { actor: string; permission: string; publicKey?: string }; export type WalletLoginContext = { chain: SessionChain; appName?: string }; export type WalletLoginResult = { identity: SessionIdentity; signer: Signer }; -export interface WalletPlugin { readonly id: string; login(context: WalletLoginContext): Promise; logout?(context: WalletLoginContext & { identity: SessionIdentity }): Promise } -export interface SessionStorage { get(key: string): Promise; set(key: string, value: string): Promise; remove(key: string): Promise } -export class MemorySessionStorage implements SessionStorage { readonly #values = new Map(); async get(key: string): Promise { return this.#values.get(key) ?? null; } async set(key: string, value: string): Promise { this.#values.set(key, value); } async remove(key: string): Promise { this.#values.delete(key); } } +export interface WalletPlugin { + readonly id: string; + login(context: WalletLoginContext): Promise; + restore?( + context: WalletLoginContext & { identity: SessionIdentity }, + ): Promise; + logout?(context: WalletLoginContext & { identity: SessionIdentity }): Promise; +} +export interface SessionStorage { + get(key: string): Promise; + set(key: string, value: string): Promise; + remove(key: string): Promise; +} + +export class MemorySessionStorage implements SessionStorage { + readonly #values = new Map(); + + async get(key: string): Promise { + return this.#values.get(key) ?? null; + } + + async set(key: string, value: string): Promise { + this.#values.set(key, value); + } + + async remove(key: string): Promise { + this.#values.delete(key); + } +} + +function validateIdentity(identity: SessionIdentity): SessionIdentity { + if (!identity || typeof identity !== "object") throw new TypeError("Wallet returned no identity"); + nameToBigInt(identity.actor); + nameToBigInt(identity.permission); + if (identity.publicKey !== undefined && typeof identity.publicKey !== "string") { + throw new TypeError("Wallet identity publicKey must be a string"); + } + return { ...identity }; +} + +function validateChain(chain: SessionChain): SessionChain { + if (!/^[0-9a-f]{64}$/i.test(chain.id)) { + throw new TypeError("Session chain id must be a 64-character Antelope chain id"); + } + const urls = Array.isArray(chain.url) ? chain.url : [chain.url]; + if (!urls.length || urls.some((url) => typeof url !== "string" || !url.trim())) { + throw new TypeError("Session chain requires at least one RPC URL"); + } + return { ...chain, id: chain.id.toLowerCase(), url: Array.isArray(chain.url) ? [...chain.url] : chain.url }; +} export class Session { - readonly chain: SessionChain; readonly identity: SessionIdentity; readonly walletPlugin: WalletPlugin; readonly client: AntelopeClient; readonly signer: Signer; - constructor(args: { chain: SessionChain; identity: SessionIdentity; walletPlugin: WalletPlugin; signer: Signer }) { this.chain = args.chain; this.identity = args.identity; this.walletPlugin = args.walletPlugin; this.signer = args.signer; this.client = new AntelopeClient({ endpoints: args.chain.url }); } - get actor(): string { return this.identity.actor; } - get permission(): string { return this.identity.permission; } - get permissionLevel(): { actor: string; permission: string } { return { actor: this.actor, permission: this.permission }; } - transact>(args: { actions: Action[]; broadcast?: boolean; expireSeconds?: number; signal?: AbortSignal }): Promise> { return this.client.transact({ ...args, signer: this.signer }); } - contract(account: string) { return this.client.contract(account); } - account(name = this.actor) { return this.client.account(name); } + readonly chain: SessionChain; + readonly identity: SessionIdentity; + readonly walletPlugin: WalletPlugin; + readonly client: AntelopeClient; + readonly signer: Signer; + + constructor(args: { + chain: SessionChain; + identity: SessionIdentity; + walletPlugin: WalletPlugin; + signer: Signer; + }) { + this.chain = validateChain(args.chain); + this.identity = validateIdentity(args.identity); + this.walletPlugin = args.walletPlugin; + this.signer = args.signer; + this.client = new AntelopeClient({ + endpoints: this.chain.url, + chainId: this.chain.id, + contracts: this.chain.contracts, + }); + } + + get actor(): string { + return this.identity.actor; + } + + get permission(): string { + return this.identity.permission; + } + + get permissionLevel(): { actor: string; permission: string } { + return { actor: this.actor, permission: this.permission }; + } + + transact>(args: { + actions: Action[]; + broadcast?: boolean; + expireSeconds?: number; + signal?: AbortSignal; + }): Promise> { + return this.client.transact({ ...args, signer: this.signer }); + } + + contract(account: string) { + return this.client.contract(account); + } + + account(name = this.actor) { + return this.client.account(name); + } } -export type SessionKitOptions = { chains: SessionChain[]; walletPlugins: WalletPlugin[]; appName?: string; storage?: SessionStorage; storageKey?: string }; +export type SessionKitOptions = { + chains: SessionChain[]; + walletPlugins: WalletPlugin[]; + appName?: string; + storage?: SessionStorage; + storageKey?: string; +}; + +type StoredSession = { + chainId: string; + walletPluginId: string; + identity: SessionIdentity; +}; + export class SessionKit { - readonly chains: readonly SessionChain[]; readonly walletPlugins: readonly WalletPlugin[]; readonly appName?: string; readonly storage: SessionStorage; readonly storageKey: string; + readonly chains: readonly SessionChain[]; + readonly walletPlugins: readonly WalletPlugin[]; + readonly appName?: string; + readonly storage: SessionStorage; + readonly storageKey: string; #session: Session | null = null; + constructor(options: SessionKitOptions) { if (!options.chains.length) throw new TypeError("SessionKit requires at least one chain"); if (!options.walletPlugins.length) throw new TypeError("SessionKit requires at least one wallet plugin"); - this.chains = options.chains; this.walletPlugins = options.walletPlugins; this.appName = options.appName; this.storage = options.storage ?? new MemorySessionStorage(); this.storageKey = options.storageKey ?? "windstack:session"; + const chains = options.chains.map(validateChain); + const chainIds = new Set(chains.map((chain) => chain.id)); + if (chainIds.size !== chains.length) throw new TypeError("SessionKit chain ids must be unique"); + const pluginIds = new Set(options.walletPlugins.map((plugin) => plugin.id)); + if (pluginIds.size !== options.walletPlugins.length || pluginIds.has("")) { + throw new TypeError("SessionKit wallet plugin ids must be non-empty and unique"); + } + this.chains = chains; + this.walletPlugins = [...options.walletPlugins]; + this.appName = options.appName; + this.storage = options.storage ?? new MemorySessionStorage(); + this.storageKey = options.storageKey ?? "windstack:session"; } - getSession(): Session | null { return this.#session; } + + getSession(): Session | null { + return this.#session; + } + async login(options: { chainId?: string; walletPluginId?: string } = {}): Promise { - const chain = options.chainId ? this.chains.find((item) => item.id === options.chainId) : this.chains[0]; - const plugin = options.walletPluginId ? this.walletPlugins.find((item) => item.id === options.walletPluginId) : this.walletPlugins[0]; - if (!chain) throw new TypeError(`Unknown chain: ${options.chainId}`); if (!plugin) throw new TypeError(`Unknown wallet plugin: ${options.walletPluginId}`); + const requestedChainId = options.chainId?.toLowerCase(); + const chain = requestedChainId + ? this.chains.find((item) => item.id === requestedChainId) + : this.chains[0]; + const plugin = options.walletPluginId + ? this.walletPlugins.find((item) => item.id === options.walletPluginId) + : this.walletPlugins[0]; + if (!chain) throw new TypeError(`Unknown chain: ${options.chainId}`); + if (!plugin) throw new TypeError(`Unknown wallet plugin: ${options.walletPluginId}`); + const result = await plugin.login({ chain, appName: this.appName }); - this.#session = new Session({ chain, identity: result.identity, walletPlugin: plugin, signer: result.signer }); - await this.storage.set(this.storageKey, JSON.stringify({ chainId: chain.id, walletPluginId: plugin.id, identity: result.identity })); - return this.#session; + const session = new Session({ + chain, + identity: validateIdentity(result.identity), + walletPlugin: plugin, + signer: result.signer, + }); + await this.storage.set( + this.storageKey, + JSON.stringify({ chainId: chain.id, walletPluginId: plugin.id, identity: session.identity }), + ); + this.#session = session; + return session; } + + async restore(): Promise { + const stored = await this.getStoredSession(); + if (!stored) return null; + const chain = this.chains.find((item) => item.id === stored.chainId.toLowerCase()); + const plugin = this.walletPlugins.find((item) => item.id === stored.walletPluginId); + if (!chain || !plugin?.restore) return null; + const result = await plugin.restore({ + chain, + appName: this.appName, + identity: stored.identity, + }); + if (!result) return null; + const session = new Session({ + chain, + identity: validateIdentity(result.identity), + walletPlugin: plugin, + signer: result.signer, + }); + this.#session = session; + return session; + } + async logout(): Promise { const session = this.#session; - if (session?.walletPlugin.logout) await session.walletPlugin.logout({ chain: session.chain, appName: this.appName, identity: session.identity }); - this.#session = null; await this.storage.remove(this.storageKey); - } - async getStoredSession(): Promise<{ chainId: string; walletPluginId: string; identity: SessionIdentity } | null> { - const raw = await this.storage.get(this.storageKey); if (!raw) return null; - const value = JSON.parse(raw) as { chainId?: unknown; walletPluginId?: unknown; identity?: unknown }; - if (typeof value.chainId !== "string" || typeof value.walletPluginId !== "string" || typeof value.identity !== "object" || !value.identity) return null; - return value as { chainId: string; walletPluginId: string; identity: SessionIdentity }; + try { + if (session?.walletPlugin.logout) { + await session.walletPlugin.logout({ + chain: session.chain, + appName: this.appName, + identity: session.identity, + }); + } + } finally { + this.#session = null; + await this.storage.remove(this.storageKey); + } + } + + async getStoredSession(): Promise { + const raw = await this.storage.get(this.storageKey); + if (!raw) return null; + try { + const value = JSON.parse(raw) as Partial; + if ( + typeof value.chainId !== "string" || + typeof value.walletPluginId !== "string" || + typeof value.identity !== "object" || + !value.identity + ) { + return null; + } + return { + chainId: value.chainId, + walletPluginId: value.walletPluginId, + identity: validateIdentity(value.identity), + }; + } catch { + return null; + } } } From 7e3d51394ab9c206d34bb1ddaa2674371dd55dc9 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:54:43 +0700 Subject: [PATCH 08/49] test(native): cover crypto ABI chain and account safety --- scripts/test-native-antelope.mjs | 216 ++++++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 18 deletions(-) diff --git a/scripts/test-native-antelope.mjs b/scripts/test-native-antelope.mjs index b8a57ad..26fd1d6 100644 --- a/scripts/test-native-antelope.mjs +++ b/scripts/test-native-antelope.mjs @@ -1,41 +1,221 @@ import assert from "node:assert/strict"; -import { AbiSerializer, bigIntToName, nameToBigInt } from "../packages/abi/dist/index.js"; -import { AntelopeClient, PrivateKeySigner } from "../packages/antelope/dist/index.js"; -import { PrivateKey, sha256Digest } from "../packages/crypto/dist/index.js"; +import { + AbiSerializer, + bigIntToName, + nameToBigInt, +} from "../packages/abi/dist/index.js"; +import { + AntelopeClient, + PrivateKeySigner, +} from "../packages/antelope/dist/index.js"; +import { + PrivateKey, + PublicKey, + sha256Digest, +} from "../packages/crypto/dist/index.js"; +import { RpcClient } from "../packages/rpc/dist/index.js"; +import { + MemorySessionStorage, + SessionKit, +} from "../packages/session/dist/index.js"; -const privateKey = PrivateKey.fromBytes("K1", Uint8Array.from({ length: 32 }, (_, index) => index === 31 ? 1 : 0)); +const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); +const privateKey = PrivateKey.fromBytes("K1", scalarOne); const digest = sha256Digest(new TextEncoder().encode("windstack-antelope-v1")); const publicKey = privateKey.toPublicKey(); const signature = privateKey.signDigest(digest); + assert.equal(signature.verifyDigest(digest, publicKey), true); assert.equal(signature.recoverDigest(digest).toString(), publicKey.toString()); +assert.equal(signature.isCanonical(), true); assert.equal(PrivateKey.fromString(privateKey.toString()).toString(), privateKey.toString()); +assert.equal(PrivateKey.fromString(privateKey.toWif()).toString(), privateKey.toString()); +assert.equal(PublicKey.fromString(publicKey.toLegacyString()).toString(), publicKey.toString()); +assert.throws(() => PrivateKey.fromBytes("K1", new Uint8Array(32)), /Invalid K1 private key/); + +const r1Key = PrivateKey.fromBytes("R1", scalarOne); +const r1Signature = r1Key.signDigest(digest); +assert.equal(r1Signature.verifyDigest(digest, r1Key.toPublicKey()), true); +assert.equal(r1Signature.recoverDigest(digest).toString(), r1Key.toPublicKey().toString()); assert.equal(bigIntToName(nameToBigInt("vex.token")), "vex.token"); +assert.throws(() => nameToBigInt("invalid-name"), /Invalid Antelope name character/); const abi = { version: "eosio::abi/1.2", - structs: [{ name: "transfer", base: "", fields: [ - { name: "from", type: "name" }, { name: "to", type: "name" }, { name: "quantity", type: "asset" }, { name: "memo", type: "string" } - ] }], - actions: [{ name: "transfer", type: "transfer" }] + structs: [ + { + name: "transfer", + base: "", + fields: [ + { name: "from", type: "name" }, + { name: "to", type: "name" }, + { name: "quantity", type: "asset" }, + { name: "memo", type: "string" }, + ], + }, + { + name: "types", + base: "", + fields: [ + { name: "signed", type: "int128" }, + { name: "unsigned", type: "uint128" }, + { name: "hash160", type: "checksum160" }, + { name: "hash512", type: "checksum512" }, + { name: "when", type: "time_point" }, + { name: "slot", type: "block_timestamp_type" }, + { name: "balance", type: "extended_asset" }, + { name: "maybe", type: "string?" }, + ], + }, + ], + actions: [ + { name: "transfer", type: "transfer" }, + { name: "types", type: "types" }, + ], + tables: [{ name: "accounts", index_type: "i64", type: "types" }], }; const serializer = new AbiSerializer(abi); -const encoded = serializer.encodeAction("transfer", { from: "alice", to: "bob", quantity: "1.0000 VEX", memo: "WindStack" }); -assert.deepEqual(serializer.decodeAction("transfer", encoded), { from: "alice", to: "bob", quantity: "1.0000 VEX", memo: "WindStack" }); +const encodedTransfer = serializer.encodeAction("transfer", { + from: "alice", + to: "bob", + quantity: "1.0000 VEX", + memo: "WindStack", +}); +assert.deepEqual(serializer.decodeAction("transfer", encodedTransfer), { + from: "alice", + to: "bob", + quantity: "1.0000 VEX", + memo: "WindStack", +}); +assert.equal(serializer.getTableType("accounts"), "types"); + +const typesValue = { + signed: "-170141183460469231731687303715884105728", + unsigned: "340282366920938463463374607431768211455", + hash160: "11".repeat(20), + hash512: "22".repeat(64), + when: "2026-09-07T00:00:00.123456Z", + slot: "2026-09-07T00:00:00.500Z", + balance: { quantity: "1.0000 VEX", contract: "vex.token" }, + maybe: null, +}; +const decodedTypes = serializer.decodeAction("types", serializer.encodeAction("types", typesValue)); +assert.equal(decodedTypes.signed, BigInt(typesValue.signed)); +assert.equal(decodedTypes.unsigned, BigInt(typesValue.unsigned)); +assert.equal(decodedTypes.hash160, typesValue.hash160); +assert.equal(decodedTypes.hash512, typesValue.hash512); +assert.equal(decodedTypes.when, typesValue.when); +assert.deepEqual(decodedTypes.balance, { quantity: "1.0000 VEX", contract: "vex.token" }); +assert.equal(decodedTypes.maybe, null); +assert.throws(() => serializer.encode("uint8", 256), /uint8/); +assert.throws(() => serializer.encode("bool", 1), /bool expects/); const chainId = "00".repeat(32); const blockId = "00".repeat(32); +let requiredKeysRequest; +let pushedTransaction; const fetchMock = async (input, init) => { const url = String(input); const body = JSON.parse(String(init?.body ?? "{}")); - if (url.endsWith("/get_info")) return Response.json({ chain_id: chainId, head_block_num: 100, last_irreversible_block_num: 99, head_block_id: blockId, head_block_time: "2026-09-07T00:00:00.000" }); - if (url.endsWith("/get_block")) return Response.json({ id: blockId, block_num: Number(body.block_num_or_id), ref_block_prefix: 123456789, timestamp: "2026-09-07T00:00:00.000" }); - if (url.endsWith("/get_required_keys")) return Response.json({ required_keys: [publicKey.toString()] }); - if (url.endsWith("/push_transaction")) return Response.json({ transaction_id: "ab".repeat(32), processed: {} }); - return new Response(JSON.stringify({ message: "not found" }), { status: 404, headers: { "content-type": "application/json" } }); + if (url.endsWith("/get_info")) { + return Response.json({ + chain_id: chainId, + head_block_num: 100, + last_irreversible_block_num: 99, + head_block_id: blockId, + head_block_time: "2026-09-07T00:00:00.000", + }); + } + if (url.endsWith("/get_block")) { + return Response.json({ + id: blockId, + block_num: Number(body.block_num_or_id), + ref_block_prefix: 123456789, + timestamp: "2026-09-06T23:50:00.000", + }); + } + if (url.endsWith("/get_abi")) return Response.json({ account_name: "vex.token", abi }); + if (url.endsWith("/get_required_keys")) { + requiredKeysRequest = body; + return Response.json({ required_keys: [publicKey.toLegacyString()] }); + } + if (url.endsWith("/push_transaction")) { + pushedTransaction = body; + return Response.json({ transaction_id: "ab".repeat(32), processed: {} }); + } + return new Response(JSON.stringify({ message: "not found" }), { + status: 404, + headers: { "content-type": "application/json" }, + }); }; -const client = new AntelopeClient({ endpoints: "https://unit.test", fetch: fetchMock }); -const result = await client.transact({ actions: [{ account: "eosio", name: "noop", authorization: [{ actor: "alice", permission: "active" }], data: "" }], signer: new PrivateKeySigner([privateKey]) }); + +const client = new AntelopeClient({ + endpoints: "https://unit.test", + fetch: fetchMock, + chainId, + contracts: { system: "vexcore", token: "vex.token" }, +}); +const account = client.account("alice"); +const transfer = await account.transfer("bob", "1.0000 VEX", "WindStack"); +assert.equal(transfer.account, "vex.token"); +const result = await client.transact({ + actions: [transfer], + signer: new PrivateKeySigner([privateKey]), +}); +assert.equal(result.transaction.expiration, "2026-09-07T00:02:00.000Z".replace(".000Z", "")); assert.equal(result.signatures.length, 1); assert.equal(result.response.transaction_id.length, 64); -console.log("native Antelope SDK tests passed"); +assert.match(requiredKeysRequest.available_keys[0], /^EOS/); +assert.equal(pushedTransaction.compression, 0); +assert.ok(pushedTransaction.packed_trx.length > 0); + +const mismatchedClient = new AntelopeClient({ + endpoints: "https://unit.test", + fetch: fetchMock, + chainId: "11".repeat(32), +}); +await assert.rejects( + () => + mismatchedClient.transact({ + actions: [transfer], + signer: new PrivateKeySigner([privateKey]), + }), + /RPC chain mismatch/, +); + +let rpcAttempts = 0; +const rpc = new RpcClient({ + endpoints: ["https://a.test", "https://b.test"], + fetch: async () => { + rpcAttempts += 1; + return new Response(JSON.stringify({ message: "invalid request" }), { + status: 400, + headers: { "content-type": "application/json" }, + }); + }, +}); +await assert.rejects(() => rpc.getInfo(), /invalid request/); +assert.equal(rpcAttempts, 1); + +const storage = new MemorySessionStorage(); +await storage.set("windstack:session", "{broken-json"); +const walletPlugin = { + id: "test-wallet", + async login() { + return { identity: { actor: "alice", permission: "active" }, signer: new PrivateKeySigner([privateKey]) }; + }, +}; +const kit = new SessionKit({ + chains: [ + { + id: chainId, + url: "https://unit.test", + contracts: { system: "vexcore", token: "vex.token" }, + }, + ], + walletPlugins: [walletPlugin], + storage, +}); +assert.equal(await kit.getStoredSession(), null); + +console.log("WindStack Antelope validation passed"); From cd9c0cbdf0aca18ae5471a4b5308b38129d3701d Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:55:57 +0700 Subject: [PATCH 09/49] chore(release): add native package release guard --- scripts/check-release.mjs | 108 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 scripts/check-release.mjs diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs new file mode 100644 index 0000000..ea9d80e --- /dev/null +++ b/scripts/check-release.mjs @@ -0,0 +1,108 @@ +import assert from "node:assert/strict"; +import { readdir, readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const nativePackages = ["crypto", "abi", "rpc", "contract", "account", "antelope", "session"]; +const forbiddenDependencies = ["elliptic", "bn.js", "crypto-browserify"]; +const forbiddenMarkdown = [ + { pattern: /\bAI\b/i, label: "AI wording" }, + { pattern: /\bengineering\b/i, label: "engineering wording" }, + { pattern: /\btechnical\b/i, label: "technical wording" }, + { pattern: /\bteknis\b/i, label: "teknis wording" }, + { pattern: /Publish native packages from VPS/i, label: "deployment note" }, + { pattern: /npm whoami/i, label: "npm authentication note" }, + { pattern: /release:npm/i, label: "release command" }, + { pattern: /not (?:a )?WharfKit fork/i, label: "implementation comparison" }, +]; +const requiredReadmeSections = ["## Overview", "## Installation", "## Usage", "## Runtime", "## License"]; +const header = "Created by Gilang Ramadan"; + +async function readJson(relativePath) { + return JSON.parse(await readFile(path.join(root, relativePath), "utf8")); +} + +async function walk(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + if (["node_modules", "dist", ".git"].includes(entry.name)) continue; + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...(await walk(fullPath))); + else files.push(fullPath); + } + return files; +} + +const rootPackage = await readJson("package.json"); +assert.equal(rootPackage.private, true, "Root package must remain private"); +assert.match(rootPackage.version, /^\d+\.\d+\.\d+$/, "Root version must be semantic"); + +const manifests = new Map(); +for (const packageDirectory of nativePackages) { + const manifest = await readJson(`packages/${packageDirectory}/package.json`); + manifests.set(manifest.name, manifest); + assert.equal(manifest.version, rootPackage.version, `${manifest.name} must use the root release version`); + assert.equal(manifest.author, "Gilang Ramadan", `${manifest.name} author must be Gilang Ramadan`); + assert.equal(manifest.license, "MIT", `${manifest.name} must use MIT`); + assert.equal(manifest.publishConfig?.access, "public", `${manifest.name} must publish as public`); + assert.equal(manifest.sideEffects, false, `${manifest.name} must declare sideEffects=false`); + assert.ok(Array.isArray(manifest.files) && manifest.files.includes("dist"), `${manifest.name} must publish dist`); + assert.ok(manifest.files.includes("README.md") && manifest.files.includes("LICENSE"), `${manifest.name} must publish documentation and license`); + + const readme = await readFile(path.join(root, `packages/${packageDirectory}/README.md`), "utf8"); + assert.ok(readme.startsWith(`# ${manifest.name}\n`), `${manifest.name} README title is invalid`); + for (const section of requiredReadmeSections) { + assert.ok(readme.includes(section), `${manifest.name} README is missing ${section}`); + } + assert.ok(readme.includes(header), `${manifest.name} README must credit Gilang Ramadan`); +} + +for (const [name, manifest] of manifests) { + for (const [dependency, version] of Object.entries(manifest.dependencies ?? {})) { + assert.ok(!dependency.startsWith("@wharfkit/"), `${name} cannot depend on ${dependency}`); + assert.ok(!forbiddenDependencies.includes(dependency), `${name} cannot depend on ${dependency}`); + if (manifests.has(dependency)) { + assert.equal(version, rootPackage.version, `${name} must pin ${dependency} to ${rootPackage.version}`); + } + } +} + +const sourceChecks = [ + "packages/crypto/src/index.ts", + "packages/abi/src/index.ts", + "packages/rpc/src/index.ts", + "packages/contract/src/index.ts", + "packages/account/src/index.ts", + "packages/antelope/src/index.ts", + "packages/session/src/index.ts", + "packages/session/src/native.ts", + "packages/session/src/compat.ts", +]; +for (const relativePath of sourceChecks) { + const source = await readFile(path.join(root, relativePath), "utf8"); + assert.ok(source.includes(header), `${relativePath} must include creator attribution`); +} + +const markdownFiles = (await walk(root)).filter((file) => file.endsWith(".md")); +for (const file of markdownFiles) { + const content = await readFile(file, "utf8"); + for (const { pattern, label } of forbiddenMarkdown) { + assert.ok(!pattern.test(content), `${path.relative(root, file)} contains ${label}`); + } +} + +const lock = await readJson("package-lock.json"); +assert.equal(lock.version, rootPackage.version, "package-lock root version must match package.json"); +assert.equal(lock.packages?.[""]?.version, rootPackage.version, "package-lock root package version is stale"); +for (const packageDirectory of nativePackages) { + const manifest = await readJson(`packages/${packageDirectory}/package.json`); + assert.equal( + lock.packages?.[`packages/${packageDirectory}`]?.version, + manifest.version, + `package-lock entry for ${manifest.name} is stale`, + ); +} + +console.log(`Release guard passed for WindStack ${rootPackage.version}`); From 2c5d56c18ea40d89338c49bb42b6b26be3dac42d Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:56:13 +0700 Subject: [PATCH 10/49] chore(release): add resumable native package publisher --- scripts/publish-native.mjs | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 scripts/publish-native.mjs diff --git a/scripts/publish-native.mjs b/scripts/publish-native.mjs new file mode 100644 index 0000000..ba6c925 --- /dev/null +++ b/scripts/publish-native.mjs @@ -0,0 +1,69 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const packageDirectories = ["crypto", "abi", "rpc", "contract", "account", "antelope", "session"]; +const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: root, + encoding: "utf8", + stdio: options.capture ? "pipe" : "inherit", + }); + if (result.status !== 0 && !options.allowFailure) { + throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}`); + } + return result; +} + +function requireCleanMain() { + const branch = execFileSync("git", ["branch", "--show-current"], { cwd: root, encoding: "utf8" }).trim(); + if (branch !== "main") throw new Error(`Release must run from main; current branch is ${branch || "detached"}`); + const status = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8" }).trim(); + if (status) throw new Error("Release requires a clean working tree"); +} + +function publishedVersion(name, version) { + const result = run("npm", ["view", `${name}@${version}`, "version", "--json"], { + capture: true, + allowFailure: true, + }); + if (result.status === 0) { + const parsed = JSON.parse(result.stdout || "null"); + return parsed === version; + } + const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`; + if (/E404|404 Not Found|No match found for version/i.test(output)) return false; + throw new Error(`Unable to query npm for ${name}@${version}: ${output.trim()}`); +} + +requireCleanMain(); +run("npm", ["whoami"]); +run("npm", ["run", "release:dry-run"]); + +for (const directory of packageDirectories) { + const manifest = JSON.parse(await readFile(path.join(root, "packages", directory, "package.json"), "utf8")); + const { name, version } = manifest; + if (publishedVersion(name, version)) { + console.log(`${name}@${version} is already published; skipping.`); + continue; + } + + console.log(`Publishing ${name}@${version}...`); + run("npm", ["publish", "--workspace", name, "--access", "public"]); + + let verified = false; + for (let attempt = 0; attempt < 6; attempt += 1) { + if (publishedVersion(name, version)) { + verified = true; + break; + } + await sleep(1500); + } + if (!verified) throw new Error(`npm did not confirm ${name}@${version} after publish`); +} + +console.log("All WindStack native packages are published and verified."); From d873e9f800e8399e74a313e51d874a69eafada0c Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:56:20 +0700 Subject: [PATCH 11/49] chore(style): add Biome formatter configuration --- biome.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..9584808 --- /dev/null +++ b/biome.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.12/schema.json", + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "lineEnding": "lf" + }, + "linter": { + "enabled": false + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always", + "trailingCommas": "all" + } + } +} From 76ad3a63d298c01cac309cd709395b20031e5347 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:56:37 +0700 Subject: [PATCH 12/49] chore(release): enforce formatting and release validation --- package.json | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ac914d9..af29ae2 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "workspaces": ["packages/*"], "scripts": { "clean": "rm -rf packages/*/dist packages/*/*.tsbuildinfo", + "format": "biome format --write packages/*/src scripts package.json package-lock.json tsconfig.json tsconfig.base.json packages/*/package.json packages/*/tsconfig.json specs/wisp-provider-contract.json biome.json", + "format:check": "biome format packages/*/src scripts package.json package-lock.json tsconfig.json tsconfig.base.json packages/*/package.json packages/*/tsconfig.json specs/wisp-provider-contract.json biome.json", + "check:release": "node scripts/check-release.mjs", "build:native": "tsc -b packages/crypto packages/abi packages/rpc packages/contract packages/account packages/antelope packages/session", "build": "tsc -b packages/core packages/evm packages/solana packages/crypto packages/abi packages/rpc packages/contract packages/account packages/antelope packages/vexanium packages/wallet-plugin-wisp packages/session", "typecheck": "npm run build", @@ -13,18 +16,28 @@ "pack:dry-run": "npm pack --dry-run --workspaces", "test": "node scripts/test-signing.mjs && node scripts/test-provider-contract.mjs && node scripts/test-provider-spec.mjs && node scripts/test-sdk-behavior.mjs && node scripts/test-native-antelope.mjs", "test:native": "node scripts/test-native-antelope.mjs", - "validate:native": "npm run clean && npm run build:native && npm run test:native && npm run pack:native", - "validate": "npm run clean && npm run build && npm test && npm run pack:dry-run", + "validate:native": "npm run clean && npm run format:check && npm run check:release && npm run build:native && npm run test:native && npm run pack:native", + "validate": "npm run clean && npm run format:check && npm run check:release && npm run build && npm test && npm run pack:dry-run", "release:dry-run": "npm run validate:native", - "release:npm": "npm publish -w @windstack/crypto --access public && npm publish -w @windstack/abi --access public && npm publish -w @windstack/rpc --access public && npm publish -w @windstack/contract --access public && npm publish -w @windstack/account --access public && npm publish -w @windstack/antelope --access public && npm publish -w @windstack/session --access public" + "release:npm": "node scripts/publish-native.mjs" + }, + "devDependencies": { + "@biomejs/biome": "2.5.12", + "typescript": "^7.0.2" + }, + "engines": { + "node": ">=20.19.0" }, - "devDependencies": { "typescript": "^7.0.2" }, - "engines": { "node": ">=20.19.0" }, "license": "MIT", "author": "Gilang Ramadan", "contributors": ["PT WIND KRIPTOGRAFI TEKNOLOGI"], - "description": "Modern TypeScript SDK packages for WindStack, Wisp Wallet, Vexanium and Antelope applications.", + "description": "TypeScript SDK packages for WindStack applications across Antelope, Vexanium, EVM, Solana, and Wisp Wallet.", "homepage": "https://github.com/windvex/windstack-sdk#readme", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git" }, - "bugs": { "url": "https://github.com/windvex/windstack-sdk/issues" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git" + }, + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + } } From f16d115641cc1f6641605331dca086531d3fad66 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:57:38 +0700 Subject: [PATCH 13/49] docs: publish professional WindStack SDK overview --- README.md | 97 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 56 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 37f6e49..9b15ded 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,81 @@ # WindStack SDK -Modern TypeScript SDKs for Antelope/Vexanium, Wisp Wallet, EVM, and Solana. +WindStack provides TypeScript packages for Antelope applications, Vexanium, Wisp Wallet, EVM providers, and Solana providers. -**Created by Gilang Ramadan** · Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI · MIT. +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. -## Native Antelope v1 +## Overview -The new Antelope stack is built from protocol specifications and Web-standard APIs; it is **not a WharfKit fork**. +The Antelope packages are separated by responsibility so applications can install only the capabilities they need. | Package | Purpose | | --- | --- | -| `@windstack/crypto` | K1/R1 private keys, public keys, recoverable signatures, Antelope encodings | -| `@windstack/abi` | ABI binary codec, names/assets/symbols, structs, aliases, variants | -| `@windstack/rpc` | Typed nodeos RPC with timeout/failover/AbortSignal | -| `@windstack/contract` | ABI-aware actions, tables and ABI cache | -| `@windstack/account` | Account, token balance and system-action helpers | -| `@windstack/antelope` | TAPOS, transaction serialization, digest, signing, broadcast, unified client | -| `@windstack/session` | Native SessionKit-style wallet/session orchestration | +| `@windstack/crypto` | K1 and R1 keys, Antelope key formats, signatures, verification, and public-key recovery | +| `@windstack/abi` | ABI serialization and deserialization for Antelope values, actions, tables, structs, variants, and binary extensions | +| `@windstack/rpc` | Typed Antelope chain RPC with endpoint failover, request timeouts, cancellation, and structured errors | +| `@windstack/contract` | Contract ABI loading, action serialization, table queries, and shared ABI caching | +| `@windstack/account` | Account queries, token transfers, staking actions, RAM actions, and configurable chain contracts | +| `@windstack/antelope` | Transaction construction, TAPOS, signing digests, required-key resolution, signing, and broadcast | +| `@windstack/session` | Wallet plugins, authenticated Antelope sessions, persistence, restore, and transaction orchestration | -The v1 native package graph does not depend on `@wharfkit/*`, `elliptic`, `bn.js`, `crypto-browserify`, or Node Buffer APIs. Crypto primitives use current Noble packages (`@noble/curves` and `@noble/hashes`). Noble v2 is ESM-only, so Node.js **20.19+** is required when running directly on Node. +Additional packages provide Wisp provider interfaces and chain-specific helpers for Vexanium, EVM, and Solana applications. + +## Installation + +Install the high-level Antelope client and session package: ```bash npm install @windstack/antelope @windstack/session ``` -```ts -import { AntelopeClient } from "@windstack/antelope"; - -const client = new AntelopeClient({ endpoints: ["https://api.windcrypto.com"] }); -const token = client.contract("vex.token"); -const action = await token.action("transfer", { - from: "alice", - to: "bob", - quantity: "1.0000 VEX", - memo: "WindStack", -}, ["alice@active"]); -``` - -## Existing packages +Individual packages can also be installed independently. -`@windstack/core`, `@windstack/evm`, `@windstack/solana`, `@windstack/vexanium`, and `@windstack/wallet-plugin-wisp` remain in this monorepo for compatibility. The legacy Vexanium/WharfKit integration is not a dependency of the seven native Antelope v1 packages above. +## Usage -## Development +The example below configures Vexanium Mainnet explicitly, including its chain ID and system contracts. -```bash -npm install -npm run validate:native -npm run validate +```ts +import { + AntelopeClient, + PrivateKey, + PrivateKeySigner, +} from "@windstack/antelope"; + +const client = new AntelopeClient({ + endpoints: ["https://api.windcrypto.com"], + chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", + contracts: { + system: "vexcore", + token: "vex.token", + }, +}); + +const signer = new PrivateKeySigner([ + PrivateKey.fromString("PVT_K1_..."), +]); + +const transfer = await client + .account("alice") + .transfer("bob", "1.0000 VEX", "WindStack"); + +const result = await client.transact({ + actions: [transfer], + signer, +}); + +console.log(result.response); ``` -## Publish native packages from VPS +Applications should keep private keys in an appropriate secure storage or signing service. A wallet integration can provide its own `Signer` implementation instead of exposing private keys to application code. -Authenticate to npm first (`npm whoami`). Then: +## Runtime -```bash -npm install -npm run release:dry-run -npm run release:npm -``` +The Antelope packages are ESM-first and use Web-standard primitives such as `Uint8Array`, `TextEncoder`, `fetch`, `AbortController`, and secure platform randomness. Node.js 20.19 or newer is supported. Browser and React Native environments must provide the Web APIs used by the selected package. -`release:npm` publishes only the seven native packages, in dependency order. +K1 and R1 cryptographic operations are provided by the Noble libraries. The Antelope package graph does not depend on `elliptic`, `bn.js`, or Node.js crypto polyfills. ## License -MIT. Created by Gilang Ramadan; copyright PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From b5b58ce9d5220432cd47e9482e043ad18d477e5c Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:57:52 +0700 Subject: [PATCH 14/49] docs(crypto): document public API and runtime --- packages/crypto/README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/crypto/README.md b/packages/crypto/README.md index 46068c4..ebaed54 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -1,20 +1,42 @@ # @windstack/crypto -Modern Antelope K1/R1 key and signature primitives for WindStack. Built from the Antelope key formats and Noble cryptography; no `elliptic`, `bn.js`, or Node crypto polyfill. +## Overview -Created by **Gilang Ramadan**. +`@windstack/crypto` provides Antelope-compatible K1 and R1 key handling for WindStack applications. It supports private keys, public keys, recoverable signatures, signature verification, public-key recovery, modern Antelope encodings, legacy EOS public keys, and legacy K1 WIF private keys. + +The package uses Noble curve and hash primitives while keeping the public API based on `Uint8Array` and Antelope key formats. + +## Installation ```bash npm install @windstack/crypto ``` +## Usage + ```ts import { PrivateKey, sha256Digest } from "@windstack/crypto"; -const key = PrivateKey.generate("K1"); -const digest = sha256Digest(new TextEncoder().encode("hello")); -const signature = key.signDigest(digest); -console.log(signature.verifyDigest(digest, key.toPublicKey())); +const privateKey = PrivateKey.fromString("PVT_K1_..."); +const publicKey = privateKey.toPublicKey(); +const digest = sha256Digest(new TextEncoder().encode("WindStack")); +const signature = privateKey.signDigest(digest); + +console.log(signature.toString()); +console.log(signature.verifyDigest(digest, publicKey)); +console.log(signature.recoverDigest(digest).equals(publicKey)); ``` -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +K1 signatures are emitted in the canonical compact form required by Antelope-compatible chains. K1 public keys can also be converted to the legacy `EOS...` representation when interacting with older node software. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Browser and React Native environments must provide the secure randomness required by key generation. Existing keys can be imported with `PrivateKey.fromString()` or `PrivateKey.fromBytes()`. + +Private keys should be stored and used through an appropriate secure storage or signing boundary. Application logs should never contain private-key material. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 7ffd203d0ca8efffeb529b0023f9f572c78ea3be Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:58:07 +0700 Subject: [PATCH 15/49] docs(abi): document supported Antelope ABI types --- packages/abi/README.md | 59 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/abi/README.md b/packages/abi/README.md index df27806..844653c 100644 --- a/packages/abi/README.md +++ b/packages/abi/README.md @@ -1,7 +1,60 @@ # @windstack/abi -Pure TypeScript Antelope ABI encoder/decoder for browser, Node.js, and React Native. Supports aliases, structs/inheritance, arrays, optionals, binary extensions, variants, names, assets, symbols, public keys, signatures, checksums, and standard integer/string/bytes primitives. +## Overview -Created by **Gilang Ramadan**. +`@windstack/abi` serializes and deserializes Antelope ABI values without requiring Node.js buffer APIs. It supports ABI aliases, structs and inheritance, arrays, optional values, binary extensions, variants, action types, table types, names, assets, symbols, keys, signatures, timestamps, checksums, integer types, floating-point values, and raw bytes. -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +The package also exposes `BinaryWriter`, `BinaryReader`, `nameToBigInt()`, `bigIntToName()`, `hexToBytes()`, and `bytesToHex()` for applications that need direct access to Antelope binary primitives. + +## Installation + +```bash +npm install @windstack/abi +``` + +## Usage + +```ts +import { AbiSerializer } from "@windstack/abi"; + +const abi = { + version: "eosio::abi/1.2", + structs: [ + { + name: "transfer", + base: "", + fields: [ + { name: "from", type: "name" }, + { name: "to", type: "name" }, + { name: "quantity", type: "asset" }, + { name: "memo", type: "string" }, + ], + }, + ], + actions: [{ name: "transfer", type: "transfer" }], +}; + +const serializer = new AbiSerializer(abi); +const bytes = serializer.encodeAction("transfer", { + from: "alice", + to: "bob", + quantity: "1.0000 VEX", + memo: "WindStack", +}); + +const decoded = serializer.decodeAction("transfer", bytes); +``` + +Numeric values are range-checked before encoding. Fixed-size checksums, public keys, signatures, names, symbols, and optional markers are validated before they are accepted. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses `Uint8Array`, `DataView`, `TextEncoder`, and `TextDecoder`, making the serializer suitable for browser and React Native environments that provide these Web APIs. + +`float128` values are represented as their exact 16-byte binary form because JavaScript does not provide a native IEEE-754 binary128 number type. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 3b04ea012a895aa75f0668cbe52299c9a699211e Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:58:20 +0700 Subject: [PATCH 16/49] docs(rpc): document RPC behavior and errors --- packages/rpc/README.md | 46 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/rpc/README.md b/packages/rpc/README.md index 8811a35..fc3a44a 100644 --- a/packages/rpc/README.md +++ b/packages/rpc/README.md @@ -1,7 +1,47 @@ # @windstack/rpc -Typed Antelope `/v1/chain/*` client with endpoint failover, timeout, AbortSignal, injected `fetch`, and structured RPC errors. Uses Web APIs only. +## Overview -Created by **Gilang Ramadan**. +`@windstack/rpc` provides a typed client for Antelope chain RPC endpoints. It supports multiple endpoints, automatic failover for retriable read failures, request timeouts, `AbortSignal` cancellation, injected `fetch`, structured RPC errors, table queries, account queries, ABI queries, currency queries, required-key resolution, and transaction broadcast. -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +Broadcast requests are not automatically retried because a transaction may already have reached the chain even when the original network response is lost. + +## Installation + +```bash +npm install @windstack/rpc +``` + +## Usage + +```ts +import { RpcClient } from "@windstack/rpc"; + +const rpc = new RpcClient({ + endpoints: [ + "https://api.windcrypto.com", + "https://backup.example", + ], + timeoutMs: 10_000, +}); + +const info = await rpc.getInfo(); +const account = await rpc.getAccount("alice"); +const rows = await rpc.getTableRows({ + code: "vex.token", + scope: "alice", + table: "accounts", +}); +``` + +HTTP request errors are exposed through `RpcError`. Timeouts use `RpcTimeoutError`. Invalid chain requests are returned immediately instead of being retried across every configured endpoint. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses the standard `fetch`, `Response`, `AbortController`, and `AbortSignal` interfaces. A compatible `fetch` implementation can be supplied through the constructor when the runtime does not provide one globally. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 9d49c8b519e8b8fbc5d8bf87b4f29b627fc26096 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:58:32 +0700 Subject: [PATCH 17/49] docs(contract): document contract client and ABI cache --- packages/contract/README.md | 48 ++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/contract/README.md b/packages/contract/README.md index d2a425f..816d723 100644 --- a/packages/contract/README.md +++ b/packages/contract/README.md @@ -1,7 +1,49 @@ # @windstack/contract -ABI-aware contract actions and table queries with a TTL ABI cache. +## Overview -Created by **Gilang Ramadan**. +`@windstack/contract` provides ABI-aware contract access for Antelope applications. It loads contract ABIs, caches them with a configurable TTL, serializes action data, validates account/action/authorization names, and exposes table-row queries through the shared RPC client. -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +`ContractKit` can be used when several contracts should share the same RPC connection and ABI cache. + +## Installation + +```bash +npm install @windstack/contract @windstack/rpc +``` + +## Usage + +```ts +import { ContractKit } from "@windstack/contract"; +import { RpcClient } from "@windstack/rpc"; + +const rpc = new RpcClient({ endpoints: "https://api.windcrypto.com" }); +const contracts = new ContractKit(rpc); +const token = await contracts.load("vex.token"); + +const action = await token.action( + "transfer", + { + from: "alice", + to: "bob", + quantity: "1.0000 VEX", + memo: "WindStack", + }, + ["alice@active"], +); + +const rows = await token.tableRows("accounts", "alice"); +``` + +Concurrent ABI reads for the same `Contract` instance share one in-flight request. Cached ABIs can be refreshed explicitly or removed through `AbiCache` when an application knows a contract has changed. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Runtime network behavior is provided by `@windstack/rpc`, while action encoding is provided by `@windstack/abi`. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 1c1a2c64492d35d7a221642c767cc0e728cd542d Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:58:45 +0700 Subject: [PATCH 18/49] docs(account): document chain-configured account helpers --- packages/account/README.md | 45 +++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/packages/account/README.md b/packages/account/README.md index b9b3e19..2614320 100644 --- a/packages/account/README.md +++ b/packages/account/README.md @@ -1,7 +1,46 @@ # @windstack/account -Account reads, token balances, transfer builders, and `eosio` system-action helpers. +## Overview -Created by **Gilang Ramadan**. +`@windstack/account` provides account reads and common Antelope account actions. It supports account queries, token balances, token transfers, CPU/NET staking actions, RAM purchases, RAM sales, and refund action construction. -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +System and token contract names are configured explicitly so the same package can be used safely across Antelope chains with different system account names. + +## Installation + +```bash +npm install @windstack/account +``` + +## Usage + +The account helper is normally created by `@windstack/antelope`, which passes the chain contract configuration automatically. + +```ts +import { AntelopeClient } from "@windstack/antelope"; + +const client = new AntelopeClient({ + endpoints: "https://api.windcrypto.com", + contracts: { + system: "vexcore", + token: "vex.token", + }, +}); + +const account = client.account("alice"); +const balances = await account.balance(undefined, "VEX"); +const transfer = await account.transfer("bob", "1.0000 VEX", "WindStack"); +const stake = await account.delegate("alice", "1.0000 VEX", "2.0000 VEX"); +``` + +If a token or system contract has not been configured, helpers that depend on it fail before building an action. Applications can also pass a token contract directly for individual token operations. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Network requests are delegated to `@windstack/rpc`, and action serialization is delegated to `@windstack/contract` and `@windstack/abi`. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 39172fb53b3ff50060cb68cc0b207efeadcfc4ca Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:58:59 +0700 Subject: [PATCH 19/49] docs(antelope): document transaction client and signer contract --- packages/antelope/README.md | 62 +++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/antelope/README.md b/packages/antelope/README.md index 3d21d03..0c76f11 100644 --- a/packages/antelope/README.md +++ b/packages/antelope/README.md @@ -1,19 +1,61 @@ # @windstack/antelope -The WindStack native Antelope SDK. It builds TAPOS transactions, serializes canonical Antelope transaction bytes, calculates signing digests, resolves required keys, signs through a pluggable signer, broadcasts to nodeos, and exposes RPC/contract/account helpers. +## Overview -It is built from Antelope protocol formats, not cloned from WharfKit. The new dependency graph has no `@wharfkit/*`, `elliptic`, or `bn.js`. +`@windstack/antelope` is the high-level transaction client for WindStack Antelope applications. It combines RPC access, ABI serialization, contract access, account helpers, TAPOS construction, transaction serialization, signing-digest calculation, required-key resolution, pluggable signers, and transaction broadcast. -Created by **Gilang Ramadan**. +A client can be bound to an expected chain ID. When configured, signing stops if the RPC endpoint reports a different chain, preventing transactions from being signed against an unintended network. + +## Installation + +```bash +npm install @windstack/antelope +``` + +## Usage ```ts -import { AntelopeClient, PrivateKey, PrivateKeySigner } from "@windstack/antelope"; +import { + AntelopeClient, + PrivateKey, + PrivateKeySigner, +} from "@windstack/antelope"; + +const client = new AntelopeClient({ + endpoints: ["https://api.windcrypto.com"], + chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", + contracts: { + system: "vexcore", + token: "vex.token", + }, +}); + +const signer = new PrivateKeySigner([ + PrivateKey.fromString("PVT_K1_..."), +]); + +const transfer = await client + .account("alice") + .transfer("bob", "1.0000 VEX", "WindStack"); -const client = new AntelopeClient({ endpoints: "https://api.example" }); -const signer = new PrivateKeySigner([PrivateKey.fromString("PVT_K1_...")]); -const token = client.contract("vex.token"); -const action = await token.action("transfer", { from: "alice", to: "bob", quantity: "1.0000 VEX", memo: "" }, ["alice@active"]); -await client.transact({ actions: [action], signer }); +const result = await client.transact({ + actions: [transfer], + signer, +}); ``` -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +`PrivateKeySigner` is intended for environments where application-managed keys are appropriate. Wallets, hardware signers, remote signers, and secure-storage integrations can implement the exported `Signer` interface and receive the chain ID, transaction, serialized bytes, digest, and required keys in one signing request. + +K1 available keys use the legacy `EOS...` representation by default for broad compatibility with older Antelope node software. Applications that require modern K1 public-key strings can set `k1PublicKeyFormat: "modern"` on `PrivateKeySigner`. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses Web-standard byte and networking APIs and does not require Node.js `Buffer` for the Antelope transaction path. + +The high-level client accepts one or more RPC endpoints. Transaction broadcast is intentionally not retried automatically when the response is uncertain. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 614212272a8b3f4e0b3453e6beb6eb5ace6d09fb Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:59:13 +0700 Subject: [PATCH 20/49] docs(session): document wallet sessions and restore --- packages/session/README.md | 55 +++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/session/README.md b/packages/session/README.md index cfdf23f..0c218e8 100644 --- a/packages/session/README.md +++ b/packages/session/README.md @@ -1,15 +1,58 @@ # @windstack/session -WindStack native Antelope session orchestration. It provides a small SessionKit-style abstraction over `@windstack/antelope` without WharfKit. +## Overview -Created by **Gilang Ramadan**. +`@windstack/session` provides wallet-session orchestration for Antelope applications. It connects wallet plugins to `@windstack/antelope`, binds sessions to a full chain ID, persists session identity, supports wallet-assisted restore, exposes account and contract helpers, and routes transactions through the signer returned by the selected wallet plugin. + +Applications can provide their own storage adapter for browser storage, React Native secure storage, database-backed sessions, or another persistence layer. + +## Installation + +```bash +npm install @windstack/session +``` + +## Usage ```ts import { SessionKit } from "@windstack/session"; -const kit = new SessionKit({ chains: [{ id: "...", url: "https://api.example" }], walletPlugins: [myWalletPlugin] }); -const session = await kit.login(); -await session.transact({ actions: [action] }); +const sessionKit = new SessionKit({ + appName: "My Vexanium App", + chains: [ + { + id: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", + url: "https://api.windcrypto.com", + contracts: { + system: "vexcore", + token: "vex.token", + }, + }, + ], + walletPlugins: [myWalletPlugin], + storage: mySessionStorage, +}); + +const session = await sessionKit.login(); +const transfer = await session + .account() + .transfer("bob", "1.0000 VEX", "WindStack"); + +await session.transact({ actions: [transfer] }); ``` -MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. +A wallet plugin implements `login()` and returns a validated identity plus an Antelope `Signer`. Plugins can optionally implement `restore()` and `logout()` to reconnect an existing wallet session and release wallet-side state. + +Stored session data contains identity and routing information, not private keys. Invalid or malformed stored data is ignored instead of being treated as a valid session. + +## Runtime + +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Browser and React Native applications can provide storage implementations appropriate for their security model. + +Each session constructs an Antelope client bound to the configured chain ID, so an endpoint that reports another chain is rejected before signing. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 432b6e97795186908529e13c6455f076c948ba45 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:59:31 +0700 Subject: [PATCH 21/49] docs(core): standardize public package documentation --- packages/core/README.md | 58 ++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/packages/core/README.md b/packages/core/README.md index d087097..1e5e105 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,57 +1,49 @@ # @windstack/core -Shared provider types and browser-safe utilities for the Wind Stack SDK. +## Overview -```bash -npm install @windstack/core -``` +`@windstack/core` contains shared provider contracts, error types, event utilities, browser helpers, and application metadata utilities used across WindStack wallet-facing packages. -## Provider contract +The exported `WISP_PROVIDER_CONTRACT` keeps provider identity, error codes, Vexanium provider identifiers, EVM chain identifiers, method names, capabilities, and discovery events consistent across packages. -`WISP_PROVIDER_CONTRACT` is the runtime single source used by the WindStack EVM -and Vexanium packages for provider identity, method names, discovery events, -chain identifiers, protocol version, capabilities, and provider error codes. +## Installation -```ts -import { WISP_PROVIDER_CONTRACT } from "@windstack/core"; - -console.log(WISP_PROVIDER_CONTRACT.vex.standard); // VexaniumProvider -console.log(WISP_PROVIDER_CONTRACT.evm.chainIdHex); // 0x1a50 -console.log(WISP_PROVIDER_CONTRACT.errors.userRejected); // 4001 +```bash +npm install @windstack/core ``` -The machine-readable repository specification at -`specs/wisp-provider-contract.json` is regression-tested against this export. -Wisp Wallet synchronizes its provider runtime from that specification. - -## dApp metadata - -`resolveDappMetadata()` combines explicit values with the current document title, description, URL, and icons. URLs are restricted to safe web/image schemes. +## Usage ```ts -import { resolveDappMetadata } from "@windstack/core"; +import { + WISP_PROVIDER_CONTRACT, + WispEventEmitter, + normalizeProviderError, + resolveDappMetadata, +} from "@windstack/core"; const metadata = resolveDappMetadata({ name: "My App", url: "https://app.example", icon: "https://app.example/icon.png", }); + +console.log(WISP_PROVIDER_CONTRACT.vex.standard); +console.log(WISP_PROVIDER_CONTRACT.evm.chainIdHex); ``` -Metadata is only for display. A wallet must derive the trusted origin from its transport, such as the browser extension sender, rather than accepting an origin supplied by a dApp. +`resolveDappMetadata()` combines explicit application metadata with safe values available from the current document. Provider error normalization preserves numeric wallet error codes, while `WispEventEmitter` supplies typed listener registration and cleanup for provider clients. -## Errors and events +## Security -```ts -import { - WispEventEmitter, - WispProviderError, - normalizeProviderError, -} from "@windstack/core"; -``` +Application metadata is display information only. Wallet permission state should be bound to an authoritative transport origin, such as the browser extension sender origin, instead of trusting an origin supplied by application content. -`WISP_ERROR_CODES` is derived from the provider contract. `normalizeProviderError()` preserves numeric provider error codes. `WispEventEmitter` provides typed `on`, `off`, `once`, and listener cleanup methods for SDK packages. +## Runtime + +The package is browser-safe and contains no chain-signing implementation. It is designed to be shared by provider clients without requiring a blockchain runtime or private-key dependency. ## License -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From b381f7d2d731d741af5c0b0fa4b74bbf61e2af84 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 07:59:42 +0700 Subject: [PATCH 22/49] docs(evm): standardize provider client documentation --- packages/evm/README.md | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/evm/README.md b/packages/evm/README.md index 972fd6b..3e6702e 100644 --- a/packages/evm/README.md +++ b/packages/evm/README.md @@ -1,11 +1,19 @@ # @windstack/evm -Small EIP-1193 client with EIP-6963 discovery for browser wallets. +## Overview + +`@windstack/evm` provides a browser client for EIP-1193 wallet providers with EIP-6963 provider discovery. It supports account access, chain queries, chain switching, chain registration, request forwarding, provider events, and VEX EVM network identifiers. + +The client prefers announced EIP-6963 providers and can use `window.ethereum` when no announced provider is available. + +## Installation ```bash npm install @windstack/core @windstack/evm ``` +## Usage + ```ts import { createEVMClient } from "@windstack/evm"; @@ -14,30 +22,37 @@ const accounts = await client.connect(); const chainId = await client.getChainId(); client.on("accountsChanged", (nextAccounts) => { - // Update application state. + console.log(nextAccounts); }); -``` -`connect()` calls `eth_requestAccounts`. `getAccounts()` calls the silent `eth_accounts` method. -`EVM_METHODS` exposes the canonical MetaMask-compatible method names implemented by Wisp, while `VEX_EVM_CHAIN_ID`, `VEX_EVM_CHAIN_ID_HEX`, and `VEX_EVM_SCOPE` expose the VEX EVM network identifiers. +await client.switchChain("0x1a50"); +``` -## Chain requests +VEX EVM can be registered with standard EIP-3085 chain metadata: ```ts -await client.switchChain("0x1a50"); - await client.addChain({ chainId: "0x1a50", chainName: "VEX EVM", - nativeCurrency: { name: "Vexanium", symbol: "VEX", decimals: 18 }, - rpcUrls: ["https://rpc.example"], + nativeCurrency: { + name: "Vexanium", + symbol: "VEX", + decimals: 18, + }, + rpcUrls: ["https://api.windcrypto.com/rpc"], }); ``` -Chain IDs must be canonical `0x`-prefixed hexadecimal values. Chain metadata URLs must use HTTPS. Always verify an RPC endpoint's `eth_chainId` response before presenting it to users. +## Security -EIP-6963 announcements are retained for the lifetime of the page, as required by the specification. `window.ethereum` is only used as a fallback when no announced provider is available. +Chain identifiers must use canonical `0x`-prefixed hexadecimal values. Applications should verify the chain ID returned by a newly supplied RPC endpoint before presenting that endpoint to users or requesting wallet registration. + +## Runtime + +The package targets browser environments with wallet providers. It forwards requests to the selected EIP-1193 provider and does not contain private-key storage or transaction-signing code of its own. ## License -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 950ae0adee61fa2780ae535c3169bd2433d34d98 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:00:52 +0700 Subject: [PATCH 23/49] docs(solana): standardize provider client documentation --- packages/solana/README.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/solana/README.md b/packages/solana/README.md index be7dbe5..ac1504e 100644 --- a/packages/solana/README.md +++ b/packages/solana/README.md @@ -1,11 +1,19 @@ # @windstack/solana -Provider client for the Solana interface exposed by Wisp-compatible wallets. +## Overview + +`@windstack/solana` provides a client for the Solana provider interface exposed by Wisp-compatible browser wallets. It supports account access, provider requests, message signing, disconnect handling, account normalization, and provider error preservation. + +The package focuses on wallet-provider communication and does not bundle a Solana transaction library. + +## Installation ```bash npm install @windstack/core @windstack/solana ``` +## Usage + ```ts import { createSolanaClient } from "@windstack/solana"; @@ -16,10 +24,18 @@ const result = await client.signMessage( new TextEncoder().encode("Sign in to My App"), accounts[0]?.publicKey, ); + +console.log(result); ``` -The client reads `window.solana`, forwards provider requests, normalizes account payloads, and preserves numeric provider error codes. It does not bundle a Solana transaction library. +The client reads the wallet provider exposed in the browser, forwards requests, normalizes account payloads, and preserves numeric provider error codes so applications can handle wallet responses consistently. + +## Runtime + +The package targets browser environments that expose a compatible Solana wallet provider. It does not manage private keys and does not include a Solana transaction serializer. ## License -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From c8ca9f1803fb2d88bd3ec8d3b7e5cb12af091f8b Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:01:50 +0700 Subject: [PATCH 24/49] docs(abi): keep Vexanium-facing examples brand-consistent --- packages/abi/README.md | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/packages/abi/README.md b/packages/abi/README.md index 844653c..3203854 100644 --- a/packages/abi/README.md +++ b/packages/abi/README.md @@ -14,27 +14,16 @@ npm install @windstack/abi ## Usage +A serializer can be created from an ABI returned by a Vexanium RPC endpoint: + ```ts import { AbiSerializer } from "@windstack/abi"; +import { RpcClient } from "@windstack/rpc"; -const abi = { - version: "eosio::abi/1.2", - structs: [ - { - name: "transfer", - base: "", - fields: [ - { name: "from", type: "name" }, - { name: "to", type: "name" }, - { name: "quantity", type: "asset" }, - { name: "memo", type: "string" }, - ], - }, - ], - actions: [{ name: "transfer", type: "transfer" }], -}; - +const rpc = new RpcClient({ endpoints: "https://api.windcrypto.com" }); +const { abi } = await rpc.getAbi("vex.token"); const serializer = new AbiSerializer(abi); + const bytes = serializer.encodeAction("transfer", { from: "alice", to: "bob", From 6dea61ea5dfe7ecd318d3106ae9b746a18c57c76 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:04:16 +0700 Subject: [PATCH 25/49] chore(antelope): refine package description --- packages/antelope/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/antelope/package.json b/packages/antelope/package.json index 7cb4118..5fe71d4 100644 --- a/packages/antelope/package.json +++ b/packages/antelope/package.json @@ -1,7 +1,7 @@ { "name": "@windstack/antelope", "version": "1.0.0", - "description": "Modern zero-WharfKit Antelope SDK: transactions, TAPOS, signing, contracts, accounts and RPC.", + "description": "Antelope transaction, signing, contract, account, and RPC client for WindStack applications.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 4b74afaf0cca2710831a6a9d8f52bfe12231ab32 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:04:34 +0700 Subject: [PATCH 26/49] feat(antelope): add Vexanium Mainnet preset --- packages/antelope/src/networks.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/antelope/src/networks.ts diff --git a/packages/antelope/src/networks.ts b/packages/antelope/src/networks.ts new file mode 100644 index 0000000..97a8eac --- /dev/null +++ b/packages/antelope/src/networks.ts @@ -0,0 +1,21 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ + +export const VEXANIUM_MAINNET = Object.freeze({ + name: "Vexanium Mainnet", + chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", + endpoints: Object.freeze(["https://api.windcrypto.com"]), + contracts: Object.freeze({ + system: "vexcore", + token: "vex.token", + }), + nativeToken: Object.freeze({ + symbol: "VEX", + precision: 4, + contract: "vex.token", + }), +}); From 11769ff85cd829db5e03118f68ea57b3190afbf8 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:12:47 +0700 Subject: [PATCH 27/49] test(vexanium): verify production ABI before release --- scripts/verify-vexanium-live.mjs | 222 +++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 scripts/verify-vexanium-live.mjs diff --git a/scripts/verify-vexanium-live.mjs b/scripts/verify-vexanium-live.mjs new file mode 100644 index 0000000..3bcb417 --- /dev/null +++ b/scripts/verify-vexanium-live.mjs @@ -0,0 +1,222 @@ +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { AbiSerializer } from "../packages/abi/dist/index.js"; +import { PrivateKey, sha256Digest } from "../packages/crypto/dist/index.js"; + +const RPC = "https://api.windcrypto.com"; +const CHAIN_ID = "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f"; +const EXPECTED_ABI = { + "vex.token": "b05a9a7fa75705eb216a5330f8549a291d66c367eea1e49c980fd64783d9c2a0", + vexcore: "3f92498072f9ae810dc758b1ae15ecfa9ee3baae7763f0a73ada5a2a185c68f5", +}; + +const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); +const privateKey = PrivateKey.fromBytes("K1", scalarOne); +const publicKey = privateKey.toPublicKey().toString(); +const signature = privateKey.signDigest(sha256Digest(new TextEncoder().encode("WindStack Vexanium ABI"))).toString(); + +async function post(path, body = {}) { + const response = await fetch(`${RPC}${path}`, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(15_000), + }); + const payload = await response.json().catch(() => null); + if (!response.ok) { + throw new Error(`Vexanium RPC ${path} failed with HTTP ${response.status}`); + } + return payload; +} + +function stableStringify(value) { + if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function structuralAbi(abi) { + return { + version: abi.version, + types: abi.types ?? [], + structs: abi.structs ?? [], + actions: (abi.actions ?? []).map(({ name, type }) => ({ name, type })), + tables: abi.tables ?? [], + variants: abi.variants ?? [], + action_results: abi.action_results ?? [], + }; +} + +function structuralHash(abi) { + return createHash("sha256").update(stableStringify(structuralAbi(abi))).digest("hex"); +} + +function makeSampler(abi) { + const aliases = new Map((abi.types ?? []).map((item) => [item.new_type_name, item.type])); + const structs = new Map((abi.structs ?? []).map((item) => [item.name, item])); + const variants = new Map((abi.variants ?? []).map((item) => [item.name, item])); + + function resolve(type) { + let current = type; + const seen = new Set(); + while (aliases.has(current)) { + if (seen.has(current)) throw new Error(`Cyclic ABI alias: ${type}`); + seen.add(current); + current = aliases.get(current); + } + return current; + } + + function sample(rawType, stack = []) { + if (rawType.endsWith("[]")) return []; + if (rawType.endsWith("?")) return null; + if (rawType.endsWith("$")) return undefined; + + const type = resolve(rawType); + if (stack.includes(type)) throw new Error(`Recursive ABI type cannot be sampled: ${[...stack, type].join(" -> ")}`); + + const struct = structs.get(type); + if (struct) { + const value = {}; + if (struct.base) Object.assign(value, sample(struct.base, [...stack, type])); + for (const field of struct.fields ?? []) { + value[field.name] = sample(field.type, [...stack, type]); + } + return value; + } + + const variant = variants.get(type); + if (variant) { + const selected = variant.types?.[0]; + if (!selected) throw new Error(`ABI variant ${type} has no alternatives`); + return { type: selected, value: sample(selected, [...stack, type]) }; + } + + switch (type) { + case "bool": + return false; + case "uint8": + case "int8": + case "uint16": + case "int16": + case "uint32": + case "int32": + case "varuint32": + case "varuint": + case "varint32": + case "varint": + case "float32": + case "float64": + return 0; + case "uint64": + case "int64": + case "uint128": + case "int128": + return "0"; + case "float128": + return "00".repeat(16); + case "name": + return "alice"; + case "string": + case "bytes": + return ""; + case "checksum160": + return "00".repeat(20); + case "checksum256": + return "00".repeat(32); + case "checksum512": + return "00".repeat(64); + case "asset": + return "1.0000 VEX"; + case "extended_asset": + return { quantity: "1.0000 VEX", contract: "vex.token" }; + case "symbol": + return "4,VEX"; + case "symbol_code": + return "VEX"; + case "time_point": + return "2026-09-07T00:00:00.000000Z"; + case "time_point_sec": + return "2026-09-07T00:00:00Z"; + case "block_timestamp_type": + return "2026-09-07T00:00:00.000Z"; + case "public_key": + case "publickey": + return publicKey; + case "signature": + return signature; + default: + throw new Error(`Unsupported ABI type in production Vexanium ABI: ${type}`); + } + } + + return sample; +} + +function validateAbi(contract, abi) { + assert.ok(abi && typeof abi === "object", `${contract} returned no ABI`); + assert.equal(abi.version, "eosio::abi/1.2", `${contract} ABI version changed`); + assert.equal(structuralHash(abi), EXPECTED_ABI[contract], `${contract} ABI changed since release audit`); + + const serializer = new AbiSerializer(abi); + const sample = makeSampler(abi); + + for (const action of abi.actions ?? []) { + const bytes = serializer.encodeAction(action.name, sample(action.type)); + serializer.decodeAction(action.name, bytes); + } + for (const table of abi.tables ?? []) { + const bytes = serializer.encode(table.type, sample(table.type)); + serializer.decode(table.type, bytes); + } + for (const result of abi.action_results ?? []) { + const bytes = serializer.encode(result.result_type, sample(result.result_type)); + serializer.decode(result.result_type, bytes); + } +} + +const info = await post("/v1/chain/get_info"); +assert.equal(String(info.chain_id).toLowerCase(), CHAIN_ID, "RPC endpoint is not Vexanium Mainnet"); + +const token = await post("/v1/chain/get_abi", { account_name: "vex.token" }); +const system = await post("/v1/chain/get_abi", { account_name: "vexcore" }); +validateAbi("vex.token", token.abi); +validateAbi("vexcore", system.abi); + +const tokenActions = new Set(token.abi.actions.map((item) => item.name)); +const tokenTables = new Set(token.abi.tables.map((item) => item.name)); +for (const name of ["transfer", "open", "close", "issue", "retire"]) assert.ok(tokenActions.has(name)); +for (const name of ["accounts", "stat", "blacklist"]) assert.ok(tokenTables.has(name)); + +const systemActions = new Set(system.abi.actions.map((item) => item.name)); +const systemTables = new Set(system.abi.tables.map((item) => item.name)); +for (const name of [ + "delegatebw", + "undelegatebw", + "buyram", + "buyrambytes", + "sellram", + "refund", + "voteproducer", + "regproducer", + "regproxy", + "newaccount", + "updateauth", + "deleteauth", + "linkauth", + "unlinkauth", +]) { + assert.ok(systemActions.has(name), `vexcore is missing ${name}`); +} +for (const name of ["producers", "voters", "refunds", "userres", "delband", "rammarket", "instantund"]) { + assert.ok(systemTables.has(name), `vexcore is missing table ${name}`); +} + +console.log( + `Vexanium production ABI verified: ${token.abi.actions.length} vex.token actions, ${system.abi.actions.length} vexcore actions`, +); From 94f98ab1678938169b50ac4f62fd6ef7d4350365 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:13:08 +0700 Subject: [PATCH 28/49] chore(release): verify live Vexanium ABI before publish --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index af29ae2..3e22178 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "pack:dry-run": "npm pack --dry-run --workspaces", "test": "node scripts/test-signing.mjs && node scripts/test-provider-contract.mjs && node scripts/test-provider-spec.mjs && node scripts/test-sdk-behavior.mjs && node scripts/test-native-antelope.mjs", "test:native": "node scripts/test-native-antelope.mjs", + "verify:vexanium": "node scripts/verify-vexanium-live.mjs", "validate:native": "npm run clean && npm run format:check && npm run check:release && npm run build:native && npm run test:native && npm run pack:native", "validate": "npm run clean && npm run format:check && npm run check:release && npm run build && npm test && npm run pack:dry-run", - "release:dry-run": "npm run validate:native", + "release:dry-run": "npm run validate:native && npm run verify:vexanium", "release:npm": "node scripts/publish-native.mjs" }, "devDependencies": { From 09fe165e1d0ab79e123663b650fcefc62c382e42 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:14:34 +0700 Subject: [PATCH 29/49] feat(account): align helpers with Vexanium system ABI --- packages/account/src/index.ts | 279 ++++++++++++++++++++++++++++++++-- 1 file changed, 268 insertions(+), 11 deletions(-) diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index dbb1df8..4b20bc3 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -13,12 +13,38 @@ export type AccountClientOptions = { systemContract?: string; }; +export type Authority = { + threshold: number; + keys: Array<{ key: string; weight: number }>; + accounts: Array<{ + permission: { actor: string; permission: string }; + weight: number; + }>; + waits: Array<{ wait_sec: number; weight: number }>; +}; + +export type SystemActionOptions = { + permission?: string; +}; + function validateName(value: string, label: string): string { if (!value) throw new TypeError(`${label} is required`); nameToBigInt(value); return value; } +function validateOptionalName(value: string, label: string): string { + if (value) nameToBigInt(value); + return value; +} + +function validateLocation(value: number): number { + if (!Number.isInteger(value) || value < 0 || value > 0xffff) { + throw new RangeError("Producer location must be an integer between 0 and 65535"); + } + return value; +} + export class AccountClient { readonly name: string; readonly rpc: RpcClient; @@ -53,7 +79,9 @@ export class AccountClient { signal?: AbortSignal, ): Promise { if (!tokenContract) { - throw new TypeError("Token contract is required; configure it on the client or pass it explicitly"); + throw new TypeError( + "Token contract is required; configure it on the client or pass it explicitly", + ); } return this.rpc.getCurrencyBalance( validateName(tokenContract, "Token contract"), @@ -64,7 +92,7 @@ export class AccountClient { } contract(account: string): Contract { - return new Contract(account, this.rpc, this.abiCache); + return new Contract(validateName(account, "Contract account"), this.rpc, this.abiCache); } async transfer( @@ -76,7 +104,9 @@ export class AccountClient { ): Promise { const tokenContract = options.tokenContract ?? this.tokenContract; if (!tokenContract) { - throw new TypeError("Token contract is required; configure it on the client or pass tokenContract"); + throw new TypeError( + "Token contract is required; configure it on the client or pass tokenContract", + ); } const permission = validateName(options.permission ?? "active", "Permission"); return this.contract(tokenContract).action( @@ -94,10 +124,12 @@ export class AccountClient { signal?: AbortSignal, ): Promise { if (!this.systemContract) { - throw new TypeError("System contract is required; configure it on the client before using system actions"); + throw new TypeError( + "System contract is required; configure it on the client before using system actions", + ); } return this.contract(this.systemContract).action( - name, + validateName(name, "System action"), data, [`${this.name}@${validateName(permission, "Permission")}`], signal, @@ -153,9 +185,18 @@ export class AccountClient { ); } + buyRamSelf(quantity: string, signal?: AbortSignal): Promise { + return this.systemAction( + "buyramself", + { account: this.name, quant: quantity }, + "active", + signal, + ); + } + buyRamBytes(receiver: string, bytes: number, signal?: AbortSignal): Promise { - if (!Number.isSafeInteger(bytes) || bytes <= 0) { - throw new RangeError("RAM bytes must be a positive safe integer"); + if (!Number.isInteger(bytes) || bytes <= 0 || bytes > 0xffffffff) { + throw new RangeError("RAM bytes must be an integer between 1 and 4294967295"); } return this.systemAction( "buyrambytes", @@ -165,14 +206,230 @@ export class AccountClient { ); } - sellRam(bytes: number, signal?: AbortSignal): Promise { - if (!Number.isSafeInteger(bytes) || bytes <= 0) { - throw new RangeError("RAM bytes must be a positive safe integer"); + sellRam(bytes: number | bigint | string, signal?: AbortSignal): Promise { + let value: bigint; + try { + value = BigInt(bytes); + } catch { + throw new TypeError("RAM bytes must be an integer-compatible value"); } - return this.systemAction("sellram", { account: this.name, bytes }, "active", signal); + if (value <= 0n || value > 0x7fffffffffffffffn) { + throw new RangeError("RAM bytes must be a positive signed 64-bit integer"); + } + return this.systemAction("sellram", { account: this.name, bytes: value }, "active", signal); } refund(signal?: AbortSignal): Promise { return this.systemAction("refund", { owner: this.name }, "active", signal); } + + voteProducers( + producers: string[], + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + if (!Array.isArray(producers) || producers.length === 0 || producers.length > 30) { + throw new RangeError("Producer voting requires between 1 and 30 producers"); + } + const normalized = producers.map((producer) => validateName(producer, "Producer")); + if (new Set(normalized).size !== normalized.length) { + throw new TypeError("Producer voting cannot contain duplicate accounts"); + } + return this.systemAction( + "voteproducer", + { voter: this.name, proxy: "", producers: normalized }, + options.permission ?? "active", + signal, + ); + } + + voteProxy( + proxy: string, + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "voteproducer", + { voter: this.name, proxy: validateName(proxy, "Proxy"), producers: [] }, + options.permission ?? "active", + signal, + ); + } + + clearVote(options: SystemActionOptions = {}, signal?: AbortSignal): Promise { + return this.systemAction( + "voteproducer", + { voter: this.name, proxy: "", producers: [] }, + options.permission ?? "active", + signal, + ); + } + + registerProxy( + isProxy = true, + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "regproxy", + { proxy: this.name, isproxy: isProxy }, + options.permission ?? "active", + signal, + ); + } + + registerProducer( + producerKey: string, + url: string, + location: number, + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + if (typeof url !== "string") throw new TypeError("Producer URL must be a string"); + return this.systemAction( + "regproducer", + { + producer: this.name, + producer_key: producerKey, + url, + location: validateLocation(location), + }, + options.permission ?? "active", + signal, + ); + } + + unregisterProducer( + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "unregprod", + { producer: this.name }, + options.permission ?? "active", + signal, + ); + } + + claimRewards( + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "claimrewards", + { owner: this.name }, + options.permission ?? "active", + signal, + ); + } + + createAccount( + accountName: string, + owner: Authority, + active: Authority, + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "newaccount", + { + creator: this.name, + name: validateName(accountName, "New account name"), + owner, + active, + }, + options.permission ?? "active", + signal, + ); + } + + updatePermission( + permission: string, + parent: string, + authority: Authority, + authorizationPermission: string, + authorizedBy?: string, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "updateauth", + { + account: this.name, + permission: validateName(permission, "Permission"), + parent: validateName(parent, "Parent permission"), + auth: authority, + authorized_by: authorizedBy + ? validateName(authorizedBy, "Authorized-by permission") + : undefined, + }, + validateName(authorizationPermission, "Authorization permission"), + signal, + ); + } + + deletePermission( + permission: string, + authorizationPermission: string, + authorizedBy?: string, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "deleteauth", + { + account: this.name, + permission: validateName(permission, "Permission"), + authorized_by: authorizedBy + ? validateName(authorizedBy, "Authorized-by permission") + : undefined, + }, + validateName(authorizationPermission, "Authorization permission"), + signal, + ); + } + + linkPermission( + code: string, + action: string, + requirement: string, + authorizationPermission: string, + authorizedBy?: string, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "linkauth", + { + account: this.name, + code: validateName(code, "Contract"), + type: validateOptionalName(action, "Action"), + requirement: validateName(requirement, "Required permission"), + authorized_by: authorizedBy + ? validateName(authorizedBy, "Authorized-by permission") + : undefined, + }, + validateName(authorizationPermission, "Authorization permission"), + signal, + ); + } + + unlinkPermission( + code: string, + action: string, + authorizationPermission: string, + authorizedBy?: string, + signal?: AbortSignal, + ): Promise { + return this.systemAction( + "unlinkauth", + { + account: this.name, + code: validateName(code, "Contract"), + type: validateOptionalName(action, "Action"), + authorized_by: authorizedBy + ? validateName(authorizedBy, "Authorized-by permission") + : undefined, + }, + validateName(authorizationPermission, "Authorization permission"), + signal, + ); + } } From 2033cd6322b9764db01aa1e2a6f63bb043f05394 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:15:42 +0700 Subject: [PATCH 30/49] feat(antelope): add canonical Vexanium mainnet preset --- packages/antelope/src/vexanium.ts | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 packages/antelope/src/vexanium.ts diff --git a/packages/antelope/src/vexanium.ts b/packages/antelope/src/vexanium.ts new file mode 100644 index 0000000..e3c6600 --- /dev/null +++ b/packages/antelope/src/vexanium.ts @@ -0,0 +1,47 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { AntelopeClient, type AntelopeClientOptions } from "./index.js"; + +export const VEXANIUM_MAINNET_CHAIN_ID = + "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f" as const; +export const VEXANIUM_MAINNET_RPC = "https://api.windcrypto.com" as const; +export const VEXANIUM_SYSTEM_CONTRACT = "vexcore" as const; +export const VEXANIUM_TOKEN_CONTRACT = "vex.token" as const; +export const VEXANIUM_NATIVE_SYMBOL = "VEX" as const; +export const VEXANIUM_NATIVE_PRECISION = 4 as const; + +export const VEXANIUM_MAINNET = Object.freeze({ + name: "Vexanium Mainnet", + chainId: VEXANIUM_MAINNET_CHAIN_ID, + endpoints: [VEXANIUM_MAINNET_RPC] as const, + contracts: Object.freeze({ + system: VEXANIUM_SYSTEM_CONTRACT, + token: VEXANIUM_TOKEN_CONTRACT, + }), + nativeToken: Object.freeze({ + contract: VEXANIUM_TOKEN_CONTRACT, + symbol: VEXANIUM_NATIVE_SYMBOL, + precision: VEXANIUM_NATIVE_PRECISION, + }), +}); + +export type VexaniumClientOptions = Omit< + AntelopeClientOptions, + "endpoints" | "chainId" | "contracts" +> & { + endpoints?: AntelopeClientOptions["endpoints"]; +}; + +export function createVexaniumClient(options: VexaniumClientOptions = {}): AntelopeClient { + const { endpoints = VEXANIUM_MAINNET_RPC, ...clientOptions } = options; + return new AntelopeClient({ + ...clientOptions, + endpoints, + chainId: VEXANIUM_MAINNET_CHAIN_ID, + contracts: VEXANIUM_MAINNET.contracts, + }); +} From 05f9cba273c3805b87e7de4862f99518a694d622 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:16:08 +0700 Subject: [PATCH 31/49] feat(antelope): add Vexanium export --- packages/antelope/package.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/antelope/package.json b/packages/antelope/package.json index 5fe71d4..2e62628 100644 --- a/packages/antelope/package.json +++ b/packages/antelope/package.json @@ -5,10 +5,19 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, + "exports": { + ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./vexanium": { "types": "./dist/vexanium.d.ts", "import": "./dist/vexanium.js" } + }, "files": ["dist", "README.md", "LICENSE", "package.json"], "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, - "dependencies": { "@windstack/account": "1.0.0", "@windstack/abi": "1.0.0", "@windstack/contract": "1.0.0", "@windstack/crypto": "1.0.0", "@windstack/rpc": "1.0.0" }, + "dependencies": { + "@windstack/account": "1.0.0", + "@windstack/abi": "1.0.0", + "@windstack/contract": "1.0.0", + "@windstack/crypto": "1.0.0", + "@windstack/rpc": "1.0.0" + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", From dc3d0d2da8ae335004b283c0130ffdf813145fce Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:17:35 +0700 Subject: [PATCH 32/49] docs(crypto): use Vexanium-facing terminology --- packages/crypto/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/crypto/README.md b/packages/crypto/README.md index ebaed54..5bb9794 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -2,9 +2,9 @@ ## Overview -`@windstack/crypto` provides Antelope-compatible K1 and R1 key handling for WindStack applications. It supports private keys, public keys, recoverable signatures, signature verification, public-key recovery, modern Antelope encodings, legacy EOS public keys, and legacy K1 WIF private keys. +`@windstack/crypto` provides K1 and R1 key handling for WindStack and Vexanium applications. It supports private keys, public keys, recoverable signatures, signature verification, public-key recovery, current Antelope key encodings, and compatibility with older K1 key encodings used by existing Vexanium accounts and node software. -The package uses Noble curve and hash primitives while keeping the public API based on `Uint8Array` and Antelope key formats. +The public API is based on `Uint8Array`. Elliptic-curve and hashing primitives are provided by the Noble libraries. ## Installation @@ -27,13 +27,13 @@ console.log(signature.verifyDigest(digest, publicKey)); console.log(signature.recoverDigest(digest).equals(publicKey)); ``` -K1 signatures are emitted in the canonical compact form required by Antelope-compatible chains. K1 public keys can also be converted to the legacy `EOS...` representation when interacting with older node software. +K1 signatures are emitted in the canonical compact form required by Vexanium-compatible transaction signing. Existing K1 private and public keys can be imported through the compatibility parsers without changing their key material. ## Runtime -The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Browser and React Native environments must provide the secure randomness required by key generation. Existing keys can be imported with `PrivateKey.fromString()` or `PrivateKey.fromBytes()`. +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Browser and React Native environments must provide secure platform randomness for key generation. Existing keys can be imported with `PrivateKey.fromString()` or `PrivateKey.fromBytes()`. -Private keys should be stored and used through an appropriate secure storage or signing boundary. Application logs should never contain private-key material. +Private keys should be held by an appropriate secure storage or signing boundary. Application logs should never contain private-key material. ## License From dba5e5d462723ed1a20f8859b6bcf000f3d9e05c Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:18:04 +0700 Subject: [PATCH 33/49] docs(vexanium): publish professional VEX documentation --- packages/vexanium/README.md | 103 +++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/packages/vexanium/README.md b/packages/vexanium/README.md index 312c573..ddeaf74 100644 --- a/packages/vexanium/README.md +++ b/packages/vexanium/README.md @@ -1,38 +1,41 @@ # @windstack/vexanium -Vexanium provider client, chain metadata, signing requests, and explorer helpers. +## Overview + +`@windstack/vexanium` provides Vexanium network metadata, browser wallet-provider access, Vexanium Signing Requests, session observation, asset helpers, and Wind Explorer URL utilities. + +VEX Native configuration exposed by this package uses: + +- Chain ID: `f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f` +- RPC and API: `https://api.windcrypto.com` +- System contract: `vexcore` +- Native token contract: `vex.token` +- Native symbol: `VEX` +- Precision: `4` + +The package also exposes VEX EVM metadata for chain ID `6736` (`0x1a50`) and the WindStack EVM endpoints. + +## Installation ```bash npm install @windstack/vexanium ``` -The package uses the current WharfKit Antelope and Signing Request libraries. It does not define a second transaction serializer. +## Usage -## Network Metadata +### Network metadata ```ts import { vexEvm, vexNative } from "@windstack/vexanium"; -``` -VEX Native: - -- Chain ID: `f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f` -- CAIP-2 scope: `antelope:f9f432b1851b5c179d2091a96f593aa` -- RPC/API base: `https://api.windcrypto.com` -- Token: `VEX`, precision 4 - -VEX EVM: - -- Chain ID: `6736` (`0x1a50`) -- Native currency: `VEX`, 18 decimals -- JSON-RPC: `https://api.windcrypto.com/rpc` -- Indexed data API: `https://api.windcrypto.com/v3/evm` -- Live stats: `https://api.windcrypto.com/v3/evm/stats` -- Explorer: `https://explorer.windcrypto.com/evm` - -Use `vexEvm.rpcUrl` with JSON-RPC clients and `wallet_addEthereumChain`. The indexed API remains a separate REST service. +console.log(vexNative.chainId); +console.log(vexNative.contracts.system); // vexcore +console.log(vexNative.contracts.token); // vex.token +console.log(vexNative.token.symbol); // VEX +console.log(vexEvm.chainId); // 6736 +``` -## Connect +### Wallet connection ```ts import { createVexaniumClient, vexNative } from "@windstack/vexanium"; @@ -51,28 +54,20 @@ if (accounts.length === 0) { } ``` -The client discovers providers announced on the page or exposed as `window.vexanium`. `providerInfo` is required, so the SDK never invents a wallet name for an unknown provider. - -`getAccounts()` checks an existing permission without prompting. `connect()` requests permission and creates an in-memory session mirror. Provider events and browser focus/visibility changes keep that mirror in sync. +`getAccounts()` reads an existing wallet permission without opening a connection prompt. `connect()` requests wallet authorization for the selected Vexanium chain. Provider events and browser visibility changes keep the local session view synchronized with the wallet. ```ts const unsubscribe = client.subscribeSession(({ session, reason }) => { - updateWalletState(session, reason); + console.log(session, reason); }); unsubscribe(); client.destroy(); ``` -## SessionKit - -For a normal VEX Native dApp, use `@windstack/wallet-plugin-wisp`. SessionKit resolves the transaction and the plugin forwards the exact serialized bytes to the provider. - -```bash -npm install @windstack/wallet-plugin-wisp @wharfkit/session -``` +### Exact transaction signing -Direct exact-byte signing is also available: +Applications that already have serialized Vexanium transaction bytes can request signatures without rebuilding the transaction: ```ts const result = await client.signTransaction({ @@ -81,16 +76,22 @@ const result = await client.signTransaction({ account: "alice", permission: "active", }); + +console.log(result.signatures); ``` -The client validates the full chain ID, serialized hex, Antelope names, and returned signatures before accepting the result. +The client validates the full Vexanium chain ID, serialized hexadecimal payload, account and permission names, and returned signatures before accepting a result. -## Vexanium Signing Requests +### Vexanium Signing Requests -Use VSR for a request that must travel through a QR code, link, clipboard, or external wallet. +VSR is available for portable requests that need to move through QR codes, links, the clipboard, or an external wallet flow. ```ts -import { createSigningRequest, parseSigningRequest, vexNative } from "@windstack/vexanium"; +import { + createSigningRequest, + parseSigningRequest, + vexNative, +} from "@windstack/vexanium"; const uri = await createSigningRequest({ chainId: vexNative.chainId, @@ -103,38 +104,40 @@ const uri = await createSigningRequest({ from: "alice", to: "bob", quantity: "1.0000 VEX", - memo: "", + memo: "WindStack", }, }, -}, { compress: true }); +}); const request = parseSigningRequest(uri); ``` -New requests are encoded as `vsr:`. When `compress: true` is used, the SDK supplies its built-in zlib implementation for both encoding and parsing. Existing `esr:` input is accepted because the payload is parsed by WharfKit's Signing Request implementation. A custom `options.zlib` provider can still be supplied for specialized runtimes. - -## Utilities +### Explorer and asset utilities ```ts import { buildExplorerAccountUrl, buildExplorerTxUrl, formatAsset, - mapExplorerTransaction, parseAsset, } from "@windstack/vexanium"; -const asset = parseAsset("-1.2500 VEX"); +const asset = parseAsset("1.2500 VEX"); const value = formatAsset(asset.amount, asset.precision, asset.symbol); +const accountUrl = buildExplorerAccountUrl("gvexa"); +const transactionUrl = buildExplorerTxUrl("transaction-id"); ``` -Explorer URL builders encode path segments. `mapExplorerTransaction` maps common node and indexer response shapes into the explorer view model while preserving the raw response. It does not decode, rebuild, or serialize an Antelope transaction. -Native token URLs use Wind Explorer's canonical `/tokens/:contract/:symbol` route when both identifiers are known. +## Runtime + +The provider client targets browser applications with a compatible Vexanium wallet provider. Network metadata and utility functions can also be used in server-side applications. -## Provider Authors +Application metadata is display information. Wallet permissions must be bound to a trusted runtime or transport origin rather than an origin supplied by application content. -The wallet contract, methods, errors, events, and security boundary are documented in the repository's `VEXANIUM-PROVIDER-V1.md` file. +For transaction construction, ABI serialization, RPC, contract actions, account operations, and signing, use the dedicated `@windstack/antelope` package family. ## License -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 67f844094915239f3da30a4dd28a8927d3df3053 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:18:25 +0700 Subject: [PATCH 34/49] docs(wisp): standardize wallet plugin documentation --- packages/wallet-plugin-wisp/README.md | 29 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/wallet-plugin-wisp/README.md b/packages/wallet-plugin-wisp/README.md index e7bb646..2238beb 100644 --- a/packages/wallet-plugin-wisp/README.md +++ b/packages/wallet-plugin-wisp/README.md @@ -1,12 +1,19 @@ # @windstack/wallet-plugin-wisp -WharfKit SessionKit wallet plugin for Wisp on Vexanium Mainnet. +## Overview + +`@windstack/wallet-plugin-wisp` connects SessionKit applications to Wisp Wallet on Vexanium Mainnet. It handles wallet discovery, account authorization, exact transaction signing, chain validation, and conversion of wallet signatures into the format expected by the connected session. + +The plugin is restricted to Vexanium Mainnet and selects the Wisp provider identified by `com.wisp.wallet` unless a provider or client is supplied explicitly. + +## Installation ```bash -npm install @windstack/wallet-plugin-wisp @windstack/vexanium \ - @wharfkit/session @wharfkit/antelope @wharfkit/signing-request +npm install @windstack/wallet-plugin-wisp ``` +## Usage + ```ts import { SessionKit } from "@wharfkit/session"; import { WispWalletPlugin } from "@windstack/wallet-plugin-wisp"; @@ -29,18 +36,24 @@ await session.transact({ from: session.actor, to: "receiver", quantity: "1.0000 VEX", - memo: "", + memo: "WindStack", }, }, }); ``` -SessionKit resolves placeholders, ABI data, signer, TAPOS, and transaction bytes. The plugin sends `ResolvedSigningRequest.serializedTransaction` to Wisp through `vex_signTransaction` and converts the returned strings to WharfKit `Signature` values. +The session resolves the Vexanium transaction before signing. The plugin forwards the exact serialized transaction bytes to Wisp through the Vexanium provider and returns the resulting signatures to the session. -The plugin supports Vexanium Mainnet only. It rejects a different chain during both login and signing. Unless a provider or client is supplied explicitly, it selects the provider whose reverse-DNS identifier is `com.wisp.wallet`. +A different chain ID is rejected during both login and signing. Portable Vexanium Signing Requests are handled separately by `@windstack/vexanium`. -Portable `vsr:` requests are handled by `@windstack/vexanium`; they are separate from the connected SessionKit transaction path. +## Runtime + +The package targets browser applications with Wisp Wallet available through the Vexanium provider interface. It does not store private keys and does not rebuild transactions after the session has resolved them. + +Applications should provide accurate dApp metadata and must treat wallet authorization as origin-bound permission state. ## License -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From a1c1e61e7ff65d9c489da3a365d4a10f91f15fea Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:19:07 +0700 Subject: [PATCH 35/49] docs(provider): publish VexaniumProvider v1 specification --- VEXANIUM-PROVIDER-V1.md | 114 ++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/VEXANIUM-PROVIDER-V1.md b/VEXANIUM-PROVIDER-V1.md index 342a7c3..11bff26 100644 --- a/VEXANIUM-PROVIDER-V1.md +++ b/VEXANIUM-PROVIDER-V1.md @@ -1,16 +1,18 @@ # VexaniumProvider v1 +## Overview + +`VexaniumProvider` defines the browser-facing contract between a Vexanium dApp and a compatible wallet provider. It standardizes provider identity, capability negotiation, account access, exact transaction signing, portable signing requests, events, errors, discovery, and the security boundary used for wallet permissions. + Protocol identifier: `VexaniumProvider` Protocol version: `1.0.0` -SDK release implementing this contract: `0.6.0` - -This document defines the wire contract between a dApp and an injected Vexanium wallet. Antelope transaction encoding and signing-request payloads remain defined by WharfKit and the Antelope protocol. +Created by **Gilang Ramadan**. -## 1. Provider object +## Provider object -A compatible wallet provider MUST expose: +A compatible provider exposes the following interface: ```ts interface VexaniumProvider { @@ -22,10 +24,10 @@ interface VexaniumProvider { } ``` -`providerInfo` is mandatory and MUST include: +`providerInfo` is required and contains the provider identity and supported Vexanium capabilities: ```ts -{ +interface VexaniumProviderInfo { uuid: string; name: string; rdns: string; @@ -37,19 +39,19 @@ interface VexaniumProvider { } ``` -A dApp MUST NOT invent missing provider identity. Discovery ignores providers that do not satisfy the v1 shape. +A client must not invent missing provider identity. Providers that do not expose the required v1 shape are ignored during discovery. -## 2. Version compatibility +## Version compatibility -The SDK and wallet negotiate semantic protocol versions through `vex_getCapabilities`. +The client and wallet negotiate semantic protocol versions through `vex_getCapabilities`. -For protocol v1, compatible implementations MUST share major version `1`. A `2.x` provider is not implicitly compatible with a `1.x` SDK. +Implementations of protocol v1 must share major version `1`. A provider using another major version is not considered compatible unless a future specification explicitly defines that compatibility. -## 3. Capabilities +## Capabilities -Defined v1 capability identifiers: +Protocol v1 defines these capability identifiers: -```txt +```text vex.accounts vex.sessions vex.signTransaction @@ -59,9 +61,9 @@ vex.signDigest vex.events ``` -A provider MUST declare static capabilities in `providerInfo.capabilities` and return negotiated capabilities from `vex_getCapabilities`. +A provider declares its static capabilities in `providerInfo.capabilities` and returns the negotiated set from `vex_getCapabilities`. -## 4. Capability negotiation +## Capability negotiation Request: @@ -88,11 +90,11 @@ Response: } ``` -The SDK rejects incompatible major versions and missing required capabilities before connect/sign flows continue. +The client rejects an incompatible major version or a missing required capability before account or signing flows continue. -## 5. Connect +## Account access -`vex_requestAccounts` is the interactive permission request. +`vex_requestAccounts` is the interactive authorization request. Request: @@ -119,11 +121,9 @@ Response: } ``` -The response is not an array and `sessionId` is mandatory. - -`chainId` in a response MUST be the 64-character Antelope chain ID. A request MAY use either that full ID or its `antelope:<32 hex characters>` CAIP-2 form. Wallets and clients MUST compare those two forms as the same chain when their prefixes match. +`sessionId` is required. `chainId` in a response is the complete 64-character Vexanium chain ID. A request may use either the complete chain ID or its `antelope:<32 hex characters>` CAIP-2 scope. Clients compare the two forms by their shared chain prefix. -`vex_getAccounts` is the silent restore/read path and returns: +`vex_getAccounts` is the non-interactive restore/read path and returns: ```ts { @@ -133,11 +133,9 @@ The response is not an array and `sessionId` is mandatory. } ``` -## 6. Signing paths +## Exact transaction signing -### Connected dApp / SessionKit - -`vex_signTransaction` signs the exact serialized Antelope transaction bytes resolved by SessionKit. +`vex_signTransaction` signs the exact serialized Vexanium transaction supplied by the application or session layer. ```ts { @@ -149,9 +147,9 @@ The response is not an array and `sessionId` is mandatory. } ``` -A wallet MUST NOT silently rebuild or mutate the transaction before signing. +A wallet must not silently rebuild or alter the transaction before signing. -`serializedTransaction` MUST contain non-empty, even-length hexadecimal bytes. `account` and `permission` MUST be valid Antelope names. A successful response contains at least one valid Antelope signature: +`serializedTransaction` contains non-empty, even-length hexadecimal bytes. `account` and `permission` must be valid Antelope names. A successful response contains at least one valid signature: ```ts { @@ -161,23 +159,25 @@ A wallet MUST NOT silently rebuild or mutate the transaction before signing. } ``` -### Portable Vexanium Signing Request +## Vexanium Signing Requests -`vex_signingRequest` is used for QR, deep-link, clipboard, or external wallet transport. +`vex_signingRequest` is used for a request transported through a QR code, deep link, clipboard, or external wallet flow. -Canonical Vexanium URI scheme: +The canonical Vexanium URI scheme is: -```txt +```text vsr://... ``` -The payload format is compatible with WharfKit SigningRequest / ESR Revision 3. `esr://...` is accepted as interoperability input. The client validates either scheme with WharfKit and forwards the original URI without decoding, re-encoding, or replacing its scheme. +The payload follows the compatible Antelope signing-request format used by existing ecosystem tooling. Compatible input using the established alternate URI scheme may be accepted for interoperability, while newly created Vexanium requests use `vsr:`. + +A successful signing-request response contains `signatures: string[]` and `broadcast: boolean`. Empty or malformed signature lists are rejected. -A signing-request response MUST include `signatures: string[]` and `broadcast: boolean`. A client MUST reject an empty or malformed signature list. +## Errors -## 7. Standard errors +Protocol v1 defines these provider error codes: -```txt +```text 4001 USER_REJECTED 4100 UNAUTHORIZED 4200 UNSUPPORTED_METHOD @@ -193,11 +193,13 @@ A signing-request response MUST include `signatures: string[]` and `broadcast: b -32603 INTERNAL_ERROR ``` -Errors MUST expose a numeric `code` and human-readable `message`. Optional `data` may provide structured context. +Errors expose a numeric `code` and a human-readable `message`. Optional `data` may carry structured context. + +## Events -## 8. Events +Compatible providers may emit: -```txt +```text connect accountsChanged disconnect @@ -205,19 +207,31 @@ chainChanged message ``` -`connect` uses the canonical connect response shape. `accountsChanged` uses the canonical accounts response shape. - -## 9. Security boundary - -`DappMetadata` is display metadata only. Wallet permission state MUST bind to an authoritative transport/runtime origin, such as the browser extension sender origin. A wallet MUST NOT trust a dApp-supplied `origin` field as the permission boundary. +`connect` uses the canonical connection response shape. `accountsChanged` uses the canonical accounts response shape. -## 10. Discovery +## Discovery -A provider may be discovered through `window.vexanium` or Vexanium provider announcement events. A discovered provider MUST expose valid mandatory `providerInfo`; the SDK does not invent provider metadata. +A provider may be exposed through `window.vexanium` or provider announcement events. Discovery uses: -Discovery uses these window events: - -```txt +```text vexanium:requestProvider vexanium:announceProvider ``` + +A discovered provider must expose valid mandatory `providerInfo` before it is accepted. + +## Security + +`DappMetadata` is display metadata only. Wallet permission state must bind to an authoritative runtime or transport origin, such as the browser extension sender origin. A wallet must not use an origin supplied by application content as the permission boundary. + +Exact transaction signing must preserve the bytes approved by the application and must not substitute a rebuilt transaction after user approval. + +## Runtime + +The specification is transport-oriented and does not require a specific UI framework, storage implementation, or signing backend. Implementations may use browser extensions, mobile wallet bridges, embedded providers, or other trusted transports as long as the observable provider contract remains compatible. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From 7032f01be65ff63bd2cb3916005a3e9a52392f10 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:19:28 +0700 Subject: [PATCH 36/49] docs(specs): clarify provider specification purpose --- specs/README.md | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/specs/README.md b/specs/README.md index ab35530..2bf6fb6 100644 --- a/specs/README.md +++ b/specs/README.md @@ -1,26 +1,35 @@ -# WindStack provider specifications +# WindStack Provider Specifications -`wisp-provider-contract.json` is the canonical machine-readable contract shared -by WindStack SDK packages and the Wisp Wallet provider runtime. +## Overview -It defines only standards-facing values: +This directory contains machine-readable provider contracts shared by WindStack SDK packages and Wisp Wallet. The files define stable identifiers and protocol values that must remain consistent across provider implementations and client libraries. -- Wisp provider identity (`name`, reverse-DNS identifier, and compatibility marker) -- Shared EIP-1193/Vexanium provider error codes -- VexaniumProvider version, chain identifiers, capabilities, methods, and - discovery events -- EIP-1193 method names, EIP-6963 discovery events, and VEX EVM chain identifiers/scope +## Provider contract -Wallet-specific message transport identifiers are intentionally excluded. Wisp -keeps those private fields in its local runtime contract while synchronizing the -canonical sections from this file. +`wisp-provider-contract.json` defines: -Run the full WindStack validation before changing the specification: +- Wisp provider identity and reverse-DNS identifier +- Vexanium provider version and chain identifiers +- Vexanium capabilities, methods, and discovery events +- Shared provider error codes +- VEX EVM chain identifiers +- EIP-1193 method names +- EIP-6963 discovery events -```bash -npm run validate -``` +Wallet-private transport identifiers and runtime-only implementation details are not part of the shared contract. -`test-provider-spec.mjs` fails when the JSON specification and exported SDK -constants no longer match. Wisp Wallet independently checks its synchronized -copy through `scripts/sync-windstack-provider-contract.mjs`. +## Usage + +WindStack packages consume the specification through exported constants in `@windstack/core`. Wisp Wallet uses the same values for provider discovery and request handling so dApps receive a consistent provider contract across supported runtimes. + +Changes to the machine-readable contract must remain compatible with the public `VexaniumProvider` specification and the exported SDK constants. + +## Related specification + +`VEXANIUM-PROVIDER-V1.md` documents the browser-facing Vexanium provider interface, capability negotiation, account access, signing requests, events, errors, discovery, and permission security boundary. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. From a5562ca89c3db8f2f530cf85f008b58eb024f0ae Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:19:58 +0700 Subject: [PATCH 37/49] docs(antelope): document Vexanium preset and signing --- packages/antelope/README.md | 55 +++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/antelope/README.md b/packages/antelope/README.md index 0c76f11..acabff8 100644 --- a/packages/antelope/README.md +++ b/packages/antelope/README.md @@ -2,9 +2,11 @@ ## Overview -`@windstack/antelope` is the high-level transaction client for WindStack Antelope applications. It combines RPC access, ABI serialization, contract access, account helpers, TAPOS construction, transaction serialization, signing-digest calculation, required-key resolution, pluggable signers, and transaction broadcast. +`@windstack/antelope` is the high-level transaction client for WindStack applications. It combines Vexanium-compatible RPC access, ABI serialization, contract access, account helpers, TAPOS construction, canonical transaction serialization, signing-digest calculation, required-key resolution, pluggable signers, and transaction broadcast. -A client can be bound to an expected chain ID. When configured, signing stops if the RPC endpoint reports a different chain, preventing transactions from being signed against an unintended network. +A client can be bound to an expected chain ID. When configured, signing stops if the RPC endpoint reports a different chain, preventing a transaction from being signed against an unintended network. + +Vexanium Mainnet is available through the `@windstack/antelope/vexanium` entrypoint with the canonical chain ID, RPC endpoint, `vexcore` system contract, `vex.token` native token contract, `VEX` symbol, and precision `4` already configured. ## Installation @@ -14,21 +16,23 @@ npm install @windstack/antelope ## Usage +### Vexanium Mainnet + ```ts import { - AntelopeClient, + VEXANIUM_MAINNET, + createVexaniumClient, +} from "@windstack/antelope/vexanium"; +import { PrivateKey, PrivateKeySigner, } from "@windstack/antelope"; -const client = new AntelopeClient({ - endpoints: ["https://api.windcrypto.com"], - chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", - contracts: { - system: "vexcore", - token: "vex.token", - }, -}); +const client = createVexaniumClient(); + +console.log(VEXANIUM_MAINNET.contracts.system); // vexcore +console.log(VEXANIUM_MAINNET.contracts.token); // vex.token +console.log(VEXANIUM_MAINNET.nativeToken.symbol); // VEX const signer = new PrivateKeySigner([ PrivateKey.fromString("PVT_K1_..."), @@ -42,17 +46,38 @@ const result = await client.transact({ actions: [transfer], signer, }); + +console.log(result.response); ``` -`PrivateKeySigner` is intended for environments where application-managed keys are appropriate. Wallets, hardware signers, remote signers, and secure-storage integrations can implement the exported `Signer` interface and receive the chain ID, transaction, serialized bytes, digest, and required keys in one signing request. +### Custom signer + +`PrivateKeySigner` is suitable only when application-managed keys are appropriate. Wallets, hardware signers, secure-storage integrations, and remote signers can implement the exported `Signer` interface. -K1 available keys use the legacy `EOS...` representation by default for broad compatibility with older Antelope node software. Applications that require modern K1 public-key strings can set `k1PublicKeyFormat: "modern"` on `PrivateKeySigner`. +A signer receives the chain ID, resolved transaction, serialized transaction bytes, signing digest, and required public keys in a single request. + +```ts +import type { Signer } from "@windstack/antelope"; + +const signer: Signer = { + async getAvailableKeys() { + return ["PUB_K1_..."]; + }, + async sign(request) { + return secureSigner.sign(request.digest, request.requiredKeys); + }, +}; +``` + +The built-in K1 signer uses the compatibility public-key representation expected by older Vexanium node software when resolving required keys. Applications can request current K1 public-key strings with `k1PublicKeyFormat: "modern"`. ## Runtime -The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses Web-standard byte and networking APIs and does not require Node.js `Buffer` for the Antelope transaction path. +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses Web-standard byte and networking APIs and does not require Node.js `Buffer` for transaction construction or signing. + +The client accepts one or more RPC endpoints for read operations and required-key resolution. Transaction broadcast is not retried automatically when the outcome of a submitted transaction is uncertain. -The high-level client accepts one or more RPC endpoints. Transaction broadcast is intentionally not retried automatically when the response is uncertain. +Vexanium production ABI compatibility is checked as part of the repository release validation against `vexcore` and `vex.token`. ## License From 7e10c9fcff1f5c1a1998ecb36815f1e133ae1394 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:20:17 +0700 Subject: [PATCH 38/49] docs(account): document Vexanium account helpers --- packages/account/README.md | 67 +++++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/packages/account/README.md b/packages/account/README.md index 2614320..587a970 100644 --- a/packages/account/README.md +++ b/packages/account/README.md @@ -2,9 +2,9 @@ ## Overview -`@windstack/account` provides account reads and common Antelope account actions. It supports account queries, token balances, token transfers, CPU/NET staking actions, RAM purchases, RAM sales, and refund action construction. +`@windstack/account` provides account reads and Vexanium-compatible account action builders. It supports account queries, VEX balances, token transfers, CPU and NET staking, unstaking, RAM operations, refunds, producer voting, proxy voting, producer registration, reward claims, account creation, and permission management. -System and token contract names are configured explicitly so the same package can be used safely across Antelope chains with different system account names. +System and token contract names are supplied by the parent client. The Vexanium preset uses `vexcore` for system actions and `vex.token` for the native `VEX` token. ## Installation @@ -12,32 +12,69 @@ System and token contract names are configured explicitly so the same package ca npm install @windstack/account ``` -## Usage +The account helper is normally created by `@windstack/antelope` so RPC, ABI caching, chain identity, and Vexanium contract configuration are shared automatically. -The account helper is normally created by `@windstack/antelope`, which passes the chain contract configuration automatically. +## Usage ```ts -import { AntelopeClient } from "@windstack/antelope"; - -const client = new AntelopeClient({ - endpoints: "https://api.windcrypto.com", - contracts: { - system: "vexcore", - token: "vex.token", - }, -}); +import { createVexaniumClient } from "@windstack/antelope/vexanium"; +const client = createVexaniumClient(); const account = client.account("alice"); + const balances = await account.balance(undefined, "VEX"); const transfer = await account.transfer("bob", "1.0000 VEX", "WindStack"); const stake = await account.delegate("alice", "1.0000 VEX", "2.0000 VEX"); +const unstake = await account.undelegate("alice", "1.0000 VEX", "1.0000 VEX"); +const buyRam = await account.buyRam("alice", "5.0000 VEX"); +const sellRam = await account.sellRam(4096); +``` + +### Voting and producer actions + +```ts +const vote = await account.voteProducers([ + "producerone", + "producertwo", +]); + +const proxyVote = await account.voteProxy("myproxy"); +const registerProxy = await account.registerProxy(true); + +const producer = await account.registerProducer( + "PUB_K1_...", + "https://producer.example", + 0, +); ``` -If a token or system contract has not been configured, helpers that depend on it fail before building an action. Applications can also pass a token contract directly for individual token operations. +Producer voting rejects duplicates and accepts at most 30 producer accounts, matching the Vexanium system contract. `clearVote()` removes the current direct producer or proxy selection. + +### Account permissions + +Permission changes require the authorization permission to be supplied explicitly. The helper does not guess whether `owner`, `active`, or another permission is appropriate for a sensitive account change. + +```ts +await account.updatePermission( + "custom", + "active", + { + threshold: 1, + keys: [{ key: "PUB_K1_...", weight: 1 }], + accounts: [], + waits: [], + }, + "active", +); +``` + +The package also exposes `deletePermission()`, `linkPermission()`, `unlinkPermission()`, `createAccount()`, `registerProxy()`, `unregisterProducer()`, `claimRewards()`, `refund()`, `buyRamSelf()`, and `buyRamBytes()`. ## Runtime -The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Network requests are delegated to `@windstack/rpc`, and action serialization is delegated to `@windstack/contract` and `@windstack/abi`. +The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Network requests are delegated to `@windstack/rpc`; ABI loading and action serialization are delegated to `@windstack/contract` and `@windstack/abi`. + +Action builders return serialized contract actions. They do not broadcast transactions themselves. Signing and broadcast are performed by `@windstack/antelope` or a compatible session layer. ## License From 96845086d635bb19332092d9b808c5c89d13049b Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:24:01 +0700 Subject: [PATCH 39/49] fix(contract): support Vexanium symbol table scopes --- packages/contract/src/index.ts | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index 449c7ee..be0026d 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -15,6 +15,7 @@ export type ContractAction = { data: string; }; export type AuthorizationInput = PermissionLevel | `${string}@${string}`; +export type TableScope = string | number | bigint; function validateName(value: string, label: string): string { if (!value) throw new TypeError(`${label} is required`); @@ -22,6 +23,25 @@ function validateName(value: string, label: string): string { return value; } +function normalizeTableScope(value: TableScope): string { + if (typeof value === "bigint") { + if (value < 0n || value > 0xffffffffffffffffn) { + throw new RangeError("Table scope integer must fit in uint64"); + } + return value.toString(); + } + if (typeof value === "number") { + if (!Number.isSafeInteger(value) || value < 0) { + throw new RangeError("Numeric table scope must be a non-negative safe integer"); + } + return String(value); + } + if (typeof value !== "string" || !value.trim()) { + throw new TypeError("Table scope must be a non-empty string or non-negative integer"); + } + return value; +} + export class AbiCache { readonly #entries = new Map(); @@ -124,13 +144,20 @@ export class Contract { tableRows>( table: string, - scope: string = this.account, + scope: TableScope = this.account, options: Omit = {}, signal?: AbortSignal, ): Promise> { validateName(table, "Table name"); - validateName(scope, "Table scope"); - return this.rpc.getTableRows({ code: this.account, scope, table, ...options }, signal); + return this.rpc.getTableRows( + { + code: this.account, + scope: normalizeTableScope(scope), + table, + ...options, + }, + signal, + ); } } From 85ca53b300c8cc0132a86a50b1bc9ee7ef3b6702 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:24:55 +0700 Subject: [PATCH 40/49] fix(account): allow root permission parent name --- packages/account/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index 4b20bc3..b781073 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -356,7 +356,7 @@ export class AccountClient { { account: this.name, permission: validateName(permission, "Permission"), - parent: validateName(parent, "Parent permission"), + parent: validateOptionalName(parent, "Parent permission"), auth: authority, authorized_by: authorizedBy ? validateName(authorizedBy, "Authorized-by permission") From c1c922bf60aaef8c6fea82ba909bba4609c640ab Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:25:43 +0700 Subject: [PATCH 41/49] chore(release): enforce professional public documentation --- scripts/check-release.mjs | 47 +++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs index ea9d80e..64becbe 100644 --- a/scripts/check-release.mjs +++ b/scripts/check-release.mjs @@ -11,12 +11,21 @@ const forbiddenMarkdown = [ { pattern: /\bengineering\b/i, label: "engineering wording" }, { pattern: /\btechnical\b/i, label: "technical wording" }, { pattern: /\bteknis\b/i, label: "teknis wording" }, + { pattern: /\bEOSIO\b/i, label: "EOSIO branding" }, + { pattern: /\bEOS\b/i, label: "EOS branding" }, { pattern: /Publish native packages from VPS/i, label: "deployment note" }, { pattern: /npm whoami/i, label: "npm authentication note" }, { pattern: /release:npm/i, label: "release command" }, { pattern: /not (?:a )?WharfKit fork/i, label: "implementation comparison" }, ]; -const requiredReadmeSections = ["## Overview", "## Installation", "## Usage", "## Runtime", "## License"]; +const requiredReadmeSections = [ + "## Overview", + "## Installation", + "## Usage", + "## Runtime", + "## License", +]; +const requiredMarkdownSections = ["## Overview", "## License"]; const header = "Created by Gilang Ramadan"; async function readJson(relativePath) { @@ -43,13 +52,23 @@ const manifests = new Map(); for (const packageDirectory of nativePackages) { const manifest = await readJson(`packages/${packageDirectory}/package.json`); manifests.set(manifest.name, manifest); - assert.equal(manifest.version, rootPackage.version, `${manifest.name} must use the root release version`); + assert.equal( + manifest.version, + rootPackage.version, + `${manifest.name} must use the root release version`, + ); assert.equal(manifest.author, "Gilang Ramadan", `${manifest.name} author must be Gilang Ramadan`); assert.equal(manifest.license, "MIT", `${manifest.name} must use MIT`); assert.equal(manifest.publishConfig?.access, "public", `${manifest.name} must publish as public`); assert.equal(manifest.sideEffects, false, `${manifest.name} must declare sideEffects=false`); - assert.ok(Array.isArray(manifest.files) && manifest.files.includes("dist"), `${manifest.name} must publish dist`); - assert.ok(manifest.files.includes("README.md") && manifest.files.includes("LICENSE"), `${manifest.name} must publish documentation and license`); + assert.ok( + Array.isArray(manifest.files) && manifest.files.includes("dist"), + `${manifest.name} must publish dist`, + ); + assert.ok( + manifest.files.includes("README.md") && manifest.files.includes("LICENSE"), + `${manifest.name} must publish documentation and license`, + ); const readme = await readFile(path.join(root, `packages/${packageDirectory}/README.md`), "utf8"); assert.ok(readme.startsWith(`# ${manifest.name}\n`), `${manifest.name} README title is invalid`); @@ -64,7 +83,11 @@ for (const [name, manifest] of manifests) { assert.ok(!dependency.startsWith("@wharfkit/"), `${name} cannot depend on ${dependency}`); assert.ok(!forbiddenDependencies.includes(dependency), `${name} cannot depend on ${dependency}`); if (manifests.has(dependency)) { - assert.equal(version, rootPackage.version, `${name} must pin ${dependency} to ${rootPackage.version}`); + assert.equal( + version, + rootPackage.version, + `${name} must pin ${dependency} to ${rootPackage.version}`, + ); } } } @@ -76,6 +99,7 @@ const sourceChecks = [ "packages/contract/src/index.ts", "packages/account/src/index.ts", "packages/antelope/src/index.ts", + "packages/antelope/src/vexanium.ts", "packages/session/src/index.ts", "packages/session/src/native.ts", "packages/session/src/compat.ts", @@ -88,14 +112,23 @@ for (const relativePath of sourceChecks) { const markdownFiles = (await walk(root)).filter((file) => file.endsWith(".md")); for (const file of markdownFiles) { const content = await readFile(file, "utf8"); + const relativePath = path.relative(root, file); for (const { pattern, label } of forbiddenMarkdown) { - assert.ok(!pattern.test(content), `${path.relative(root, file)} contains ${label}`); + assert.ok(!pattern.test(content), `${relativePath} contains ${label}`); + } + for (const section of requiredMarkdownSections) { + assert.ok(content.includes(section), `${relativePath} is missing ${section}`); } + assert.ok(content.includes(header), `${relativePath} must credit Gilang Ramadan`); } const lock = await readJson("package-lock.json"); assert.equal(lock.version, rootPackage.version, "package-lock root version must match package.json"); -assert.equal(lock.packages?.[""]?.version, rootPackage.version, "package-lock root package version is stale"); +assert.equal( + lock.packages?.[""]?.version, + rootPackage.version, + "package-lock root package version is stale", +); for (const packageDirectory of nativePackages) { const manifest = await readJson(`packages/${packageDirectory}/package.json`); assert.equal( From 0a36278c60da2486d71765a4ef67d7032587792f Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:26:03 +0700 Subject: [PATCH 42/49] docs: use canonical Vexanium preset --- README.md | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9b15ded..2003294 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,30 @@ # WindStack SDK -WindStack provides TypeScript packages for Antelope applications, Vexanium, Wisp Wallet, EVM providers, and Solana providers. +WindStack provides TypeScript packages for Vexanium and Antelope applications, Wisp Wallet, EVM providers, and Solana providers. Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. ## Overview -The Antelope packages are separated by responsibility so applications can install only the capabilities they need. +The Vexanium and Antelope packages are separated by responsibility so applications can install only the capabilities they need. | Package | Purpose | | --- | --- | -| `@windstack/crypto` | K1 and R1 keys, Antelope key formats, signatures, verification, and public-key recovery | -| `@windstack/abi` | ABI serialization and deserialization for Antelope values, actions, tables, structs, variants, and binary extensions | -| `@windstack/rpc` | Typed Antelope chain RPC with endpoint failover, request timeouts, cancellation, and structured errors | +| `@windstack/crypto` | K1 and R1 keys, signatures, verification, recovery, and Vexanium-compatible key encoding | +| `@windstack/abi` | ABI serialization and deserialization for actions, tables, structs, variants, and binary extensions | +| `@windstack/rpc` | Chain RPC with endpoint failover, request timeouts, cancellation, and structured errors | | `@windstack/contract` | Contract ABI loading, action serialization, table queries, and shared ABI caching | -| `@windstack/account` | Account queries, token transfers, staking actions, RAM actions, and configurable chain contracts | -| `@windstack/antelope` | Transaction construction, TAPOS, signing digests, required-key resolution, signing, and broadcast | -| `@windstack/session` | Wallet plugins, authenticated Antelope sessions, persistence, restore, and transaction orchestration | +| `@windstack/account` | VEX balances, transfers, staking, RAM, voting, producers, accounts, and permissions | +| `@windstack/antelope` | TAPOS, transaction serialization, signing digests, required keys, signing, and broadcast | +| `@windstack/session` | Wallet plugins, sessions, persistence, restore, and transaction orchestration | -Additional packages provide Wisp provider interfaces and chain-specific helpers for Vexanium, EVM, and Solana applications. +Vexanium Mainnet is available as a first-class preset with the canonical chain ID, RPC endpoint, `vexcore` system contract, `vex.token` native token contract, `VEX` symbol, and precision `4`. + +Additional packages provide Wisp provider interfaces and chain-specific helpers for VEX Native, VEX EVM, and Solana wallet integrations. ## Installation -Install the high-level Antelope client and session package: +Install the high-level transaction client and session package: ```bash npm install @windstack/antelope @windstack/session @@ -32,23 +34,21 @@ Individual packages can also be installed independently. ## Usage -The example below configures Vexanium Mainnet explicitly, including its chain ID and system contracts. - ```ts import { - AntelopeClient, PrivateKey, PrivateKeySigner, } from "@windstack/antelope"; +import { + VEXANIUM_MAINNET, + createVexaniumClient, +} from "@windstack/antelope/vexanium"; -const client = new AntelopeClient({ - endpoints: ["https://api.windcrypto.com"], - chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", - contracts: { - system: "vexcore", - token: "vex.token", - }, -}); +const client = createVexaniumClient(); + +console.log(VEXANIUM_MAINNET.contracts.system); // vexcore +console.log(VEXANIUM_MAINNET.contracts.token); // vex.token +console.log(VEXANIUM_MAINNET.nativeToken.symbol); // VEX const signer = new PrivateKeySigner([ PrivateKey.fromString("PVT_K1_..."), @@ -66,13 +66,15 @@ const result = await client.transact({ console.log(result.response); ``` -Applications should keep private keys in an appropriate secure storage or signing service. A wallet integration can provide its own `Signer` implementation instead of exposing private keys to application code. +Applications should keep private keys in an appropriate secure storage or signing service. Wallet integrations can provide their own `Signer` implementation so application code never receives private-key material. ## Runtime -The Antelope packages are ESM-first and use Web-standard primitives such as `Uint8Array`, `TextEncoder`, `fetch`, `AbortController`, and secure platform randomness. Node.js 20.19 or newer is supported. Browser and React Native environments must provide the Web APIs used by the selected package. +The seven release packages are ESM-first and use Web-standard primitives such as `Uint8Array`, `TextEncoder`, `fetch`, `AbortController`, and secure platform randomness. Node.js 20.19 or newer is supported. Browser and React Native environments must provide the Web APIs used by the selected package. + +K1 and R1 cryptographic operations are provided by the Noble libraries. The seven-package transaction stack does not depend on `elliptic`, `bn.js`, or Node.js crypto polyfills. -K1 and R1 cryptographic operations are provided by the Noble libraries. The Antelope package graph does not depend on `elliptic`, `bn.js`, or Node.js crypto polyfills. +Release validation checks package metadata, formatting, documentation, dependency boundaries, tests, package contents, and the current production ABIs for `vexcore` and `vex.token`. ## License From 99d25268479682d60ccda5d0ef36fbea6e2c8b15 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:28:57 +0700 Subject: [PATCH 43/49] fix(antelope): harden context-free data and signer validation --- packages/antelope/src/index.ts | 102 ++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 20 deletions(-) diff --git a/packages/antelope/src/index.ts b/packages/antelope/src/index.ts index c8dffc0..1188e05 100644 --- a/packages/antelope/src/index.ts +++ b/packages/antelope/src/index.ts @@ -41,6 +41,7 @@ export type SignRequest = { chainId: string; transaction: Transaction; serializedTransaction: Uint8Array; + serializedContextFreeData: Uint8Array; digest: Uint8Array; requiredKeys: string[]; }; @@ -52,7 +53,7 @@ export type TransactArgs = { actions: Action[]; signer: Signer; contextFreeActions?: Action[]; - contextFreeData?: Uint8Array; + contextFreeData?: Uint8Array[]; transactionExtensions?: TransactionExtension[]; broadcast?: boolean; expireSeconds?: number; @@ -61,6 +62,7 @@ export type TransactArgs = { export type TransactResult> = { transaction: Transaction; serializedTransaction: Uint8Array; + serializedContextFreeData: Uint8Array; signatures: string[]; response?: T; }; @@ -121,7 +123,9 @@ export function serializeTransaction(transaction: Transaction): Uint8Array { writer.writeUint32(assertUint(expirationSeconds, 0xffffffff, "expiration")); writer.writeUint16(assertUint(transaction.ref_block_num, 0xffff, "ref_block_num")); writer.writeUint32(assertUint(transaction.ref_block_prefix, 0xffffffff, "ref_block_prefix")); - writer.writeVarUint(assertUint(transaction.max_net_usage_words, 0xffffffff, "max_net_usage_words")); + writer.writeVarUint( + assertUint(transaction.max_net_usage_words, 0xffffffff, "max_net_usage_words"), + ); writer.writeByte(assertUint(transaction.max_cpu_usage_ms, 0xff, "max_cpu_usage_ms")); writer.writeVarUint(assertUint(transaction.delay_sec, 0xffffffff, "delay_sec")); writer.writeVarUint(transaction.context_free_actions.length); @@ -136,17 +140,36 @@ export function serializeTransaction(transaction: Transaction): Uint8Array { return writer.toBytes(); } +export function serializeContextFreeData(items: Uint8Array[]): Uint8Array { + if (!Array.isArray(items)) throw new TypeError("Context-free data must be an array"); + const writer = new BinaryWriter(); + writer.writeVarUint(items.length); + for (const item of items) { + if (!(item instanceof Uint8Array)) { + throw new TypeError("Each context-free data item must be Uint8Array"); + } + writer.writeVarBytes(item); + } + return writer.toBytes(); +} + export function transactionDigest( chainId: string, serializedTransaction: Uint8Array, contextFreeDataHash = new Uint8Array(32), ): Uint8Array { - const id = cryptoHexToBytes(chainId); - if (id.length !== 32) throw new TypeError("Antelope chain id must be 32 bytes"); + if (!/^[0-9a-f]{64}$/i.test(chainId)) { + throw new TypeError("Antelope chain id must be exactly 64 hexadecimal characters"); + } + if (!(serializedTransaction instanceof Uint8Array)) { + throw new TypeError("Serialized transaction must be Uint8Array"); + } if (!(contextFreeDataHash instanceof Uint8Array) || contextFreeDataHash.length !== 32) { throw new TypeError("Context-free data hash must be 32 bytes"); } - return sha256Digest(concatBytes(id, serializedTransaction, contextFreeDataHash)); + return sha256Digest( + concatBytes(cryptoHexToBytes(chainId), serializedTransaction, contextFreeDataHash), + ); } export type PrivateKeySignerOptions = { @@ -198,9 +221,8 @@ export class AntelopeClient { readonly contracts: Readonly; constructor(options: AntelopeClientOptions) { - if (options.chainId) { - const chainId = cryptoHexToBytes(options.chainId); - if (chainId.length !== 32) throw new TypeError("Configured Antelope chain id must be 32 bytes"); + if (options.chainId && !/^[0-9a-f]{64}$/i.test(options.chainId)) { + throw new TypeError("Configured Antelope chain id must be exactly 64 hexadecimal characters"); } this.rpc = new RpcClient(options); this.abiCache = options.abiCache ?? new AbiCache(); @@ -221,7 +243,10 @@ export class AntelopeClient { } async transact>(args: TransactArgs): Promise> { - if (!args.actions.length) throw new TypeError("Transaction must include at least one action"); + const contextFreeActions = args.contextFreeActions ?? []; + if (!args.actions.length && !contextFreeActions.length) { + throw new TypeError("Transaction must include at least one action"); + } const expireSeconds = args.expireSeconds ?? 120; if (!Number.isInteger(expireSeconds) || expireSeconds < 1 || expireSeconds > 3600) { throw new RangeError("expireSeconds must be an integer between 1 and 3600"); @@ -247,52 +272,89 @@ export class AntelopeClient { max_net_usage_words: 0, max_cpu_usage_ms: 0, delay_sec: 0, - context_free_actions: args.contextFreeActions ?? [], + context_free_actions: contextFreeActions, actions: args.actions, transaction_extensions: args.transactionExtensions ?? [], }; const serializedTransaction = serializeTransaction(transaction); - const contextFreeDataHash = args.contextFreeData?.length - ? sha256Digest(args.contextFreeData) + const contextFreeData = args.contextFreeData ?? []; + const serializedContextFreeData = contextFreeData.length + ? serializeContextFreeData(contextFreeData) + : new Uint8Array(); + const contextFreeDataHash = serializedContextFreeData.length + ? sha256Digest(serializedContextFreeData) : new Uint8Array(32); const digest = transactionDigest(actualChainId, serializedTransaction, contextFreeDataHash); - const availableKeys = await args.signer.getAvailableKeys(); + + const availableKeys = [...new Set(await args.signer.getAvailableKeys())]; if (!availableKeys.length) throw new Error("Signer returned no available keys"); + for (const key of availableKeys) PublicKey.fromString(key); + const { required_keys: requiredKeys } = await this.rpc.getRequiredKeys( transactionForRpc(transaction), availableKeys, args.signal, ); + if (!Array.isArray(requiredKeys)) throw new TypeError("RPC returned invalid required keys"); + const normalizedRequiredKeys = requiredKeys.map((key) => PublicKey.fromString(key).toString()); + if (new Set(normalizedRequiredKeys).size !== normalizedRequiredKeys.length) { + throw new TypeError("RPC returned duplicate required keys"); + } + const signed = await args.signer.sign({ chainId: actualChainId, transaction, serializedTransaction, + serializedContextFreeData, digest, requiredKeys, }); - const signatures = signed.map((signature) => - typeof signature === "string" ? Signature.fromString(signature).toString() : signature.toString(), + const parsedSignatures = signed.map((value) => + typeof value === "string" ? Signature.fromString(value) : value, ); - if (signatures.length !== requiredKeys.length) { + if (parsedSignatures.length !== requiredKeys.length) { throw new Error( - `Signer returned ${signatures.length} signatures for ${requiredKeys.length} required keys`, + `Signer returned ${parsedSignatures.length} signatures for ${requiredKeys.length} required keys`, ); } + const recoveredKeys = parsedSignatures.map((signature) => + signature.recoverDigest(digest).toString(), + ); + const requiredSet = new Set(normalizedRequiredKeys); + if ( + new Set(recoveredKeys).size !== recoveredKeys.length || + recoveredKeys.some((key) => !requiredSet.has(key)) + ) { + throw new Error("Signer returned a signature that does not match the required keys"); + } + + const signatures = parsedSignatures.map((signature) => signature.toString()); if (args.broadcast === false) { - return { transaction, serializedTransaction, signatures }; + return { + transaction, + serializedTransaction, + serializedContextFreeData, + signatures, + }; } const response = await this.rpc.pushTransaction( { signatures, compression: 0, - packed_context_free_data: args.contextFreeData ? bytesToHex(args.contextFreeData) : "", + packed_context_free_data: bytesToHex(serializedContextFreeData), packed_trx: bytesToHex(serializedTransaction), }, args.signal, ); - return { transaction, serializedTransaction, signatures, response }; + return { + transaction, + serializedTransaction, + serializedContextFreeData, + signatures, + response, + }; } } From b58d231ce5917ee72e0a89387c54115c859dfddd Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:29:56 +0700 Subject: [PATCH 44/49] fix(session): harden login restore and persisted state --- packages/session/src/native.ts | 132 ++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 20 deletions(-) diff --git a/packages/session/src/native.ts b/packages/session/src/native.ts index db432df..a537dae 100644 --- a/packages/session/src/native.ts +++ b/packages/session/src/native.ts @@ -51,14 +51,32 @@ export class MemorySessionStorage implements SessionStorage { } } +function validateNonEmptyName(value: string, label: string): string { + if (typeof value !== "string" || !value) throw new TypeError(`${label} is required`); + nameToBigInt(value); + return value; +} + function validateIdentity(identity: SessionIdentity): SessionIdentity { if (!identity || typeof identity !== "object") throw new TypeError("Wallet returned no identity"); - nameToBigInt(identity.actor); - nameToBigInt(identity.permission); + const actor = validateNonEmptyName(identity.actor, "Wallet identity actor"); + const permission = validateNonEmptyName(identity.permission, "Wallet identity permission"); if (identity.publicKey !== undefined && typeof identity.publicKey !== "string") { throw new TypeError("Wallet identity publicKey must be a string"); } - return { ...identity }; + return Object.freeze({ ...identity, actor, permission }); +} + +function validateSigner(signer: Signer): Signer { + if ( + !signer || + typeof signer !== "object" || + typeof signer.getAvailableKeys !== "function" || + typeof signer.sign !== "function" + ) { + throw new TypeError("Wallet returned an invalid signer"); + } + return signer; } function validateChain(chain: SessionChain): SessionChain { @@ -69,7 +87,36 @@ function validateChain(chain: SessionChain): SessionChain { if (!urls.length || urls.some((url) => typeof url !== "string" || !url.trim())) { throw new TypeError("Session chain requires at least one RPC URL"); } - return { ...chain, id: chain.id.toLowerCase(), url: Array.isArray(chain.url) ? [...chain.url] : chain.url }; + const contracts = chain.contracts + ? Object.freeze({ + ...(chain.contracts.system + ? { system: validateNonEmptyName(chain.contracts.system, "System contract") } + : {}), + ...(chain.contracts.token + ? { token: validateNonEmptyName(chain.contracts.token, "Token contract") } + : {}), + }) + : undefined; + return Object.freeze({ + ...chain, + id: chain.id.toLowerCase(), + url: Array.isArray(chain.url) ? Object.freeze([...chain.url]) : chain.url, + contracts, + }); +} + +function validatePlugin(plugin: WalletPlugin): WalletPlugin { + if ( + !plugin || + typeof plugin !== "object" || + typeof plugin.id !== "string" || + !plugin.id.trim() || + plugin.id !== plugin.id.trim() || + typeof plugin.login !== "function" + ) { + throw new TypeError("Wallet plugins require a non-empty stable id and login function"); + } + return plugin; } export class Session { @@ -87,8 +134,8 @@ export class Session { }) { this.chain = validateChain(args.chain); this.identity = validateIdentity(args.identity); - this.walletPlugin = args.walletPlugin; - this.signer = args.signer; + this.walletPlugin = validatePlugin(args.walletPlugin); + this.signer = validateSigner(args.signer); this.client = new AntelopeClient({ endpoints: this.chain.url, chainId: this.chain.id, @@ -152,14 +199,18 @@ export class SessionKit { if (!options.chains.length) throw new TypeError("SessionKit requires at least one chain"); if (!options.walletPlugins.length) throw new TypeError("SessionKit requires at least one wallet plugin"); const chains = options.chains.map(validateChain); + const plugins = options.walletPlugins.map(validatePlugin); const chainIds = new Set(chains.map((chain) => chain.id)); if (chainIds.size !== chains.length) throw new TypeError("SessionKit chain ids must be unique"); - const pluginIds = new Set(options.walletPlugins.map((plugin) => plugin.id)); - if (pluginIds.size !== options.walletPlugins.length || pluginIds.has("")) { - throw new TypeError("SessionKit wallet plugin ids must be non-empty and unique"); + const pluginIds = new Set(plugins.map((plugin) => plugin.id)); + if (pluginIds.size !== plugins.length) { + throw new TypeError("SessionKit wallet plugin ids must be unique"); + } + if (options.storageKey !== undefined && !options.storageKey.trim()) { + throw new TypeError("Session storage key must be non-empty"); } - this.chains = chains; - this.walletPlugins = [...options.walletPlugins]; + this.chains = Object.freeze(chains); + this.walletPlugins = Object.freeze(plugins); this.appName = options.appName; this.storage = options.storage ?? new MemorySessionStorage(); this.storageKey = options.storageKey ?? "windstack:session"; @@ -170,6 +221,9 @@ export class SessionKit { } async login(options: { chainId?: string; walletPluginId?: string } = {}): Promise { + if (this.#session) { + throw new Error("A wallet session is already active; logout before starting another session"); + } const requestedChainId = options.chainId?.toLowerCase(); const chain = requestedChainId ? this.chains.find((item) => item.id === requestedChainId) @@ -185,17 +239,31 @@ export class SessionKit { chain, identity: validateIdentity(result.identity), walletPlugin: plugin, - signer: result.signer, + signer: validateSigner(result.signer), }); - await this.storage.set( - this.storageKey, - JSON.stringify({ chainId: chain.id, walletPluginId: plugin.id, identity: session.identity }), - ); + try { + await this.storage.set( + this.storageKey, + JSON.stringify({ + chainId: chain.id, + walletPluginId: plugin.id, + identity: session.identity, + }), + ); + } catch (error) { + if (plugin.logout) { + await plugin + .logout({ chain, appName: this.appName, identity: session.identity }) + .catch(() => undefined); + } + throw error; + } this.#session = session; return session; } async restore(): Promise { + if (this.#session) return this.#session; const stored = await this.getStoredSession(); if (!stored) return null; const chain = this.chains.find((item) => item.id === stored.chainId.toLowerCase()); @@ -211,14 +279,23 @@ export class SessionKit { chain, identity: validateIdentity(result.identity), walletPlugin: plugin, - signer: result.signer, + signer: validateSigner(result.signer), }); + await this.storage.set( + this.storageKey, + JSON.stringify({ + chainId: chain.id, + walletPluginId: plugin.id, + identity: session.identity, + }), + ); this.#session = session; return session; } async logout(): Promise { const session = this.#session; + let logoutError: unknown; try { if (session?.walletPlugin.logout) { await session.walletPlugin.logout({ @@ -227,10 +304,23 @@ export class SessionKit { identity: session.identity, }); } - } finally { - this.#session = null; + } catch (error) { + logoutError = error; + } + + this.#session = null; + let storageError: unknown; + try { await this.storage.remove(this.storageKey); + } catch (error) { + storageError = error; + } + + if (logoutError && storageError) { + throw new AggregateError([logoutError, storageError], "Wallet logout and session cleanup failed"); } + if (logoutError) throw logoutError; + if (storageError) throw storageError; } async getStoredSession(): Promise { @@ -240,14 +330,16 @@ export class SessionKit { const value = JSON.parse(raw) as Partial; if ( typeof value.chainId !== "string" || + !/^[0-9a-f]{64}$/i.test(value.chainId) || typeof value.walletPluginId !== "string" || + !value.walletPluginId.trim() || typeof value.identity !== "object" || !value.identity ) { return null; } return { - chainId: value.chainId, + chainId: value.chainId.toLowerCase(), walletPluginId: value.walletPluginId, identity: validateIdentity(value.identity), }; From 8dc3b207b689307c3687cd32ce275a3a73eeca03 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 08:30:52 +0700 Subject: [PATCH 45/49] fix(session): remove insecure session id fallback --- packages/session/src/compat.ts | 190 +++++++++++++++++++++++++++------ 1 file changed, 159 insertions(+), 31 deletions(-) diff --git a/packages/session/src/compat.ts b/packages/session/src/compat.ts index ed06854..3c02265 100644 --- a/packages/session/src/compat.ts +++ b/packages/session/src/compat.ts @@ -21,18 +21,27 @@ export type LegacyWispSession = { type EVMCompatClient = { connect(): Promise; getChainId(): Promise; - request(args: LegacyRequestArguments): Promise; + request( + args: LegacyRequestArguments, + ): Promise; disconnect?: () => Promise; }; type SolanaCompatClient = { connect(): Promise>; - request(args: LegacyRequestArguments): Promise; + request( + args: LegacyRequestArguments, + ): Promise; disconnect(): Promise; }; type VexaniumCompatClient = { - connect(args: { chainId: string; dapp?: unknown }): Promise>; + connect(args: { + chainId: string; + dapp?: unknown; + }): Promise>; getSession(): { chainId: string; walletSessionId?: string } | null; - request(args: LegacyRequestArguments): Promise; + request( + args: LegacyRequestArguments, + ): Promise; disconnect(): Promise; }; export type LegacyWispSessionClientOptions = { @@ -41,25 +50,57 @@ export type LegacyWispSessionClientOptions = { solana?: SolanaCompatClient; vexanium?: VexaniumCompatClient; }; -export type LegacyWispInvokeArgs = { scope: LegacyWispScope; request: LegacyRequestArguments }; +export type LegacyWispInvokeArgs = { + scope: LegacyWispScope; + request: LegacyRequestArguments; +}; function providerError(code: number, message: string): Error & { code: number } { return Object.assign(new Error(message), { code }); } + function evmScopeFromHexChainId(chainId: string): `eip155:${number}` { - if (!/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(chainId)) throw providerError(-32603, `Provider returned invalid EVM chain ID: ${chainId}`); + if (!/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(chainId)) { + throw providerError(-32603, `Provider returned invalid EVM chain ID: ${chainId}`); + } return `eip155:${BigInt(chainId).toString()}` as `eip155:${number}`; } + function cloneSession(session: LegacyWispSession | null): LegacyWispSession | null { - return session ? { ...session, scopes: [...session.scopes], accounts: session.accounts.map((account) => ({ ...account })) } : null; + return session + ? { + ...session, + scopes: [...session.scopes], + accounts: session.accounts.map((account) => ({ ...account })), + } + : null; +} + +function secureSessionEntropy(): string { + const crypto = globalThis.crypto; + if (crypto?.randomUUID) return crypto.randomUUID(); + if (crypto?.getRandomValues) { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); + } + throw new Error("Secure platform randomness is required to create a wallet session id"); +} + +export function isEVMScope(scope: string): scope is `eip155:${number}` { + return /^eip155:(?:0|[1-9]\d*)$/.test(scope); +} + +export function isVexaniumScope(scope: string): scope is `antelope:${string}` { + return /^antelope:[0-9a-f]{32}$/.test(scope); +} + +export function isSolanaScope(scope: string): scope is `solana:${string}` { + return /^solana:[a-zA-Z0-9_-]+$/.test(scope); } -export function isEVMScope(scope: string): scope is `eip155:${number}` { return /^eip155:(?:0|[1-9]\d*)$/.test(scope); } -export function isVexaniumScope(scope: string): scope is `antelope:${string}` { return /^antelope:[0-9a-f]{32}$/.test(scope); } -export function isSolanaScope(scope: string): scope is `solana:${string}` { return /^solana:[a-zA-Z0-9_-]+$/.test(scope); } export function createSessionId(scopes: string[]): string { - const randomPart = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2); - return `wisp:${Date.now().toString(36)}:${scopes.join(",")}:${randomPart}`; + return `wisp:${Date.now().toString(36)}:${scopes.join(",")}:${secureSessionEntropy()}`; } export async function createWispSessionClient(options: LegacyWispSessionClientOptions = {}) { @@ -69,26 +110,52 @@ export async function createWispSessionClient(options: LegacyWispSessionClientOp let session: LegacyWispSession | null = null; const getEVM = async (): Promise => { - if (!evm) { const module = await import("@windstack/evm"); evm = await module.createEVMClient() as EVMCompatClient; } + if (!evm) { + const module = await import("@windstack/evm"); + evm = (await module.createEVMClient()) as EVMCompatClient; + } return evm; }; + const getSolana = async (): Promise => { - if (!solana) { const module = await import("@windstack/solana"); solana = await module.createSolanaClient() as SolanaCompatClient; } + if (!solana) { + const module = await import("@windstack/solana"); + solana = (await module.createSolanaClient()) as SolanaCompatClient; + } return solana; }; + const getVexanium = async (): Promise => { - if (!vexanium) { const module = await import("@windstack/vexanium"); vexanium = await module.createVexaniumClient({ dapp: options.dapp as never }) as VexaniumCompatClient; } + if (!vexanium) { + const module = await import("@windstack/vexanium"); + vexanium = (await module.createVexaniumClient({ + dapp: options.dapp as never, + })) as VexaniumCompatClient; + } return vexanium; }; + const disconnectAll = async (): Promise => { - await Promise.allSettled([evm?.disconnect?.(), solana?.disconnect(), vexanium?.disconnect()].filter(Boolean) as Promise[]); + await Promise.allSettled( + [evm?.disconnect?.(), solana?.disconnect(), vexanium?.disconnect()].filter( + Boolean, + ) as Promise[], + ); }; return { async connect(scopes: LegacyWispScope[]): Promise { const unique = [...new Set(scopes)]; - if (!unique.length || !unique.every((scope) => isEVMScope(scope) || isSolanaScope(scope) || isVexaniumScope(scope))) throw providerError(-32602, "One or more wallet scopes are invalid"); + if ( + !unique.length || + !unique.every( + (scope) => isEVMScope(scope) || isSolanaScope(scope) || isVexaniumScope(scope), + ) + ) { + throw providerError(-32602, "One or more wallet scopes are invalid"); + } if (session) throw providerError(-32002, "A Wisp session is already active"); + const accounts: LegacySessionAccount[] = []; try { const evmScope = unique.find(isEVMScope); @@ -96,33 +163,94 @@ export async function createWispSessionClient(options: LegacyWispSessionClientOp const client = await getEVM(); const connected = await client.connect(); const activeScope = evmScopeFromHexChainId(await client.getChainId()); - if (activeScope !== evmScope) throw providerError(-32602, `EVM provider is connected to ${activeScope}, not requested scope ${evmScope}`); + if (activeScope !== evmScope) { + throw providerError( + -32602, + `EVM provider is connected to ${activeScope}, not requested scope ${evmScope}`, + ); + } for (const address of connected) accounts.push({ scope: evmScope, address }); } + const solanaScope = unique.find(isSolanaScope); - if (solanaScope) for (const account of await (await getSolana()).connect()) accounts.push({ scope: solanaScope, address: account.publicKey, label: account.label }); + if (solanaScope) { + for (const account of await (await getSolana()).connect()) { + accounts.push({ + scope: solanaScope, + address: account.publicKey, + label: account.label, + }); + } + } + const vexScope = unique.find(isVexaniumScope); if (vexScope) { const client = await getVexanium(); const connected = await client.connect({ chainId: vexScope, dapp: options.dapp }); const active = client.getSession()?.chainId; - if (!active || !(active === vexScope || active.endsWith(vexScope.slice("antelope:".length)))) throw providerError(-32603, "Vexanium provider returned the wrong chain"); - for (const account of connected) accounts.push({ scope: vexScope, address: account.permissionLevel, label: account.label }); + if ( + !active || + !(active === vexScope || active.endsWith(vexScope.slice("antelope:".length))) + ) { + throw providerError(-32603, "Vexanium provider returned the wrong chain"); + } + for (const account of connected) { + accounts.push({ + scope: vexScope, + address: account.permissionLevel, + label: account.label, + }); + } } - } catch (error) { await disconnectAll(); throw error; } - if (!accounts.length) { await disconnectAll(); throw providerError(4100, "No wallet accounts were authorized"); } + } catch (error) { + await disconnectAll(); + throw error; + } + + if (!accounts.length) { + await disconnectAll(); + throw providerError(4100, "No wallet accounts were authorized"); + } + const now = Date.now(); - session = { id: createSessionId(unique), dapp: options.dapp, origin: typeof globalThis.location?.origin === "string" ? globalThis.location.origin : undefined, scopes: unique, accounts, createdAt: now, updatedAt: now }; + session = { + id: createSessionId(unique), + dapp: options.dapp, + origin: + typeof globalThis.location?.origin === "string" ? globalThis.location.origin : undefined, + scopes: unique, + accounts, + createdAt: now, + updatedAt: now, + }; return cloneSession(session)!; }, - getSession(): LegacyWispSession | null { return cloneSession(session); }, - async invoke(args: LegacyWispInvokeArgs): Promise { - if (!session || !session.scopes.includes(args.scope)) throw providerError(-32602, "No active session for requested scope"); - if (isEVMScope(args.scope)) return (await getEVM()).request(args.request); - if (isSolanaScope(args.scope)) return (await getSolana()).request(args.request); - if (isVexaniumScope(args.scope)) return (await getVexanium()).request(args.request); + + getSession(): LegacyWispSession | null { + return cloneSession(session); + }, + + async invoke( + args: LegacyWispInvokeArgs, + ): Promise { + if (!session || !session.scopes.includes(args.scope)) { + throw providerError(-32602, "No active session for requested scope"); + } + if (isEVMScope(args.scope)) { + return (await getEVM()).request(args.request); + } + if (isSolanaScope(args.scope)) { + return (await getSolana()).request(args.request); + } + if (isVexaniumScope(args.scope)) { + return (await getVexanium()).request(args.request); + } throw providerError(-32602, `Unsupported scope: ${args.scope}`); }, - async disconnect(): Promise { await disconnectAll(); session = null; }, + + async disconnect(): Promise { + await disconnectAll(); + session = null; + }, }; } From bff1303d40b01b31b5b04b331cbab5a306a28a34 Mon Sep 17 00:00:00 2001 From: Windcrypto Date: Mon, 7 Sep 2026 04:48:51 +0200 Subject: [PATCH 46/49] feat(native): harden crypto ABI RPC and account stack --- packages/abi/README.md | 2 +- packages/abi/package.json | 42 +- packages/abi/src/index.ts | 250 +- packages/abi/tsconfig.json | 6 +- packages/account/README.md | 2 +- packages/account/package.json | 44 +- packages/account/src/index.ts | 96 +- packages/account/tsconfig.json | 8 +- packages/contract/README.md | 2 +- packages/contract/package.json | 43 +- packages/contract/src/index.ts | 61 +- packages/contract/tsconfig.json | 6 +- packages/crypto/README.md | 4 +- packages/crypto/package.json | 45 +- packages/crypto/src/index.ts | 15 + packages/crypto/tsconfig.json | 6 +- packages/rpc/README.md | 6 +- packages/rpc/package.json | 38 +- packages/rpc/src/index.ts | 94 +- packages/rpc/tsconfig.json | 6 +- scripts/fetch-vexanium-abi.mjs | 35 + scripts/test-abi.mjs | 179 ++ scripts/test-contract-account.mjs | 144 + scripts/test-crypto.mjs | 78 + scripts/test-rpc.mjs | 131 + scripts/test-vexanium-abi.mjs | 193 ++ test/fixtures/vexanium/vex.token.abi.json | 276 ++ test/fixtures/vexanium/vexcore.abi.json | 3334 +++++++++++++++++++++ 28 files changed, 5039 insertions(+), 107 deletions(-) create mode 100644 scripts/fetch-vexanium-abi.mjs create mode 100644 scripts/test-abi.mjs create mode 100644 scripts/test-contract-account.mjs create mode 100644 scripts/test-crypto.mjs create mode 100644 scripts/test-rpc.mjs create mode 100644 scripts/test-vexanium-abi.mjs create mode 100644 test/fixtures/vexanium/vex.token.abi.json create mode 100644 test/fixtures/vexanium/vexcore.abi.json diff --git a/packages/abi/README.md b/packages/abi/README.md index 3203854..48cc1bc 100644 --- a/packages/abi/README.md +++ b/packages/abi/README.md @@ -4,7 +4,7 @@ `@windstack/abi` serializes and deserializes Antelope ABI values without requiring Node.js buffer APIs. It supports ABI aliases, structs and inheritance, arrays, optional values, binary extensions, variants, action types, table types, names, assets, symbols, keys, signatures, timestamps, checksums, integer types, floating-point values, and raw bytes. -The package also exposes `BinaryWriter`, `BinaryReader`, `nameToBigInt()`, `bigIntToName()`, `hexToBytes()`, and `bytesToHex()` for applications that need direct access to Antelope binary primitives. +The package also exposes `BinaryWriter`, `BinaryReader`, `nameToBigInt()`, `bigIntToName()`, `parseAsset()`, `formatAsset()`, `hexToBytes()`, and `bytesToHex()` for applications that need direct access to Antelope values and binary primitives. ## Installation diff --git a/packages/abi/package.json b/packages/abi/package.json index c608efe..196e733 100644 --- a/packages/abi/package.json +++ b/packages/abi/package.json @@ -5,14 +5,42 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, - "dependencies": { "@windstack/crypto": "1.0.0" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" + }, + "dependencies": { + "@windstack/crypto": "1.0.0" + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/abi" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/abi" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/abi#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/abi/src/index.ts b/packages/abi/src/index.ts index a51b3f8..bb8941c 100644 --- a/packages/abi/src/index.ts +++ b/packages/abi/src/index.ts @@ -7,15 +7,52 @@ import { PublicKey, Signature } from "@windstack/crypto"; const encoder = new TextEncoder(); -const decoder = new TextDecoder(); +const decoder = new TextDecoder("utf-8", { fatal: true }); const NAME_CHARS = ".12345abcdefghijklmnopqrstuvwxyz"; const BLOCK_TIMESTAMP_EPOCH_MS = Date.UTC(2000, 0, 1); +const PRIMITIVE_TYPES = new Set([ + "asset", + "block_timestamp_type", + "bool", + "bytes", + "checksum160", + "checksum256", + "checksum512", + "extended_asset", + "float128", + "float32", + "float64", + "int128", + "int16", + "int32", + "int64", + "int8", + "name", + "public_key", + "publickey", + "signature", + "string", + "symbol", + "symbol_code", + "time_point", + "time_point_sec", + "uint128", + "uint16", + "uint32", + "uint64", + "uint8", + "varint", + "varint32", + "varuint", + "varuint32", +]); export type AbiField = { name: string; type: string }; export type AbiStruct = { name: string; base?: string; fields: AbiField[] }; export type AbiTypeDef = { new_type_name: string; type: string }; export type AbiAction = { name: string; type: string; ricardian_contract?: string }; export type AbiVariant = { name: string; types: string[] }; +export type AbiActionResult = { name: string; result_type: string }; export type AbiTable = { name: string; index_type: string; @@ -30,6 +67,7 @@ export type Abi = { actions?: AbiAction[]; tables?: AbiTable[]; variants?: AbiVariant[]; + action_results?: AbiActionResult[]; [key: string]: unknown; }; @@ -45,12 +83,19 @@ function assertBigIntRange(value: bigint, bits: number, signed: boolean, label: const min = signed ? -(1n << (width - 1n)) : 0n; const max = signed ? (1n << (width - 1n)) - 1n : (1n << width) - 1n; if (value < min || value > max) { - throw new RangeError(`${label} is outside the ${signed ? "signed" : "unsigned"} ${bits}-bit range`); + throw new RangeError( + `${label} is outside the ${signed ? "signed" : "unsigned"} ${bits}-bit range`, + ); } return value; } function toBigInt(value: unknown, label: string): bigint { + if (typeof value === "number" && !Number.isSafeInteger(value)) { + throw new TypeError( + `${label} must use bigint or a decimal string outside the safe integer range`, + ); + } try { return BigInt(value as string | number | bigint); } catch { @@ -250,11 +295,12 @@ export class BinaryReader { readUint32(): number { return ( - this.readByte() | - (this.readByte() << 8) | - (this.readByte() << 16) | - (this.readByte() << 24) - ) >>> 0; + (this.readByte() | + (this.readByte() << 8) | + (this.readByte() << 16) | + (this.readByte() << 24)) >>> + 0 + ); } readInt32(): number { @@ -333,13 +379,42 @@ export class BinaryReader { } } -function parseAsset(value: string): { amount: bigint; precision: number; symbol: string } { - const match = /^(-?)(\d+)(?:\.(\d+))? ([A-Z]{1,7})$/.exec(value); +export type AssetValue = Readonly<{ + amount: bigint; + precision: number; + symbol: string; + value: string; +}>; + +export function parseAsset(value: string): AssetValue { + const match = /^(-?)(0|[1-9]\d*)(?:\.(\d+))? ([A-Z]{1,7})$/.exec(value); if (!match) throw new TypeError(`Invalid asset: ${value}`); const fraction = match[3] ?? ""; + assertInteger(fraction.length, 0, 18, "asset precision"); const amount = BigInt(`${match[1]}${match[2]}${fraction}`); assertBigIntRange(amount, 64, true, "asset amount"); - return { amount, precision: fraction.length, symbol: match[4]! }; + return Object.freeze({ + amount, + precision: fraction.length, + symbol: match[4]!, + value, + }); +} + +export function formatAsset( + amount: bigint | string | number, + precision: number, + symbol: string, +): string { + const units = assertBigIntRange(toBigInt(amount, "asset amount"), 64, true, "asset amount"); + validateSymbol(symbol); + assertInteger(precision, 0, 18, "symbol precision"); + const negative = units < 0n; + const digits = (negative ? -units : units).toString().padStart(precision + 1, "0"); + const quantity = precision + ? `${digits.slice(0, -precision)}.${digits.slice(-precision)}` + : digits; + return `${negative ? "-" : ""}${quantity} ${symbol}`; } function validateSymbol(symbol: string): string { @@ -363,7 +438,8 @@ function symbolFromBigInt(raw: bigint): { precision: number; symbol: string } { let symbol = ""; while (value > 0n) { const code = Number(value & 0xffn); - if (code === 0 || code < 65 || code > 90) throw new TypeError("Invalid encoded Antelope symbol"); + if (code === 0 || code < 65 || code > 90) + throw new TypeError("Invalid encoded Antelope symbol"); symbol += String.fromCharCode(code); value >>= 8n; } @@ -375,7 +451,8 @@ function timePointToMicros(value: unknown): bigint { if (typeof value === "bigint" || typeof value === "number") { return assertBigIntRange(toBigInt(value, "time_point"), 64, true, "time_point"); } - if (typeof value !== "string") throw new TypeError("time_point expects an ISO timestamp or microseconds"); + if (typeof value !== "string") + throw new TypeError("time_point expects an ISO timestamp or microseconds"); if (/^-?\d+$/.test(value)) return assertBigIntRange(BigInt(value), 64, true, "time_point"); const match = /^(.+T\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})?$/.exec(value); if (!match) throw new TypeError(`Invalid time_point: ${value}`); @@ -397,13 +474,20 @@ function microsToTimePoint(value: bigint): string { function blockTimestampToSlot(value: unknown): number { if (typeof value === "number") return assertInteger(value, 0, 0xffffffff, "block_timestamp_type"); - if (typeof value !== "string") throw new TypeError("block_timestamp_type expects an ISO timestamp or slot number"); - if (/^\d+$/.test(value)) return assertInteger(Number(value), 0, 0xffffffff, "block_timestamp_type"); + if (typeof value !== "string") + throw new TypeError("block_timestamp_type expects an ISO timestamp or slot number"); + if (/^\d+$/.test(value)) + return assertInteger(Number(value), 0, 0xffffffff, "block_timestamp_type"); const timestamp = Date.parse(/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`); if (!Number.isFinite(timestamp) || timestamp < BLOCK_TIMESTAMP_EPOCH_MS) { throw new TypeError(`Invalid block_timestamp_type: ${value}`); } - return assertInteger(Math.floor((timestamp - BLOCK_TIMESTAMP_EPOCH_MS) / 500), 0, 0xffffffff, "block_timestamp_type"); + return assertInteger( + Math.floor((timestamp - BLOCK_TIMESTAMP_EPOCH_MS) / 500), + 0, + 0xffffffff, + "block_timestamp_type", + ); } function decodeAsset(reader: BinaryReader): string { @@ -449,9 +533,102 @@ export class AbiSerializer { throw new TypeError("A valid Antelope ABI is required"); } this.abi = abi; - for (const type of abi.types ?? []) this.#aliases.set(type.new_type_name, type.type); - for (const struct of abi.structs ?? []) this.#structs.set(struct.name, struct); - for (const variant of abi.variants ?? []) this.#variants.set(variant.name, variant); + for (const type of abi.types ?? []) { + if (this.#aliases.has(type.new_type_name)) { + throw new TypeError(`Duplicate ABI alias: ${type.new_type_name}`); + } + this.#aliases.set(type.new_type_name, type.type); + } + for (const struct of abi.structs ?? []) { + if (this.#structs.has(struct.name)) + throw new TypeError(`Duplicate ABI struct: ${struct.name}`); + this.#structs.set(struct.name, struct); + } + for (const variant of abi.variants ?? []) { + if (this.#variants.has(variant.name)) { + throw new TypeError(`Duplicate ABI variant: ${variant.name}`); + } + this.#variants.set(variant.name, variant); + } + this.validate(); + } + + validate(): void { + for (const [name] of this.#aliases) this.#validateTypeReference(name, `alias ${name}`); + + for (const struct of this.#structs.values()) { + if (struct.base) this.#validateBase(struct.name, new Set()); + let extensionStarted = false; + const fieldNames = new Set(); + for (const field of struct.fields) { + if (!field.name || fieldNames.has(field.name)) { + throw new TypeError( + `Invalid or duplicate field in ABI struct ${struct.name}: ${field.name}`, + ); + } + fieldNames.add(field.name); + if (field.type.endsWith("$")) extensionStarted = true; + else if (extensionStarted) { + throw new TypeError(`Binary-extension fields must be last in ABI struct ${struct.name}`); + } + this.#validateTypeReference(field.type, `field ${struct.name}.${field.name}`); + } + } + + for (const variant of this.#variants.values()) { + if (!variant.types.length) + throw new TypeError(`ABI variant ${variant.name} has no alternatives`); + for (const type of variant.types) { + this.#validateTypeReference(type, `variant ${variant.name}`); + } + } + + this.#validateNamedTypes(this.abi.actions ?? [], "action", (item) => item.type); + this.#validateNamedTypes(this.abi.tables ?? [], "table", (item) => item.type); + this.#validateNamedTypes( + this.abi.action_results ?? [], + "action result", + (item) => item.result_type, + ); + } + + #validateNamedTypes( + items: readonly T[], + label: string, + getType: (item: T) => string, + ): void { + const names = new Set(); + for (const item of items) { + if (!item.name || names.has(item.name)) + throw new TypeError(`Invalid or duplicate ABI ${label}: ${item.name}`); + names.add(item.name); + this.#validateTypeReference(getType(item), `${label} ${item.name}`); + } + } + + #validateBase(name: string, seen: Set): void { + if (seen.has(name)) + throw new TypeError(`Cyclic ABI struct inheritance: ${[...seen, name].join(" -> ")}`); + const struct = this.#structs.get(name); + if (!struct?.base) return; + const base = this.resolveType(struct.base); + if (!this.#structs.has(base)) + throw new TypeError(`Unknown ABI base struct ${struct.base} for ${name}`); + this.#validateBase(base, new Set([...seen, name])); + } + + #validateTypeReference(rawType: string, context: string): void { + if (typeof rawType !== "string" || !rawType) + throw new TypeError(`Missing ABI type for ${context}`); + if (rawType.endsWith("[]")) return this.#validateTypeReference(rawType.slice(0, -2), context); + if (rawType.endsWith("?") || rawType.endsWith("$")) { + return this.#validateTypeReference(rawType.slice(0, -1), context); + } + const type = this.resolveType(rawType); + if (type !== rawType) return this.#validateTypeReference(type, context); + if (!PRIMITIVE_TYPES.has(type) && !this.#structs.has(type) && !this.#variants.has(type)) { + throw new TypeError(`Unsupported ABI type ${type} referenced by ${context}`); + } } resolveType(type: string): string { @@ -515,11 +692,16 @@ export class AbiSerializer { return; } if (rawType.endsWith("$")) { - if (value !== null && value !== undefined) this.#encodeType(writer, rawType.slice(0, -1), value); + if (value !== null && value !== undefined) + this.#encodeType(writer, rawType.slice(0, -1), value); return; } const type = this.resolveType(rawType); + if (type !== rawType && /(?:\[\]|\?|\$)$/.test(type)) { + this.#encodeType(writer, type, value); + return; + } const struct = this.#structs.get(type); if (struct) { if (typeof value !== "object" || value === null || Array.isArray(value)) { @@ -533,7 +715,8 @@ export class AbiSerializer { const variant = this.#variants.get(type); if (variant) { - if (value === null || value === undefined) throw new TypeError(`${type} expects a variant value`); + if (value === null || value === undefined) + throw new TypeError(`${type} expects a variant value`); const pair = Array.isArray(value) ? value : [String((value as { type: string }).type), (value as { value: unknown }).value]; @@ -606,7 +789,11 @@ export class AbiSerializer { writer.writeString(value); return; case "bytes": - writer.writeVarBytes(typeof value === "string" ? hexToBytes(value) : assertHexBytes(value, (value as Uint8Array).length, type)); + writer.writeVarBytes( + typeof value === "string" + ? hexToBytes(value) + : assertHexBytes(value, (value as Uint8Array).length, type), + ); return; case "checksum160": writer.writeBytes(assertHexBytes(value, 20, type)); @@ -645,9 +832,12 @@ export class AbiSerializer { writer.writeInt64(timePointToMicros(value)); return; case "time_point_sec": { - const seconds = typeof value === "string" - ? Math.floor(Date.parse(/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`) / 1000) - : toNumber(value, type); + const seconds = + typeof value === "string" + ? Math.floor( + Date.parse(/(?:Z|[+-]\d\d:\d\d)$/.test(value) ? value : `${value}Z`) / 1000, + ) + : toNumber(value, type); writer.writeUint32(assertInteger(seconds, 0, 0xffffffff, type)); return; } @@ -679,7 +869,8 @@ export class AbiSerializer { } if (rawType.endsWith("?")) { const present = reader.readByte(); - if (present !== 0 && present !== 1) throw new TypeError(`Invalid optional marker: ${present}`); + if (present !== 0 && present !== 1) + throw new TypeError(`Invalid optional marker: ${present}`); return present ? this.#decodeType(reader, rawType.slice(0, -1)) : null; } if (rawType.endsWith("$")) { @@ -687,6 +878,9 @@ export class AbiSerializer { } const type = this.resolveType(rawType); + if (type !== rawType && /(?:\[\]|\?|\$)$/.test(type)) { + return this.#decodeType(reader, type); + } const struct = this.#structs.get(type); if (struct) { const out: Record = {}; @@ -787,12 +981,14 @@ export class AbiSerializer { case "public_key": case "publickey": { const keyType = reader.readByte(); - if (keyType !== 0 && keyType !== 1) throw new TypeError(`Unsupported public-key type: ${keyType}`); + if (keyType !== 0 && keyType !== 1) + throw new TypeError(`Unsupported public-key type: ${keyType}`); return PublicKey.fromBytes(keyType === 0 ? "K1" : "R1", reader.readBytes(33)).toString(); } case "signature": { const keyType = reader.readByte(); - if (keyType !== 0 && keyType !== 1) throw new TypeError(`Unsupported signature type: ${keyType}`); + if (keyType !== 0 && keyType !== 1) + throw new TypeError(`Unsupported signature type: ${keyType}`); return Signature.fromBytes(keyType === 0 ? "K1" : "R1", reader.readBytes(65)).toString(); } default: diff --git a/packages/abi/tsconfig.json b/packages/abi/tsconfig.json index 8d0d56a..b55de06 100644 --- a/packages/abi/tsconfig.json +++ b/packages/abi/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, "include": ["src/**/*.ts"], "references": [{ "path": "../crypto" }] } diff --git a/packages/account/README.md b/packages/account/README.md index 587a970..efcbca4 100644 --- a/packages/account/README.md +++ b/packages/account/README.md @@ -68,7 +68,7 @@ await account.updatePermission( ); ``` -The package also exposes `deletePermission()`, `linkPermission()`, `unlinkPermission()`, `createAccount()`, `registerProxy()`, `unregisterProducer()`, `claimRewards()`, `refund()`, `buyRamSelf()`, and `buyRamBytes()`. +The package also exposes `deletePermission()`, `linkPermission()`, `unlinkPermission()`, `createAccount()`, `registerProxy()`, `unregisterProxy()`, `unregisterProducer()`, `claimRewards()`, `refund()`, `buyRamSelf()`, and `buyRamBytes()`. ## Runtime diff --git a/packages/account/package.json b/packages/account/package.json index d791e00..30a76ee 100644 --- a/packages/account/package.json +++ b/packages/account/package.json @@ -5,14 +5,44 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, - "dependencies": { "@windstack/contract": "1.0.0", "@windstack/rpc": "1.0.0" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" + }, + "dependencies": { + "@windstack/contract": "1.0.0", + "@windstack/crypto": "1.0.0", + "@windstack/rpc": "1.0.0" + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/account" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/account" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/account#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index b781073..626da4d 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -6,6 +6,7 @@ */ import { nameToBigInt } from "@windstack/abi"; import { AbiCache, Contract, type ContractAction } from "@windstack/contract"; +import { PublicKey } from "@windstack/crypto"; import { RpcClient } from "@windstack/rpc"; export type AccountClientOptions = { @@ -34,6 +35,7 @@ function validateName(value: string, label: string): string { } function validateOptionalName(value: string, label: string): string { + if (typeof value !== "string") throw new TypeError(`${label} must be a string`); if (value) nameToBigInt(value); return value; } @@ -45,6 +47,62 @@ function validateLocation(value: number): number { return value; } +function validateAuthority(authority: Authority, label: string): Authority { + if (!authority || typeof authority !== "object") throw new TypeError(`${label} is required`); + if ( + !Number.isInteger(authority.threshold) || + authority.threshold < 1 || + authority.threshold > 0xffffffff + ) { + throw new RangeError(`${label} threshold must be an integer between 1 and 4294967295`); + } + if ( + !Array.isArray(authority.keys) || + !Array.isArray(authority.accounts) || + !Array.isArray(authority.waits) + ) { + throw new TypeError(`${label} keys, accounts, and waits must be arrays`); + } + + const keyIds = new Set(); + const keys = authority.keys.map((item) => { + const key = PublicKey.fromString(item.key).toString(); + if (keyIds.has(key)) throw new TypeError(`${label} contains duplicate public keys`); + keyIds.add(key); + return { key: item.key, weight: validateWeight(item.weight, `${label} key weight`) }; + }); + const accountIds = new Set(); + const accounts = authority.accounts.map((item) => { + const actor = validateName(item.permission.actor, `${label} account actor`); + const permission = validateName(item.permission.permission, `${label} account permission`); + const id = `${actor}@${permission}`; + if (accountIds.has(id)) throw new TypeError(`${label} contains duplicate permission levels`); + accountIds.add(id); + return { + permission: { actor, permission }, + weight: validateWeight(item.weight, `${label} account weight`), + }; + }); + const waits = authority.waits.map((item) => { + if (!Number.isInteger(item.wait_sec) || item.wait_sec < 0 || item.wait_sec > 0xffffffff) { + throw new RangeError(`${label} wait_sec must fit in uint32`); + } + return { wait_sec: item.wait_sec, weight: validateWeight(item.weight, `${label} wait weight`) }; + }); + const totalWeight = [...keys, ...accounts, ...waits].reduce((sum, item) => sum + item.weight, 0); + if (authority.threshold > totalWeight) { + throw new RangeError(`${label} threshold exceeds the total available weight`); + } + return { threshold: authority.threshold, keys, accounts, waits }; +} + +function validateWeight(value: number, label: string): number { + if (!Number.isInteger(value) || value < 1 || value > 0xffff) { + throw new RangeError(`${label} must be an integer between 1 and 65535`); + } + return value; +} + export class AccountClient { readonly name: string; readonly rpc: RpcClient; @@ -157,6 +215,16 @@ export class AccountClient { ); } + stake( + receiver: string, + stakeNetQuantity: string, + stakeCpuQuantity: string, + transfer = false, + signal?: AbortSignal, + ): Promise { + return this.delegate(receiver, stakeNetQuantity, stakeCpuQuantity, transfer, signal); + } + undelegate( receiver: string, unstakeNetQuantity: string, @@ -176,6 +244,15 @@ export class AccountClient { ); } + unstake( + receiver: string, + unstakeNetQuantity: string, + unstakeCpuQuantity: string, + signal?: AbortSignal, + ): Promise { + return this.undelegate(receiver, unstakeNetQuantity, unstakeCpuQuantity, signal); + } + buyRam(receiver: string, quantity: string, signal?: AbortSignal): Promise { return this.systemAction( "buyram", @@ -278,6 +355,13 @@ export class AccountClient { ); } + unregisterProxy( + options: SystemActionOptions = {}, + signal?: AbortSignal, + ): Promise { + return this.registerProxy(false, options, signal); + } + registerProducer( producerKey: string, url: string, @@ -286,6 +370,7 @@ export class AccountClient { signal?: AbortSignal, ): Promise { if (typeof url !== "string") throw new TypeError("Producer URL must be a string"); + PublicKey.fromString(producerKey); return this.systemAction( "regproducer", { @@ -311,10 +396,7 @@ export class AccountClient { ); } - claimRewards( - options: SystemActionOptions = {}, - signal?: AbortSignal, - ): Promise { + claimRewards(options: SystemActionOptions = {}, signal?: AbortSignal): Promise { return this.systemAction( "claimrewards", { owner: this.name }, @@ -335,8 +417,8 @@ export class AccountClient { { creator: this.name, name: validateName(accountName, "New account name"), - owner, - active, + owner: validateAuthority(owner, "Owner authority"), + active: validateAuthority(active, "Active authority"), }, options.permission ?? "active", signal, @@ -357,7 +439,7 @@ export class AccountClient { account: this.name, permission: validateName(permission, "Permission"), parent: validateOptionalName(parent, "Parent permission"), - auth: authority, + auth: validateAuthority(authority, "Permission authority"), authorized_by: authorizedBy ? validateName(authorizedBy, "Authorized-by permission") : undefined, diff --git a/packages/account/tsconfig.json b/packages/account/tsconfig.json index e45ac57..6c2d632 100644 --- a/packages/account/tsconfig.json +++ b/packages/account/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, "include": ["src/**/*.ts"], - "references": [{ "path": "../contract" }, { "path": "../rpc" }] + "references": [{ "path": "../contract" }, { "path": "../crypto" }, { "path": "../rpc" }] } diff --git a/packages/contract/README.md b/packages/contract/README.md index 816d723..85ae98b 100644 --- a/packages/contract/README.md +++ b/packages/contract/README.md @@ -36,7 +36,7 @@ const action = await token.action( const rows = await token.tableRows("accounts", "alice"); ``` -Concurrent ABI reads for the same `Contract` instance share one in-flight request. Cached ABIs can be refreshed explicitly or removed through `AbiCache` when an application knows a contract has changed. +Concurrent ABI reads that use the same `AbiCache` share one in-flight request, including reads from different `Contract` instances. Use `refreshAbi()` for an explicit refresh and `deleteAbi()` or `AbiCache.clear()` when an application knows a contract has changed. ## Runtime diff --git a/packages/contract/package.json b/packages/contract/package.json index 7acc8b1..ad27270 100644 --- a/packages/contract/package.json +++ b/packages/contract/package.json @@ -5,14 +5,43 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, - "dependencies": { "@windstack/abi": "1.0.0", "@windstack/rpc": "1.0.0" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" + }, + "dependencies": { + "@windstack/abi": "1.0.0", + "@windstack/rpc": "1.0.0" + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/contract" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/contract" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/contract#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index be0026d..52789e9 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -39,19 +39,35 @@ function normalizeTableScope(value: TableScope): string { if (typeof value !== "string" || !value.trim()) { throw new TypeError("Table scope must be a non-empty string or non-negative integer"); } - return value; + const normalized = value.trim(); + if (/^\d+$/.test(normalized)) { + const integer = BigInt(normalized); + if (integer > 0xffffffffffffffffn) { + throw new RangeError("Table scope integer must fit in uint64"); + } + return integer.toString(); + } + if (/^[A-Z]{1,7}$/.test(normalized)) return normalized; + try { + nameToBigInt(normalized); + } catch { + throw new TypeError("Table scope must be name-like, symbol-like, or a uint64 value"); + } + return normalized; } export class AbiCache { readonly #entries = new Map(); + readonly #pending = new Map>(); constructor(readonly ttlMs = 5 * 60_000) { - if (!Number.isFinite(ttlMs) || ttlMs < 0) throw new RangeError("ABI cache TTL must be non-negative"); + if (!Number.isFinite(ttlMs) || ttlMs < 0) + throw new RangeError("ABI cache TTL must be non-negative"); } get(account: string): Abi | undefined { const entry = this.#entries.get(account); - if (!entry || entry.expiresAt < Date.now()) { + if (!entry || entry.expiresAt <= Date.now()) { this.#entries.delete(account); return undefined; } @@ -69,6 +85,23 @@ export class AbiCache { clear(): void { this.#entries.clear(); } + + async getOrLoad(account: string, loader: () => Promise): Promise { + const cached = this.get(account); + if (cached) return cached; + const pending = this.#pending.get(account); + if (pending) return pending; + const request = loader() + .then((abi) => { + this.set(account, abi); + return abi; + }) + .finally(() => { + this.#pending.delete(account); + }); + this.#pending.set(account, request); + return request; + } } function normalizeAuthorization(input: AuthorizationInput[]): PermissionLevel[] { @@ -93,7 +126,6 @@ export class Contract { readonly account: string; readonly rpc: RpcClient; readonly abiCache: AbiCache; - #pendingAbi: Promise | null = null; constructor(account: string, rpc: RpcClient, abiCache = new AbiCache()) { this.account = validateName(account, "Contract account"); @@ -105,7 +137,6 @@ export class Contract { if (!force) { const cached = this.abiCache.get(this.account); if (cached) return cached; - if (this.#pendingAbi) return this.#pendingAbi; } const load = async (): Promise => { @@ -115,15 +146,23 @@ export class Contract { } const abi = result.abi as Abi; new AbiSerializer(abi); - this.abiCache.set(this.account, abi); return abi; }; - if (force) return load(); - this.#pendingAbi = load().finally(() => { - this.#pendingAbi = null; - }); - return this.#pendingAbi; + if (force) { + const abi = await load(); + this.abiCache.set(this.account, abi); + return abi; + } + return this.abiCache.getOrLoad(this.account, load); + } + + refreshAbi(signal?: AbortSignal): Promise { + return this.getAbi(true, signal); + } + + deleteAbi(): void { + this.abiCache.delete(this.account); } async action( diff --git a/packages/contract/tsconfig.json b/packages/contract/tsconfig.json index c4b03dc..947c284 100644 --- a/packages/contract/tsconfig.json +++ b/packages/contract/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, "include": ["src/**/*.ts"], "references": [{ "path": "../abi" }, { "path": "../rpc" }] } diff --git a/packages/crypto/README.md b/packages/crypto/README.md index 5bb9794..40dd4aa 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -33,7 +33,9 @@ K1 signatures are emitted in the canonical compact form required by Vexanium-com The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Browser and React Native environments must provide secure platform randomness for key generation. Existing keys can be imported with `PrivateKey.fromString()` or `PrivateKey.fromBytes()`. -Private keys should be held by an appropriate secure storage or signing boundary. Application logs should never contain private-key material. +## Security + +Private keys should be held by an appropriate secure storage or signing boundary. Digest signing accepts exactly 32 bytes, and application logs should never contain private-key material. ## License diff --git a/packages/crypto/package.json b/packages/crypto/package.json index eb65411..950cecb 100644 --- a/packages/crypto/package.json +++ b/packages/crypto/package.json @@ -1,18 +1,47 @@ { "name": "@windstack/crypto", "version": "1.0.0", - "description": "Modern Antelope K1/R1 keys and signatures for WindStack, powered by Noble cryptography.", + "description": "Portable Antelope K1/R1 key and signature operations powered by Noble cryptography.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, - "dependencies": { "@noble/curves": "^2.4.0", "@noble/hashes": "^2.4.0" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" + }, + "dependencies": { + "@noble/curves": "^2.4.0", + "@noble/hashes": "^2.4.0" + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/crypto" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/crypto" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/crypto#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts index c163baf..20b12e7 100644 --- a/packages/crypto/src/index.ts +++ b/packages/crypto/src/index.ts @@ -18,6 +18,11 @@ function curveFor(type: KeyType) { return type === "K1" ? secp256k1 : p256; } +function assertKeyType(type: KeyType): void { + if (type !== "K1" && type !== "R1") + throw new TypeError(`Unsupported Antelope key type: ${String(type)}`); +} + function assertBytes(value: Uint8Array, length: number, label: string): void { if (!(value instanceof Uint8Array) || value.length !== length) { throw new TypeError(`${label} must be ${length} bytes`); @@ -141,6 +146,7 @@ export class PublicKey { readonly #data: Uint8Array; private constructor(type: KeyType, data: Uint8Array) { + assertKeyType(type); assertBytes(data, 33, "Public key"); if (!curveFor(type).utils.isValidPublicKey(data)) { throw new TypeError(`Invalid ${type} public key`); @@ -198,10 +204,18 @@ export class Signature { readonly #data: Uint8Array; private constructor(type: KeyType, data: Uint8Array) { + assertKeyType(type); assertBytes(data, 65, "Signature"); if (data[0]! < 31 || data[0]! > 34) { throw new TypeError("Invalid Antelope recovery header"); } + try { + const parsed = curveFor(type).Signature.fromBytes(data.slice(1), "compact"); + if (parsed.hasHighS()) throw new TypeError("Antelope signatures must use low-S form"); + } catch (error) { + if (error instanceof TypeError && error.message.includes("low-S")) throw error; + throw new TypeError(`Invalid ${type} compact signature`); + } this.type = type; this.#data = data.slice(); } @@ -259,6 +273,7 @@ export class PrivateKey { readonly #data: Uint8Array; private constructor(type: KeyType, data: Uint8Array) { + assertKeyType(type); assertBytes(data, 32, "Private key"); if (!curveFor(type).utils.isValidSecretKey(data)) { throw new TypeError(`Invalid ${type} private key`); diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json index ac7d40a..b273cb4 100644 --- a/packages/crypto/tsconfig.json +++ b/packages/crypto/tsconfig.json @@ -1,5 +1,9 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, "include": ["src/**/*.ts"] } diff --git a/packages/rpc/README.md b/packages/rpc/README.md index fc3a44a..e8d531c 100644 --- a/packages/rpc/README.md +++ b/packages/rpc/README.md @@ -2,7 +2,7 @@ ## Overview -`@windstack/rpc` provides a typed client for Antelope chain RPC endpoints. It supports multiple endpoints, automatic failover for retriable read failures, request timeouts, `AbortSignal` cancellation, injected `fetch`, structured RPC errors, table queries, account queries, ABI queries, currency queries, required-key resolution, and transaction broadcast. +`@windstack/rpc` provides a typed client for Antelope chain RPC endpoints. It supports multiple endpoints, automatic failover for retriable read failures, request timeouts, `AbortSignal` cancellation, injected `fetch`, structured RPC errors, block queries, table queries, account queries, ABI queries, currency queries, required-key resolution, and transaction submission. Broadcast requests are not automatically retried because a transaction may already have reached the chain even when the original network response is lost. @@ -36,6 +36,10 @@ const rows = await rpc.getTableRows({ HTTP request errors are exposed through `RpcError`. Timeouts use `RpcTimeoutError`. Invalid chain requests are returned immediately instead of being retried across every configured endpoint. +## Security + +Read requests retry only failures that may be transient. `push_transaction` is never retried automatically, because losing the response does not prove that the node rejected the transaction. Callers should reconcile an uncertain result before choosing to submit again. + ## Runtime The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses the standard `fetch`, `Response`, `AbortController`, and `AbortSignal` interfaces. A compatible `fetch` implementation can be supplied through the constructor when the runtime does not provide one globally. diff --git a/packages/rpc/package.json b/packages/rpc/package.json index 952f16b..4c6cbce 100644 --- a/packages/rpc/package.json +++ b/packages/rpc/package.json @@ -5,13 +5,39 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/rpc" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/rpc" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/rpc#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts index 28bb9b7..25deff7 100644 --- a/packages/rpc/src/index.ts +++ b/packages/rpc/src/index.ts @@ -6,7 +6,7 @@ */ export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise; export type RpcClientOptions = { - endpoints: string | string[]; + endpoints: string | readonly string[]; fetch?: FetchLike; timeoutMs?: number; retries?: number; @@ -65,6 +65,16 @@ export type TableByScopeRow = { payer: string; count: number; }; +export type PackedTransaction = { + signatures: string[]; + compression?: number; + packed_context_free_data?: string; + packed_trx: string; +}; +export type SendTransaction2Request = PackedTransaction & { + return_failure_trace?: boolean; + retry_trx?: boolean; +}; export class RpcError extends Error { readonly status: number; @@ -92,6 +102,13 @@ export class RpcTimeoutError extends Error { } } +export class RpcResponseError extends RpcError { + constructor(endpoint: string, status: number, payload: string) { + super(`RPC response from ${endpoint} is not valid JSON`, status, endpoint, payload); + this.name = "RpcResponseError"; + } +} + function rpcMessage(payload: unknown, fallback: string): string { if (!payload || typeof payload !== "object") return fallback; const record = payload as Record; @@ -101,7 +118,10 @@ function rpcMessage(payload: unknown, fallback: string): string { if (typeof error.what === "string" && error.what) return error.what; if (Array.isArray(error.details)) { const detail = error.details.find( - (item) => item && typeof item === "object" && typeof (item as Record).message === "string", + (item) => + item && + typeof item === "object" && + typeof (item as Record).message === "string", ) as Record | undefined; if (detail?.message) return String(detail.message); } @@ -111,8 +131,11 @@ function rpcMessage(payload: unknown, fallback: string): string { function isRetriable(error: unknown): boolean { if (error instanceof RpcTimeoutError) return true; + if (error instanceof RpcResponseError) return true; if (error instanceof RpcError) { - return error.status === 408 || error.status === 425 || error.status === 429 || error.status >= 500; + return ( + error.status === 408 || error.status === 425 || error.status === 429 || error.status >= 500 + ); } return error instanceof TypeError || (error instanceof Error && error.name === "AbortError"); } @@ -125,9 +148,13 @@ export class RpcClient { #cursor = 0; constructor(options: RpcClientOptions) { - const endpoints = (Array.isArray(options.endpoints) ? options.endpoints : [options.endpoints]) - .map((endpoint) => endpoint.trim().replace(/\/+$/, "")) - .filter(Boolean); + const endpoints = [ + ...new Set( + (typeof options.endpoints === "string" ? [options.endpoints] : options.endpoints) + .map((endpoint) => endpoint.trim().replace(/\/+$/, "")) + .filter(Boolean), + ), + ]; if (!endpoints.length || endpoints.some((endpoint) => !/^https?:\/\//.test(endpoint))) { throw new TypeError("At least one http(s) RPC endpoint is required"); } @@ -137,10 +164,12 @@ export class RpcClient { } const timeoutMs = options.timeoutMs ?? 10_000; const retries = options.retries ?? Math.max(0, endpoints.length - 1); - if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new RangeError("timeoutMs must be greater than zero"); - if (!Number.isInteger(retries) || retries < 0) throw new RangeError("retries must be a non-negative integer"); + if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) + throw new RangeError("timeoutMs must be greater than zero"); + if (!Number.isInteger(retries) || retries < 0) + throw new RangeError("retries must be a non-negative integer"); - this.endpoints = Object.freeze([...new Set(endpoints)]); + this.endpoints = Object.freeze(endpoints); this.#fetch = fetchImplementation.bind(globalThis); this.#timeoutMs = timeoutMs; this.#retries = retries; @@ -158,6 +187,9 @@ export class RpcClient { } const retries = options.retries ?? this.#retries; + if (!Number.isInteger(retries) || retries < 0) { + throw new RangeError("RPC request retries must be a non-negative integer"); + } const attempts = retries + 1; let lastError: unknown; @@ -185,6 +217,7 @@ export class RpcClient { try { payload = text ? JSON.parse(text) : null; } catch { + if (response.ok) throw new RpcResponseError(endpoint, response.status, text); payload = text; } if (!response.ok) { @@ -220,11 +253,21 @@ export class RpcClient { return this.request("/v1/chain/get_block", { block_num_or_id: blockNumOrId }, signal); } + getBlockInfo(blockNum: number, signal?: AbortSignal): Promise { + if (!Number.isInteger(blockNum) || blockNum < 0 || blockNum > 0xffffffff) { + throw new RangeError("Block number must be a uint32 integer"); + } + return this.request("/v1/chain/get_block_info", { block_num: blockNum }, signal); + } + getAccount>(accountName: string, signal?: AbortSignal): Promise { return this.request("/v1/chain/get_account", { account_name: accountName }, signal); } - getAbi(accountName: string, signal?: AbortSignal): Promise<{ account_name: string; abi: unknown }> { + getAbi( + accountName: string, + signal?: AbortSignal, + ): Promise<{ account_name: string; abi: unknown }> { return this.request("/v1/chain/get_abi", { account_name: accountName }, signal); } @@ -284,12 +327,7 @@ export class RpcClient { } pushTransaction>( - transaction: { - signatures: string[]; - compression?: number; - packed_context_free_data?: string; - packed_trx: string; - }, + transaction: PackedTransaction, signal?: AbortSignal, ): Promise { return this.request( @@ -299,4 +337,28 @@ export class RpcClient { { retries: 0 }, ); } + + sendTransaction>( + transaction: PackedTransaction, + signal?: AbortSignal, + ): Promise { + return this.request( + "/v1/chain/send_transaction", + { compression: 0, packed_context_free_data: "", ...transaction }, + signal, + { retries: 0 }, + ); + } + + sendTransaction2>( + transaction: SendTransaction2Request, + signal?: AbortSignal, + ): Promise { + return this.request( + "/v1/chain/send_transaction2", + { compression: 0, packed_context_free_data: "", ...transaction }, + signal, + { retries: 0 }, + ); + } } diff --git a/packages/rpc/tsconfig.json b/packages/rpc/tsconfig.json index ac7d40a..b273cb4 100644 --- a/packages/rpc/tsconfig.json +++ b/packages/rpc/tsconfig.json @@ -1,5 +1,9 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, "include": ["src/**/*.ts"] } diff --git a/scripts/fetch-vexanium-abi.mjs b/scripts/fetch-vexanium-abi.mjs new file mode 100644 index 0000000..0cbeaf1 --- /dev/null +++ b/scripts/fetch-vexanium-abi.mjs @@ -0,0 +1,35 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const fixtureDirectory = path.join(root, "test", "fixtures", "vexanium"); +const endpoint = "https://api.windcrypto.com"; + +await mkdir(fixtureDirectory, { recursive: true }); +for (const account of ["vex.token", "vexcore"]) { + const response = await fetch(`${endpoint}/v1/chain/get_abi`, { + method: "POST", + headers: { "content-type": "application/json", accept: "application/json" }, + body: JSON.stringify({ account_name: account }), + signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) throw new Error(`Unable to fetch ${account} ABI: HTTP ${response.status}`); + const payload = await response.json(); + if (!payload?.abi || typeof payload.abi !== "object") { + throw new TypeError(`Vexanium RPC returned no ABI for ${account}`); + } + await writeFile( + path.join(fixtureDirectory, `${account}.abi.json`), + `${JSON.stringify(payload.abi, null, 2)}\n`, + "utf8", + ); +} + +console.log("Updated Vexanium production ABI fixtures"); diff --git a/scripts/test-abi.mjs b/scripts/test-abi.mjs new file mode 100644 index 0000000..51106f3 --- /dev/null +++ b/scripts/test-abi.mjs @@ -0,0 +1,179 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { + AbiSerializer, + bigIntToName, + formatAsset, + nameToBigInt, + parseAsset, +} from "../packages/abi/dist/index.js"; +import { PrivateKey, sha256Digest } from "../packages/crypto/dist/index.js"; + +const abi = { + version: "eosio::abi/1.2", + types: [ + { new_type_name: "account_name", type: "name" }, + { new_type_name: "account_list", type: "account_name[]" }, + ], + structs: [ + { name: "base", base: "", fields: [{ name: "id", type: "uint64" }] }, + { + name: "child", + base: "base", + fields: [ + { name: "accounts", type: "account_list" }, + { name: "memo", type: "string?" }, + ], + }, + { + name: "extension", + base: "", + fields: [ + { name: "value", type: "string" }, + { name: "extra", type: "uint32$" }, + ], + }, + ], + variants: [{ name: "value_variant", types: ["string", "uint64"] }], + actions: [{ name: "save", type: "child" }], + tables: [{ name: "records", index_type: "i64", type: "child" }], + action_results: [{ name: "save", result_type: "value_variant" }], +}; +const serializer = new AbiSerializer(abi); + +const child = { id: "18446744073709551615", accounts: ["alice", "vex.token"], memo: null }; +assert.deepEqual(serializer.decode("child", serializer.encode("child", child)), { + id: 18446744073709551615n, + accounts: ["alice", "vex.token"], + memo: null, +}); +assert.deepEqual(serializer.decode("account_list", serializer.encode("account_list", ["alice"])), [ + "alice", +]); +assert.deepEqual(serializer.decode("extension", serializer.encode("extension", { value: "VEX" })), { + value: "VEX", + extra: undefined, +}); +assert.deepEqual( + serializer.decode("extension", serializer.encode("extension", { value: "VEX", extra: 7 })), + { value: "VEX", extra: 7 }, +); +assert.deepEqual( + serializer.decode( + "value_variant", + serializer.encode("value_variant", { type: "uint64", value: "9" }), + ), + { type: "uint64", value: 9n }, +); +assert.equal(serializer.getActionType("save"), "child"); +assert.equal(serializer.getTableType("records"), "child"); + +const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); +const privateKey = PrivateKey.fromBytes("K1", scalarOne); +const publicKey = privateKey.toPublicKey().toString(); +const signature = privateKey.signDigest(sha256Digest(new Uint8Array(0))).toString(); +const cases = [ + ["bool", true, true], + ["uint8", 255, 255], + ["int8", -128, -128], + ["uint16", 65535, 65535], + ["int16", -32768, -32768], + ["uint32", 4294967295, 4294967295], + ["int32", -2147483648, -2147483648], + ["uint64", "18446744073709551615", 18446744073709551615n], + ["int64", "-9223372036854775808", -9223372036854775808n], + ["uint128", "340282366920938463463374607431768211455", 340282366920938463463374607431768211455n], + ["int128", "-170141183460469231731687303715884105728", -170141183460469231731687303715884105728n], + ["varuint32", 4294967295, 4294967295], + ["varint32", -2147483648, -2147483648], + ["float32", 1.5, 1.5], + ["float64", Math.PI, Math.PI], + ["float128", "ab".repeat(16), "ab".repeat(16)], + ["string", "Vexanium", "Vexanium"], + ["bytes", "00ff", "00ff"], + ["checksum160", "11".repeat(20), "11".repeat(20)], + ["checksum256", "22".repeat(32), "22".repeat(32)], + ["checksum512", "33".repeat(64), "33".repeat(64)], + ["asset", "1.0000 VEX", "1.0000 VEX"], + ["symbol", "4,VEX", "4,VEX"], + ["symbol_code", "VEX", "VEX"], + [ + "extended_asset", + { quantity: "1.0000 VEX", contract: "vex.token" }, + { quantity: "1.0000 VEX", contract: "vex.token" }, + ], + ["time_point", "2026-09-07T00:00:00.123456Z", "2026-09-07T00:00:00.123456Z"], + ["time_point_sec", "2026-09-07T00:00:00Z", "2026-09-07T00:00:00.000Z"], + ["block_timestamp_type", "2026-09-07T00:00:00.500Z", "2026-09-07T00:00:00.500Z"], + ["public_key", publicKey, publicKey], + ["signature", signature, signature], +]; +for (const [type, input, expected] of cases) { + assert.deepEqual(serializer.decode(type, serializer.encode(type, input)), expected, type); +} + +assert.equal(bigIntToName(nameToBigInt("vex.token")), "vex.token"); +assert.deepEqual(parseAsset("1.0000 VEX"), { + amount: 10000n, + precision: 4, + symbol: "VEX", + value: "1.0000 VEX", +}); +assert.equal(formatAsset(-10000n, 4, "VEX"), "-1.0000 VEX"); +assert.equal(bigIntToName(nameToBigInt("abcdefghij123")), "abcdefghij123"); +assert.throws(() => nameToBigInt("aaaaaaaaaaaak"), /13th Antelope name character/); +assert.throws( + () => serializer.encode("uint64", Number.MAX_SAFE_INTEGER + 1), + /bigint or a decimal string/, +); +assert.throws(() => serializer.encode("uint8", 256), /uint8/); +assert.throws(() => parseAsset("01.0000 VEX"), /Invalid asset/); +assert.throws(() => serializer.decode("bool", Uint8Array.of(2)), /Invalid bool/); +assert.throws(() => serializer.decode("uint64", new Uint8Array(7)), /Unexpected end/); +assert.throws(() => serializer.decode("uint8", Uint8Array.of(1, 2)), /Unused ABI bytes/); +assert.throws( + () => serializer.decode("string", Uint8Array.of(1, 0xff)), + /encoded data was not valid/, +); +assert.throws( + () => + new AbiSerializer({ + version: "eosio::abi/1.2", + structs: [{ name: "bad", fields: [{ name: "x", type: "mystery" }] }], + }), + /Unsupported ABI type mystery/, +); +assert.throws( + () => + new AbiSerializer({ + version: "eosio::abi/1.2", + types: [ + { new_type_name: "a", type: "b" }, + { new_type_name: "b", type: "a" }, + ], + }), + /Cyclic ABI alias/, +); +assert.throws( + () => + new AbiSerializer({ + version: "eosio::abi/1.2", + structs: [ + { + name: "bad", + fields: [ + { name: "tail", type: "uint8$" }, + { name: "later", type: "uint8" }, + ], + }, + ], + }), + /Binary-extension fields must be last/, +); + +console.log("ABI codec tests passed"); diff --git a/scripts/test-contract-account.mjs b/scripts/test-contract-account.mjs new file mode 100644 index 0000000..2ab5cb3 --- /dev/null +++ b/scripts/test-contract-account.mjs @@ -0,0 +1,144 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { AbiSerializer, hexToBytes } from "../packages/abi/dist/index.js"; +import { AccountClient } from "../packages/account/dist/index.js"; +import { AbiCache, Contract, ContractKit } from "../packages/contract/dist/index.js"; +import { PrivateKey } from "../packages/crypto/dist/index.js"; +import { RpcClient } from "../packages/rpc/dist/index.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const tokenAbi = JSON.parse( + await readFile(path.join(root, "test/fixtures/vexanium/vex.token.abi.json"), "utf8"), +); +const systemAbi = JSON.parse( + await readFile(path.join(root, "test/fixtures/vexanium/vexcore.abi.json"), "utf8"), +); +let abiRequests = 0; +const tableRequests = []; +const rpc = new RpcClient({ + endpoints: "https://unit.test", + fetch: async (input, init) => { + const url = String(input); + const body = JSON.parse(String(init?.body ?? "{}")); + if (url.endsWith("/get_abi")) { + abiRequests += 1; + await Promise.resolve(); + return Response.json({ + account_name: body.account_name, + abi: body.account_name === "vex.token" ? tokenAbi : systemAbi, + }); + } + if (url.endsWith("/get_table_rows")) { + tableRequests.push(body); + return Response.json({ rows: [], more: false }); + } + if (url.endsWith("/get_currency_balance")) return Response.json(["1.0000 VEX"]); + return Response.json({ message: "not found" }, { status: 404 }); + }, +}); + +const cache = new AbiCache(60_000); +const first = new Contract("vex.token", rpc, cache); +const second = new Contract("vex.token", rpc, cache); +await Promise.all([first.getAbi(), second.getAbi()]); +assert.equal(abiRequests, 1, "ABI requests must be shared across contract instances"); +await first.refreshAbi(); +assert.equal(abiRequests, 2); +first.deleteAbi(); +await second.getAbi(); +assert.equal(abiRequests, 3); + +const transfer = await first.action( + "transfer", + { from: "alice", to: "bob", quantity: "1.0000 VEX", memo: "WindStack" }, + ["alice@active"], +); +assert.deepEqual(transfer.authorization, [{ actor: "alice", permission: "active" }]); +assert.deepEqual(new AbiSerializer(tokenAbi).decodeAction("transfer", hexToBytes(transfer.data)), { + from: "alice", + to: "bob", + quantity: "1.0000 VEX", + memo: "WindStack", +}); +await first.tableRows("accounts", "alice", { limit: 10 }); +await first.tableRows("stat", "VEX", { lower_bound: "VEX" }); +await first.tableRows("accounts", 42n); +assert.deepEqual( + tableRequests.map((item) => item.scope), + ["alice", "VEX", "42"], +); +assert.equal(new ContractKit(rpc, { abiCache: cache }).contract("vex.token").abiCache, cache); + +const account = new AccountClient("alice", rpc, cache, { + tokenContract: "vex.token", + systemContract: "vexcore", +}); +assert.deepEqual(await account.balance(), ["1.0000 VEX"]); +const publicKey = PrivateKey.fromBytes( + "K1", + Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)), +) + .toPublicKey() + .toString(); +const authority = { + threshold: 1, + keys: [{ key: publicKey, weight: 1 }], + accounts: [], + waits: [], +}; +const actions = await Promise.all([ + account.transfer("bob", "1.0000 VEX"), + account.delegate("bob", "1.0000 VEX", "2.0000 VEX"), + account.stake("bob", "1.0000 VEX", "2.0000 VEX"), + account.undelegate("bob", "1.0000 VEX", "2.0000 VEX"), + account.unstake("bob", "1.0000 VEX", "2.0000 VEX"), + account.buyRam("bob", "1.0000 VEX"), + account.buyRamSelf("1.0000 VEX"), + account.buyRamBytes("bob", 4096), + account.sellRam("9223372036854775807"), + account.refund(), + account.voteProducers(["alice", "bob"]), + account.voteProxy("bob"), + account.clearVote(), + account.registerProxy(), + account.unregisterProxy(), + account.registerProducer(publicKey, "https://producer.example", 65535), + account.unregisterProducer(), + account.claimRewards(), + account.createAccount("bob", authority, authority), + account.updatePermission("active", "owner", authority, "owner"), + account.deletePermission("custom", "owner"), + account.linkPermission("vex.token", "transfer", "active", "owner"), + account.unlinkPermission("vex.token", "transfer", "owner"), +]); +const systemSerializer = new AbiSerializer(systemAbi); +for (const action of actions) { + const actionAbi = action.account === "vex.token" ? new AbiSerializer(tokenAbi) : systemSerializer; + actionAbi.decodeAction(action.name, hexToBytes(action.data)); +} +assert.equal(actions[1].name, "delegatebw"); +assert.equal(actions[8].name, "sellram"); +assert.equal(actions[14].name, "regproxy"); + +assert.throws(() => account.voteProducers(["alice", "alice"]), /duplicate/); +assert.throws( + () => account.voteProducers(Array.from({ length: 31 }, (_, index) => `a${index}`)), + /between 1 and 30/, +); +assert.throws(() => account.buyRamBytes("bob", 0), /RAM bytes/); +assert.throws(() => account.sellRam("9223372036854775808"), /signed 64-bit/); +assert.throws(() => account.registerProducer("invalid", "", 0), /public-key format/); +assert.throws( + () => account.createAccount("bob", { ...authority, threshold: 2 }, authority), + /threshold exceeds/, +); + +console.log("Contract and account compatibility tests passed"); diff --git a/scripts/test-crypto.mjs b/scripts/test-crypto.mjs new file mode 100644 index 0000000..f481528 --- /dev/null +++ b/scripts/test-crypto.mjs @@ -0,0 +1,78 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { + PrivateKey, + PublicKey, + Signature, + hexToBytes, + sha256Digest, +} from "../packages/crypto/dist/index.js"; + +const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); +const digest = sha256Digest(new TextEncoder().encode("WindStack deterministic signature vector")); +const otherDigest = sha256Digest(new TextEncoder().encode("different digest")); + +for (const type of ["K1", "R1"]) { + const privateKey = PrivateKey.fromBytes(type, scalarOne); + const publicKey = privateKey.toPublicKey(); + const first = privateKey.signDigest(digest); + const second = privateKey.signDigest(digest); + assert.equal(first.toString(), second.toString(), `${type} signing must be deterministic`); + assert.equal(first.verifyDigest(digest, publicKey), true); + assert.equal(first.verifyDigest(otherDigest, publicKey), false); + assert.equal(first.recoverDigest(digest).toString(), publicKey.toString()); + assert.equal(first.isCanonical(), true); + assert.equal(PrivateKey.fromString(privateKey.toString()).toString(), privateKey.toString()); + assert.equal(PublicKey.fromString(publicKey.toString()).toString(), publicKey.toString()); + assert.equal(Signature.fromString(first.toString()).toString(), first.toString()); +} + +const k1 = PrivateKey.fromBytes("K1", scalarOne); +const r1 = PrivateKey.fromBytes("R1", scalarOne); +assert.equal(k1.signDigest(digest).verifyDigest(digest, r1.toPublicKey()), false); +assert.equal(PrivateKey.fromString(k1.toWif()).toString(), k1.toString()); +assert.equal( + PublicKey.fromString(k1.toPublicKey().toLegacyString()).toString(), + k1.toPublicKey().toString(), +); +assert.equal(PrivateKey.generate("K1").toBytes().length, 32); +assert.equal(PrivateKey.generate("R1").toBytes().length, 32); + +function corrupt(value) { + const last = value.at(-1); + return `${value.slice(0, -1)}${last === "1" ? "2" : "1"}`; +} + +assert.throws(() => PrivateKey.fromBytes("K1", new Uint8Array(32)), /Invalid K1 private key/); +assert.throws( + () => PrivateKey.fromBytes("K1", new Uint8Array(32).fill(0xff)), + /Invalid K1 private key/, +); +assert.throws(() => PrivateKey.fromBytes("invalid", scalarOne), /Unsupported Antelope key type/); +assert.throws(() => PublicKey.fromBytes("K1", new Uint8Array(33)), /Invalid K1 public key/); +assert.throws( + () => PublicKey.fromBytes("K1", Uint8Array.of(4, ...new Uint8Array(32))), + /Invalid K1 public key/, +); +assert.throws( + () => Signature.fromBytes("K1", Uint8Array.of(30, ...new Uint8Array(64))), + /recovery header/, +); +assert.throws( + () => Signature.fromBytes("K1", Uint8Array.of(31, ...new Uint8Array(64))), + /compact signature/, +); +assert.throws(() => PrivateKey.fromString(corrupt(k1.toString())), /checksum/); +assert.throws(() => PrivateKey.fromString(corrupt(k1.toWif())), /checksum|Unsupported/); +assert.throws(() => PublicKey.fromString(corrupt(k1.toPublicKey().toString())), /checksum/); +assert.throws(() => Signature.fromString(corrupt(k1.signDigest(digest).toString())), /checksum/); +assert.throws(() => k1.signDigest(new Uint8Array(31)), /Digest must be 32 bytes/); +assert.throws(() => k1.signDigest("not bytes"), /Digest must be 32 bytes/); +assert.throws(() => hexToBytes("abc"), /Invalid hexadecimal/); + +console.log("Crypto security tests passed"); diff --git a/scripts/test-rpc.mjs b/scripts/test-rpc.mjs new file mode 100644 index 0000000..f8e76c8 --- /dev/null +++ b/scripts/test-rpc.mjs @@ -0,0 +1,131 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { + RpcClient, + RpcError, + RpcResponseError, + RpcTimeoutError, +} from "../packages/rpc/dist/index.js"; + +const requests = []; +const rpc = new RpcClient({ + endpoints: ["https://one.test/", "https://one.test", "https://two.test"], + fetch: async (input, init) => { + requests.push({ url: String(input), body: JSON.parse(String(init?.body)) }); + return Response.json({ chain_id: "00".repeat(32), head_block_num: 1 }); + }, +}); +assert.deepEqual(rpc.endpoints, ["https://one.test", "https://two.test"]); +await rpc.getInfo(); +assert.equal(requests[0].url, "https://one.test/v1/chain/get_info"); + +let attempts = 0; +const failover = new RpcClient({ + endpoints: ["https://bad.test", "https://good.test"], + fetch: async (input) => { + attempts += 1; + if (String(input).startsWith("https://bad.test")) { + return Response.json({ message: "temporary failure" }, { status: 503 }); + } + return Response.json({ chain_id: "00".repeat(32), head_block_num: 1 }); + }, +}); +assert.equal((await failover.getInfo()).head_block_num, 1); +assert.equal(attempts, 2); + +attempts = 0; +const invalidRequest = new RpcClient({ + endpoints: ["https://a.test", "https://b.test"], + fetch: async () => { + attempts += 1; + return new Response("invalid request", { status: 400 }); + }, +}); +await assert.rejects( + () => invalidRequest.getInfo(), + (error) => error instanceof RpcError && error.status === 400, +); +assert.equal(attempts, 1); + +attempts = 0; +const invalidJson = new RpcClient({ + endpoints: "https://json.test", + retries: 1, + fetch: async () => { + attempts += 1; + return attempts === 1 + ? new Response("not-json", { status: 200 }) + : Response.json({ chain_id: "00".repeat(32), head_block_num: 1 }); + }, +}); +assert.equal((await invalidJson.getInfo()).head_block_num, 1); +assert.equal(attempts, 2); + +const invalidJsonNoRetry = new RpcClient({ + endpoints: "https://json.test", + retries: 0, + fetch: async () => new Response("not-json", { status: 200 }), +}); +await assert.rejects(() => invalidJsonNoRetry.getInfo(), RpcResponseError); + +const timeoutRpc = new RpcClient({ + endpoints: "https://timeout.test", + timeoutMs: 5, + retries: 0, + fetch: async (_input, init) => + new Promise((_resolve, reject) => { + init?.signal?.addEventListener( + "abort", + () => reject(new DOMException("Aborted", "AbortError")), + { + once: true, + }, + ); + }), +}); +await assert.rejects(() => timeoutRpc.getInfo(), RpcTimeoutError); + +const controller = new AbortController(); +const reason = new Error("caller cancelled"); +controller.abort(reason); +await assert.rejects( + () => rpc.getInfo(controller.signal), + (error) => error === reason, +); + +attempts = 0; +const pushRpc = new RpcClient({ + endpoints: ["https://a.test", "https://b.test"], + retries: 5, + fetch: async () => { + attempts += 1; + return Response.json({ message: "uncertain broadcast result" }, { status: 503 }); + }, +}); +await assert.rejects( + () => pushRpc.pushTransaction({ signatures: [], packed_trx: "00" }), + /uncertain broadcast result/, +); +assert.equal(attempts, 1, "push_transaction must never retry automatically"); +await assert.rejects( + () => pushRpc.sendTransaction({ signatures: [], packed_trx: "00" }), + /uncertain broadcast result/, +); +assert.equal(attempts, 2, "send_transaction must never retry automatically"); +await assert.rejects( + () => pushRpc.sendTransaction2({ signatures: [], packed_trx: "00" }), + /uncertain broadcast result/, +); +assert.equal(attempts, 3, "send_transaction2 must never retry automatically"); + +assert.throws( + () => new RpcClient({ endpoints: ["https://same.test", "https://same.test"], retries: -1 }), + /non-negative integer/, +); + +console.log("RPC resilience tests passed"); diff --git a/scripts/test-vexanium-abi.mjs b/scripts/test-vexanium-abi.mjs new file mode 100644 index 0000000..9ee5379 --- /dev/null +++ b/scripts/test-vexanium-abi.mjs @@ -0,0 +1,193 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { AbiSerializer } from "../packages/abi/dist/index.js"; +import { PrivateKey, sha256Digest } from "../packages/crypto/dist/index.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); +const privateKey = PrivateKey.fromBytes("K1", scalarOne); +const publicKey = privateKey.toPublicKey().toString(); +const signature = privateKey + .signDigest(sha256Digest(new TextEncoder().encode("WindStack ABI fixture"))) + .toString(); + +async function readAbi(account) { + return JSON.parse( + await readFile(path.join(root, "test", "fixtures", "vexanium", `${account}.abi.json`), "utf8"), + ); +} + +function createSampler(abi) { + const aliases = new Map((abi.types ?? []).map((item) => [item.new_type_name, item.type])); + const structs = new Map((abi.structs ?? []).map((item) => [item.name, item])); + const variants = new Map((abi.variants ?? []).map((item) => [item.name, item])); + + function resolve(type) { + const seen = new Set(); + let current = type; + while (aliases.has(current)) { + if (seen.has(current)) throw new TypeError(`Cyclic fixture alias: ${type}`); + seen.add(current); + current = aliases.get(current); + } + return current; + } + + function sample(rawType, stack = []) { + if (rawType.endsWith("[]")) return []; + if (rawType.endsWith("?")) return null; + if (rawType.endsWith("$")) return undefined; + const type = resolve(rawType); + if (type !== rawType) return sample(type, stack); + if (stack.includes(type)) { + throw new TypeError( + `Recursive fixture type cannot be sampled: ${[...stack, type].join(" -> ")}`, + ); + } + const struct = structs.get(type); + if (struct) { + const value = {}; + if (struct.base) Object.assign(value, sample(struct.base, [...stack, type])); + for (const field of struct.fields ?? []) + value[field.name] = sample(field.type, [...stack, type]); + return value; + } + const variant = variants.get(type); + if (variant) { + const selected = variant.types[0]; + return { type: selected, value: sample(selected, [...stack, type]) }; + } + switch (type) { + case "bool": + return false; + case "uint8": + case "int8": + case "uint16": + case "int16": + case "uint32": + case "int32": + case "varuint32": + case "varuint": + case "varint32": + case "varint": + case "float32": + case "float64": + return 0; + case "uint64": + case "int64": + case "uint128": + case "int128": + return "0"; + case "float128": + return "00".repeat(16); + case "name": + return "alice"; + case "string": + case "bytes": + return ""; + case "checksum160": + return "00".repeat(20); + case "checksum256": + return "00".repeat(32); + case "checksum512": + return "00".repeat(64); + case "asset": + return "1.0000 VEX"; + case "extended_asset": + return { quantity: "1.0000 VEX", contract: "vex.token" }; + case "symbol": + return "4,VEX"; + case "symbol_code": + return "VEX"; + case "time_point": + return "2026-09-07T00:00:00.123456Z"; + case "time_point_sec": + return "2026-09-07T00:00:00Z"; + case "block_timestamp_type": + return "2026-09-07T00:00:00.000Z"; + case "public_key": + case "publickey": + return publicKey; + case "signature": + return signature; + default: + throw new TypeError(`Unsupported primitive in Vexanium fixture: ${type}`); + } + } + return sample; +} + +function validateFixture(account, abi) { + assert.equal(abi.version, "eosio::abi/1.2"); + const serializer = new AbiSerializer(abi); + const sample = createSampler(abi); + + for (const alias of abi.types ?? []) { + assert.equal(typeof serializer.resolveType(alias.new_type_name), "string"); + } + for (const struct of abi.structs ?? []) { + const bytes = serializer.encode(struct.name, sample(struct.name)); + serializer.decode(struct.name, bytes); + } + for (const variant of abi.variants ?? []) { + const bytes = serializer.encode(variant.name, sample(variant.name)); + serializer.decode(variant.name, bytes); + } + for (const action of abi.actions ?? []) { + serializer.decodeAction(action.name, serializer.encodeAction(action.name, sample(action.type))); + } + for (const table of abi.tables ?? []) { + assert.equal(serializer.getTableType(table.name), table.type); + serializer.decode(table.type, serializer.encode(table.type, sample(table.type))); + } + for (const result of abi.action_results ?? []) { + serializer.decode( + result.result_type, + serializer.encode(result.result_type, sample(result.result_type)), + ); + } + + return { + account, + actions: abi.actions?.length ?? 0, + tables: abi.tables?.length ?? 0, + structs: abi.structs?.length ?? 0, + }; +} + +const token = await readAbi("vex.token"); +const system = await readAbi("vexcore"); +const tokenResult = validateFixture("vex.token", token); +const systemResult = validateFixture("vexcore", system); + +assert.equal(tokenResult.actions, 10); +assert.equal(tokenResult.tables, 3); +assert.equal(systemResult.actions, 84); +assert.equal(systemResult.tables, 34); +assert.deepEqual( + token.actions.map((item) => item.name), + [ + "addblacklist", + "close", + "create", + "issue", + "issuefixed", + "open", + "retire", + "rmblacklist", + "setmaxsupply", + "transfer", + ], +); + +console.log( + `Vexanium ABI fixtures passed: ${tokenResult.actions + systemResult.actions} actions, ${tokenResult.tables + systemResult.tables} tables, ${tokenResult.structs + systemResult.structs} structs`, +); diff --git a/test/fixtures/vexanium/vex.token.abi.json b/test/fixtures/vexanium/vex.token.abi.json new file mode 100644 index 0000000..a9625f2 --- /dev/null +++ b/test/fixtures/vexanium/vex.token.abi.json @@ -0,0 +1,276 @@ +{ + "version": "eosio::abi/1.2", + "types": [], + "structs": [ + { + "name": "account", + "base": "", + "fields": [ + { + "name": "balance", + "type": "asset" + } + ] + }, + { + "name": "account_blacklist", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + } + ] + }, + { + "name": "addblacklist", + "base": "", + "fields": [ + { + "name": "accounts", + "type": "name[]" + } + ] + }, + { + "name": "close", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "symbol", + "type": "symbol" + } + ] + }, + { + "name": "create", + "base": "", + "fields": [ + { + "name": "issuer", + "type": "name" + }, + { + "name": "maximum_supply", + "type": "asset" + } + ] + }, + { + "name": "currency_stats", + "base": "", + "fields": [ + { + "name": "supply", + "type": "asset" + }, + { + "name": "max_supply", + "type": "asset" + }, + { + "name": "issuer", + "type": "name" + } + ] + }, + { + "name": "issue", + "base": "", + "fields": [ + { + "name": "to", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "issuefixed", + "base": "", + "fields": [ + { + "name": "to", + "type": "name" + }, + { + "name": "supply", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "open", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "symbol", + "type": "symbol" + }, + { + "name": "ram_payer", + "type": "name" + } + ] + }, + { + "name": "retire", + "base": "", + "fields": [ + { + "name": "quantity", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "rmblacklist", + "base": "", + "fields": [ + { + "name": "accounts", + "type": "name[]" + } + ] + }, + { + "name": "setmaxsupply", + "base": "", + "fields": [ + { + "name": "issuer", + "type": "name" + }, + { + "name": "maximum_supply", + "type": "asset" + } + ] + }, + { + "name": "transfer", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "to", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + } + ], + "actions": [ + { + "name": "addblacklist", + "type": "addblacklist", + "ricardian_contract": "" + }, + { + "name": "close", + "type": "close", + "ricardian_contract": "" + }, + { + "name": "create", + "type": "create", + "ricardian_contract": "" + }, + { + "name": "issue", + "type": "issue", + "ricardian_contract": "" + }, + { + "name": "issuefixed", + "type": "issuefixed", + "ricardian_contract": "" + }, + { + "name": "open", + "type": "open", + "ricardian_contract": "" + }, + { + "name": "retire", + "type": "retire", + "ricardian_contract": "" + }, + { + "name": "rmblacklist", + "type": "rmblacklist", + "ricardian_contract": "" + }, + { + "name": "setmaxsupply", + "type": "setmaxsupply", + "ricardian_contract": "" + }, + { + "name": "transfer", + "type": "transfer", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "accounts", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "account" + }, + { + "name": "blacklist", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "account_blacklist" + }, + { + "name": "stat", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "currency_stats" + } + ], + "ricardian_clauses": [], + "error_messages": [], + "abi_extensions": [], + "variants": [], + "action_results": [] +} diff --git a/test/fixtures/vexanium/vexcore.abi.json b/test/fixtures/vexanium/vexcore.abi.json new file mode 100644 index 0000000..941fc75 --- /dev/null +++ b/test/fixtures/vexanium/vexcore.abi.json @@ -0,0 +1,3334 @@ +{ + "version": "eosio::abi/1.2", + "types": [ + { + "new_type_name": "block_signing_authority", + "type": "variant_block_signing_authority_v0" + }, + { + "new_type_name": "blockchain_parameters_t", + "type": "blockchain_parameters" + } + ], + "structs": [ + { + "name": "abi_hash", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "hash", + "type": "checksum256" + } + ] + }, + { + "name": "actfinkey", + "base": "", + "fields": [ + { + "name": "finalizer_name", + "type": "name" + }, + { + "name": "finalizer_key", + "type": "string" + } + ] + }, + { + "name": "action_return_buyram", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "bytes_purchased", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + }, + { + "name": "fee", + "type": "asset" + } + ] + }, + { + "name": "action_return_ramtransfer", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "to", + "type": "name" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "from_ram_bytes", + "type": "int64" + }, + { + "name": "to_ram_bytes", + "type": "int64" + } + ] + }, + { + "name": "action_return_sellram", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "bytes_sold", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + }, + { + "name": "fee", + "type": "asset" + } + ] + }, + { + "name": "activate", + "base": "", + "fields": [ + { + "name": "feature_digest", + "type": "checksum256" + } + ] + }, + { + "name": "authority", + "base": "", + "fields": [ + { + "name": "threshold", + "type": "uint32" + }, + { + "name": "keys", + "type": "key_weight[]" + }, + { + "name": "accounts", + "type": "permission_level_weight[]" + }, + { + "name": "waits", + "type": "wait_weight[]" + } + ] + }, + { + "name": "bid_refund", + "base": "", + "fields": [ + { + "name": "bidder", + "type": "name" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "bidname", + "base": "", + "fields": [ + { + "name": "bidder", + "type": "name" + }, + { + "name": "newname", + "type": "name" + }, + { + "name": "bid", + "type": "asset" + } + ] + }, + { + "name": "bidrefund", + "base": "", + "fields": [ + { + "name": "bidder", + "type": "name" + }, + { + "name": "newname", + "type": "name" + } + ] + }, + { + "name": "block_header", + "base": "", + "fields": [ + { + "name": "timestamp", + "type": "uint32" + }, + { + "name": "producer", + "type": "name" + }, + { + "name": "confirmed", + "type": "uint16" + }, + { + "name": "previous", + "type": "checksum256" + }, + { + "name": "transaction_mroot", + "type": "checksum256" + }, + { + "name": "action_mroot", + "type": "checksum256" + }, + { + "name": "schedule_version", + "type": "uint32" + }, + { + "name": "new_producers", + "type": "producer_schedule?" + } + ] + }, + { + "name": "block_info_record", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "block_height", + "type": "uint32" + }, + { + "name": "block_timestamp", + "type": "time_point" + } + ] + }, + { + "name": "block_signing_authority_v0", + "base": "", + "fields": [ + { + "name": "threshold", + "type": "uint32" + }, + { + "name": "keys", + "type": "key_weight[]" + } + ] + }, + { + "name": "blockchain_parameters", + "base": "", + "fields": [ + { + "name": "max_block_net_usage", + "type": "uint64" + }, + { + "name": "target_block_net_usage_pct", + "type": "uint32" + }, + { + "name": "max_transaction_net_usage", + "type": "uint32" + }, + { + "name": "base_per_transaction_net_usage", + "type": "uint32" + }, + { + "name": "net_usage_leeway", + "type": "uint32" + }, + { + "name": "context_free_discount_net_usage_num", + "type": "uint32" + }, + { + "name": "context_free_discount_net_usage_den", + "type": "uint32" + }, + { + "name": "max_block_cpu_usage", + "type": "uint32" + }, + { + "name": "target_block_cpu_usage_pct", + "type": "uint32" + }, + { + "name": "max_transaction_cpu_usage", + "type": "uint32" + }, + { + "name": "min_transaction_cpu_usage", + "type": "uint32" + }, + { + "name": "max_transaction_lifetime", + "type": "uint32" + }, + { + "name": "deferred_trx_expiration_window", + "type": "uint32" + }, + { + "name": "max_transaction_delay", + "type": "uint32" + }, + { + "name": "max_inline_action_size", + "type": "uint32" + }, + { + "name": "max_inline_action_depth", + "type": "uint16" + }, + { + "name": "max_authority_depth", + "type": "uint16" + } + ] + }, + { + "name": "buyram", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "quant", + "type": "asset" + } + ] + }, + { + "name": "buyramburn", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "buyrambytes", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "bytes", + "type": "uint32" + } + ] + }, + { + "name": "buyramself", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "quant", + "type": "asset" + } + ] + }, + { + "name": "buyrex", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "canceldelay", + "base": "", + "fields": [ + { + "name": "canceling_auth", + "type": "permission_level" + }, + { + "name": "trx_id", + "type": "checksum256" + } + ] + }, + { + "name": "cfgpowerup", + "base": "", + "fields": [ + { + "name": "args", + "type": "powerup_config" + } + ] + }, + { + "name": "claimrewards", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + } + ] + }, + { + "name": "closerex", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + } + ] + }, + { + "name": "cnclrexorder", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + } + ] + }, + { + "name": "connector", + "base": "", + "fields": [ + { + "name": "balance", + "type": "asset" + }, + { + "name": "weight", + "type": "float64" + } + ] + }, + { + "name": "consolidate", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + } + ] + }, + { + "name": "defcpuloan", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "loan_num", + "type": "uint64" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "defnetloan", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "loan_num", + "type": "uint64" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "delegatebw", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "stake_net_quantity", + "type": "asset" + }, + { + "name": "stake_cpu_quantity", + "type": "asset" + }, + { + "name": "transfer", + "type": "bool" + } + ] + }, + { + "name": "delegated_bandwidth", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "to", + "type": "name" + }, + { + "name": "net_weight", + "type": "asset" + }, + { + "name": "cpu_weight", + "type": "asset" + } + ] + }, + { + "name": "deleteauth", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "permission", + "type": "name" + }, + { + "name": "authorized_by", + "type": "name$" + } + ] + }, + { + "name": "delfinkey", + "base": "", + "fields": [ + { + "name": "finalizer_name", + "type": "name" + }, + { + "name": "finalizer_key", + "type": "string" + } + ] + }, + { + "name": "delschedule", + "base": "", + "fields": [ + { + "name": "start_time", + "type": "time_point_sec" + } + ] + }, + { + "name": "deposit", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "donatetorex", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "eosio_global_state", + "base": "blockchain_parameters", + "fields": [ + { + "name": "max_ram_size", + "type": "uint64" + }, + { + "name": "total_ram_bytes_reserved", + "type": "uint64" + }, + { + "name": "total_ram_stake", + "type": "int64" + }, + { + "name": "last_producer_schedule_update", + "type": "block_timestamp_type" + }, + { + "name": "last_pervote_bucket_fill", + "type": "time_point" + }, + { + "name": "pervote_bucket", + "type": "int64" + }, + { + "name": "perblock_bucket", + "type": "int64" + }, + { + "name": "total_unpaid_blocks", + "type": "uint32" + }, + { + "name": "total_activated_stake", + "type": "int64" + }, + { + "name": "thresh_activated_stake_time", + "type": "time_point" + }, + { + "name": "last_producer_schedule_size", + "type": "uint16" + }, + { + "name": "total_producer_vote_weight", + "type": "float64" + }, + { + "name": "last_name_close", + "type": "block_timestamp_type" + } + ] + }, + { + "name": "eosio_global_state2", + "base": "", + "fields": [ + { + "name": "new_ram_per_block", + "type": "uint16" + }, + { + "name": "last_ram_increase", + "type": "block_timestamp_type" + }, + { + "name": "last_block_num", + "type": "block_timestamp_type" + }, + { + "name": "total_producer_votepay_share", + "type": "float64" + }, + { + "name": "revision", + "type": "uint8" + } + ] + }, + { + "name": "eosio_global_state3", + "base": "", + "fields": [ + { + "name": "last_vpay_state_update", + "type": "time_point" + }, + { + "name": "total_vpay_share_change_rate", + "type": "float64" + } + ] + }, + { + "name": "eosio_global_state4", + "base": "", + "fields": [ + { + "name": "continuous_rate", + "type": "float64" + }, + { + "name": "inflation_pay_factor", + "type": "int64" + }, + { + "name": "votepay_factor", + "type": "int64" + } + ] + }, + { + "name": "exchange_state", + "base": "", + "fields": [ + { + "name": "supply", + "type": "asset" + }, + { + "name": "base", + "type": "connector" + }, + { + "name": "quote", + "type": "connector" + } + ] + }, + { + "name": "execschedule", + "base": "", + "fields": [] + }, + { + "name": "fin_key_id_generator_info", + "base": "", + "fields": [ + { + "name": "next_finalizer_key_id", + "type": "uint64" + } + ] + }, + { + "name": "finalizer_auth_info", + "base": "", + "fields": [ + { + "name": "key_id", + "type": "uint64" + }, + { + "name": "fin_authority", + "type": "finalizer_authority" + } + ] + }, + { + "name": "finalizer_authority", + "base": "", + "fields": [ + { + "name": "description", + "type": "string" + }, + { + "name": "weight", + "type": "uint64" + }, + { + "name": "public_key", + "type": "bytes" + } + ] + }, + { + "name": "finalizer_info", + "base": "", + "fields": [ + { + "name": "finalizer_name", + "type": "name" + }, + { + "name": "active_key_id", + "type": "uint64" + }, + { + "name": "active_key_binary", + "type": "bytes" + }, + { + "name": "finalizer_key_count", + "type": "uint32" + } + ] + }, + { + "name": "finalizer_key_info", + "base": "", + "fields": [ + { + "name": "id", + "type": "uint64" + }, + { + "name": "finalizer_name", + "type": "name" + }, + { + "name": "finalizer_key", + "type": "string" + }, + { + "name": "finalizer_key_binary", + "type": "bytes" + } + ] + }, + { + "name": "fundcpuloan", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "loan_num", + "type": "uint64" + }, + { + "name": "payment", + "type": "asset" + } + ] + }, + { + "name": "fundnetloan", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "loan_num", + "type": "uint64" + }, + { + "name": "payment", + "type": "asset" + } + ] + }, + { + "name": "init", + "base": "", + "fields": [ + { + "name": "version", + "type": "varuint32" + }, + { + "name": "core", + "type": "symbol" + } + ] + }, + { + "name": "instant_unstake_info", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + } + ] + }, + { + "name": "key_weight", + "base": "", + "fields": [ + { + "name": "key", + "type": "public_key" + }, + { + "name": "weight", + "type": "uint16" + } + ] + }, + { + "name": "last_prop_finalizers_info", + "base": "", + "fields": [ + { + "name": "last_proposed_finalizers", + "type": "finalizer_auth_info[]" + } + ] + }, + { + "name": "limitauthchg", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "allow_perms", + "type": "name[]" + }, + { + "name": "disallow_perms", + "type": "name[]" + } + ] + }, + { + "name": "linkauth", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "code", + "type": "name" + }, + { + "name": "type", + "type": "name" + }, + { + "name": "requirement", + "type": "name" + }, + { + "name": "authorized_by", + "type": "name$" + } + ] + }, + { + "name": "logbuyram", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + }, + { + "name": "fee", + "type": "asset" + } + ] + }, + { + "name": "logramchange", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + } + ] + }, + { + "name": "logsellram", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "quantity", + "type": "asset" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "ram_bytes", + "type": "int64" + }, + { + "name": "fee", + "type": "asset" + } + ] + }, + { + "name": "logsystemfee", + "base": "", + "fields": [ + { + "name": "protocol", + "type": "name" + }, + { + "name": "fee", + "type": "asset" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "migrate", + "base": "", + "fields": [] + }, + { + "name": "mvfrsavings", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "rex", + "type": "asset" + } + ] + }, + { + "name": "mvtosavings", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "rex", + "type": "asset" + } + ] + }, + { + "name": "name_bid", + "base": "", + "fields": [ + { + "name": "newname", + "type": "name" + }, + { + "name": "high_bidder", + "type": "name" + }, + { + "name": "high_bid", + "type": "int64" + }, + { + "name": "last_bid_time", + "type": "time_point" + } + ] + }, + { + "name": "newaccount", + "base": "", + "fields": [ + { + "name": "creator", + "type": "name" + }, + { + "name": "name", + "type": "name" + }, + { + "name": "owner", + "type": "authority" + }, + { + "name": "active", + "type": "authority" + } + ] + }, + { + "name": "onblock", + "base": "", + "fields": [ + { + "name": "header", + "type": "block_header" + } + ] + }, + { + "name": "onerror", + "base": "", + "fields": [ + { + "name": "sender_id", + "type": "uint128" + }, + { + "name": "sent_trx", + "type": "bytes" + } + ] + }, + { + "name": "pair_time_point_sec_int64", + "base": "", + "fields": [ + { + "name": "first", + "type": "time_point_sec" + }, + { + "name": "second", + "type": "int64" + } + ] + }, + { + "name": "permission_level", + "base": "", + "fields": [ + { + "name": "actor", + "type": "name" + }, + { + "name": "permission", + "type": "name" + } + ] + }, + { + "name": "permission_level_weight", + "base": "", + "fields": [ + { + "name": "permission", + "type": "permission_level" + }, + { + "name": "weight", + "type": "uint16" + } + ] + }, + { + "name": "powerup", + "base": "", + "fields": [ + { + "name": "payer", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "days", + "type": "uint32" + }, + { + "name": "net_frac", + "type": "int64" + }, + { + "name": "cpu_frac", + "type": "int64" + }, + { + "name": "max_payment", + "type": "asset" + } + ] + }, + { + "name": "powerup_config", + "base": "", + "fields": [ + { + "name": "net", + "type": "powerup_config_resource" + }, + { + "name": "cpu", + "type": "powerup_config_resource" + }, + { + "name": "powerup_days", + "type": "uint32?" + }, + { + "name": "min_powerup_fee", + "type": "asset?" + } + ] + }, + { + "name": "powerup_config_resource", + "base": "", + "fields": [ + { + "name": "current_weight_ratio", + "type": "int64?" + }, + { + "name": "target_weight_ratio", + "type": "int64?" + }, + { + "name": "assumed_stake_weight", + "type": "int64?" + }, + { + "name": "target_timestamp", + "type": "time_point_sec?" + }, + { + "name": "exponent", + "type": "float64?" + }, + { + "name": "decay_secs", + "type": "uint32?" + }, + { + "name": "min_price", + "type": "asset?" + }, + { + "name": "max_price", + "type": "asset?" + } + ] + }, + { + "name": "powerup_order", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "id", + "type": "uint64" + }, + { + "name": "owner", + "type": "name" + }, + { + "name": "net_weight", + "type": "int64" + }, + { + "name": "cpu_weight", + "type": "int64" + }, + { + "name": "expires", + "type": "time_point_sec" + } + ] + }, + { + "name": "powerup_state", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "net", + "type": "powerup_state_resource" + }, + { + "name": "cpu", + "type": "powerup_state_resource" + }, + { + "name": "powerup_days", + "type": "uint32" + }, + { + "name": "min_powerup_fee", + "type": "asset" + } + ] + }, + { + "name": "powerup_state_resource", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "weight", + "type": "int64" + }, + { + "name": "weight_ratio", + "type": "int64" + }, + { + "name": "assumed_stake_weight", + "type": "int64" + }, + { + "name": "initial_weight_ratio", + "type": "int64" + }, + { + "name": "target_weight_ratio", + "type": "int64" + }, + { + "name": "initial_timestamp", + "type": "time_point_sec" + }, + { + "name": "target_timestamp", + "type": "time_point_sec" + }, + { + "name": "exponent", + "type": "float64" + }, + { + "name": "decay_secs", + "type": "uint32" + }, + { + "name": "min_price", + "type": "asset" + }, + { + "name": "max_price", + "type": "asset" + }, + { + "name": "utilization", + "type": "int64" + }, + { + "name": "adjusted_utilization", + "type": "int64" + }, + { + "name": "utilization_timestamp", + "type": "time_point_sec" + } + ] + }, + { + "name": "powerupexec", + "base": "", + "fields": [ + { + "name": "user", + "type": "name" + }, + { + "name": "max", + "type": "uint16" + } + ] + }, + { + "name": "producer_info", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "total_votes", + "type": "float64" + }, + { + "name": "producer_key", + "type": "public_key" + }, + { + "name": "is_active", + "type": "bool" + }, + { + "name": "url", + "type": "string" + }, + { + "name": "unpaid_blocks", + "type": "uint32" + }, + { + "name": "last_claim_time", + "type": "time_point" + }, + { + "name": "location", + "type": "uint16" + }, + { + "name": "producer_authority", + "type": "block_signing_authority$" + } + ] + }, + { + "name": "producer_info2", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "votepay_share", + "type": "float64" + }, + { + "name": "last_votepay_share_update", + "type": "time_point" + } + ] + }, + { + "name": "producer_key", + "base": "", + "fields": [ + { + "name": "producer_name", + "type": "name" + }, + { + "name": "block_signing_key", + "type": "public_key" + } + ] + }, + { + "name": "producer_schedule", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint32" + }, + { + "name": "producers", + "type": "producer_key[]" + } + ] + }, + { + "name": "ramburn", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "ramtransfer", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "to", + "type": "name" + }, + { + "name": "bytes", + "type": "int64" + }, + { + "name": "memo", + "type": "string" + } + ] + }, + { + "name": "refund", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + } + ] + }, + { + "name": "refund_request", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "request_time", + "type": "time_point_sec" + }, + { + "name": "net_amount", + "type": "asset" + }, + { + "name": "cpu_amount", + "type": "asset" + } + ] + }, + { + "name": "regfinkey", + "base": "", + "fields": [ + { + "name": "finalizer_name", + "type": "name" + }, + { + "name": "finalizer_key", + "type": "string" + }, + { + "name": "proof_of_possession", + "type": "string" + } + ] + }, + { + "name": "regproducer", + "base": "", + "fields": [ + { + "name": "producer", + "type": "name" + }, + { + "name": "producer_key", + "type": "public_key" + }, + { + "name": "url", + "type": "string" + }, + { + "name": "location", + "type": "uint16" + } + ] + }, + { + "name": "regproducer2", + "base": "", + "fields": [ + { + "name": "producer", + "type": "name" + }, + { + "name": "producer_authority", + "type": "block_signing_authority" + }, + { + "name": "url", + "type": "string" + }, + { + "name": "location", + "type": "uint16" + } + ] + }, + { + "name": "regproxy", + "base": "", + "fields": [ + { + "name": "proxy", + "type": "name" + }, + { + "name": "isproxy", + "type": "bool" + } + ] + }, + { + "name": "rentcpu", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "loan_payment", + "type": "asset" + }, + { + "name": "loan_fund", + "type": "asset" + } + ] + }, + { + "name": "rentnet", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "loan_payment", + "type": "asset" + }, + { + "name": "loan_fund", + "type": "asset" + } + ] + }, + { + "name": "rex_balance", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "owner", + "type": "name" + }, + { + "name": "vote_stake", + "type": "asset" + }, + { + "name": "rex_balance", + "type": "asset" + }, + { + "name": "matured_rex", + "type": "int64" + }, + { + "name": "rex_maturities", + "type": "pair_time_point_sec_int64[]" + } + ] + }, + { + "name": "rex_fund", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "owner", + "type": "name" + }, + { + "name": "balance", + "type": "asset" + } + ] + }, + { + "name": "rex_loan", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "from", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "payment", + "type": "asset" + }, + { + "name": "balance", + "type": "asset" + }, + { + "name": "total_staked", + "type": "asset" + }, + { + "name": "loan_num", + "type": "uint64" + }, + { + "name": "expiration", + "type": "time_point" + } + ] + }, + { + "name": "rex_maturity", + "base": "", + "fields": [ + { + "name": "num_of_maturity_buckets", + "type": "uint32" + }, + { + "name": "sell_matured_rex", + "type": "bool" + }, + { + "name": "buy_rex_to_savings", + "type": "bool" + } + ] + }, + { + "name": "rex_order", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "owner", + "type": "name" + }, + { + "name": "rex_requested", + "type": "asset" + }, + { + "name": "proceeds", + "type": "asset" + }, + { + "name": "stake_change", + "type": "asset" + }, + { + "name": "order_time", + "type": "time_point" + }, + { + "name": "is_open", + "type": "bool" + } + ] + }, + { + "name": "rex_pool", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "total_lent", + "type": "asset" + }, + { + "name": "total_unlent", + "type": "asset" + }, + { + "name": "total_rent", + "type": "asset" + }, + { + "name": "total_lendable", + "type": "asset" + }, + { + "name": "total_rex", + "type": "asset" + }, + { + "name": "namebid_proceeds", + "type": "asset" + }, + { + "name": "loan_num", + "type": "uint64" + } + ] + }, + { + "name": "rex_return_buckets", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "return_buckets", + "type": "pair_time_point_sec_int64[]" + } + ] + }, + { + "name": "rex_return_pool", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "last_dist_time", + "type": "time_point_sec" + }, + { + "name": "pending_bucket_time", + "type": "time_point_sec" + }, + { + "name": "oldest_bucket_time", + "type": "time_point_sec" + }, + { + "name": "pending_bucket_proceeds", + "type": "int64" + }, + { + "name": "current_rate_of_increase", + "type": "int64" + }, + { + "name": "proceeds", + "type": "int64" + } + ] + }, + { + "name": "rexexec", + "base": "", + "fields": [ + { + "name": "user", + "type": "name" + }, + { + "name": "max", + "type": "uint16" + } + ] + }, + { + "name": "rmvproducer", + "base": "", + "fields": [ + { + "name": "producer", + "type": "name" + } + ] + }, + { + "name": "schedules_info", + "base": "", + "fields": [ + { + "name": "start_time", + "type": "time_point_sec" + }, + { + "name": "continuous_rate", + "type": "float64" + } + ] + }, + { + "name": "sellram", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "bytes", + "type": "int64" + } + ] + }, + { + "name": "sellrex", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "rex", + "type": "asset" + } + ] + }, + { + "name": "setabi", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "abi", + "type": "bytes" + }, + { + "name": "memo", + "type": "string$" + } + ] + }, + { + "name": "setacctcpu", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "cpu_weight", + "type": "int64?" + } + ] + }, + { + "name": "setacctnet", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "net_weight", + "type": "int64?" + } + ] + }, + { + "name": "setacctram", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "ram_bytes", + "type": "int64?" + } + ] + }, + { + "name": "setalimits", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "ram_bytes", + "type": "int64" + }, + { + "name": "net_weight", + "type": "int64" + }, + { + "name": "cpu_weight", + "type": "int64" + } + ] + }, + { + "name": "setcode", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "vmtype", + "type": "uint8" + }, + { + "name": "vmversion", + "type": "uint8" + }, + { + "name": "code", + "type": "bytes" + }, + { + "name": "memo", + "type": "string$" + } + ] + }, + { + "name": "setinflation", + "base": "", + "fields": [ + { + "name": "annual_rate", + "type": "int64" + }, + { + "name": "inflation_pay_factor", + "type": "int64" + }, + { + "name": "votepay_factor", + "type": "int64" + } + ] + }, + { + "name": "setinstant", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "enabled", + "type": "bool" + } + ] + }, + { + "name": "setparams", + "base": "", + "fields": [ + { + "name": "params", + "type": "blockchain_parameters_t" + } + ] + }, + { + "name": "setpayfactor", + "base": "", + "fields": [ + { + "name": "inflation_pay_factor", + "type": "int64" + }, + { + "name": "votepay_factor", + "type": "int64" + } + ] + }, + { + "name": "setpriv", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "is_priv", + "type": "uint8" + } + ] + }, + { + "name": "setram", + "base": "", + "fields": [ + { + "name": "max_ram_size", + "type": "uint64" + } + ] + }, + { + "name": "setramrate", + "base": "", + "fields": [ + { + "name": "bytes_per_block", + "type": "uint16" + } + ] + }, + { + "name": "setrex", + "base": "", + "fields": [ + { + "name": "balance", + "type": "asset" + } + ] + }, + { + "name": "setrexlimit", + "base": "", + "fields": [ + { + "name": "new_limit", + "type": "asset" + } + ] + }, + { + "name": "setrexmature", + "base": "", + "fields": [ + { + "name": "num_of_maturity_buckets", + "type": "uint32?" + }, + { + "name": "sell_matured_rex", + "type": "bool?" + }, + { + "name": "buy_rex_to_savings", + "type": "bool?" + } + ] + }, + { + "name": "setschedule", + "base": "", + "fields": [ + { + "name": "start_time", + "type": "time_point_sec" + }, + { + "name": "continuous_rate", + "type": "float64" + } + ] + }, + { + "name": "setundlimit", + "base": "", + "fields": [ + { + "name": "new_limit", + "type": "asset" + } + ] + }, + { + "name": "switchtosvnn", + "base": "", + "fields": [] + }, + { + "name": "undelegatebw", + "base": "", + "fields": [ + { + "name": "from", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "unstake_net_quantity", + "type": "asset" + }, + { + "name": "unstake_cpu_quantity", + "type": "asset" + } + ] + }, + { + "name": "unlinkauth", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "code", + "type": "name" + }, + { + "name": "type", + "type": "name" + }, + { + "name": "authorized_by", + "type": "name$" + } + ] + }, + { + "name": "unregprod", + "base": "", + "fields": [ + { + "name": "producer", + "type": "name" + } + ] + }, + { + "name": "unstaketorex", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "receiver", + "type": "name" + }, + { + "name": "from_net", + "type": "asset" + }, + { + "name": "from_cpu", + "type": "asset" + } + ] + }, + { + "name": "unvest", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "unvest_net_quantity", + "type": "asset" + }, + { + "name": "unvest_cpu_quantity", + "type": "asset" + } + ] + }, + { + "name": "updateauth", + "base": "", + "fields": [ + { + "name": "account", + "type": "name" + }, + { + "name": "permission", + "type": "name" + }, + { + "name": "parent", + "type": "name" + }, + { + "name": "auth", + "type": "authority" + }, + { + "name": "authorized_by", + "type": "name$" + } + ] + }, + { + "name": "updaterex", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + } + ] + }, + { + "name": "updtrevision", + "base": "", + "fields": [ + { + "name": "revision", + "type": "uint8" + } + ] + }, + { + "name": "user_resources", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "net_weight", + "type": "asset" + }, + { + "name": "cpu_weight", + "type": "asset" + }, + { + "name": "ram_bytes", + "type": "int64" + } + ] + }, + { + "name": "vex_limits_state", + "base": "", + "fields": [ + { + "name": "daily_withdraw_limit", + "type": "asset" + }, + { + "name": "daily_undelegate_limit", + "type": "asset" + }, + { + "name": "total_withdrawn_today", + "type": "asset" + }, + { + "name": "total_undelegated_today", + "type": "asset" + }, + { + "name": "last_reset_time", + "type": "time_point" + } + ] + }, + { + "name": "voteproducer", + "base": "", + "fields": [ + { + "name": "voter", + "type": "name" + }, + { + "name": "proxy", + "type": "name" + }, + { + "name": "producers", + "type": "name[]" + } + ] + }, + { + "name": "voter_info", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "proxy", + "type": "name" + }, + { + "name": "producers", + "type": "name[]" + }, + { + "name": "staked", + "type": "int64" + }, + { + "name": "last_vote_weight", + "type": "float64" + }, + { + "name": "proxied_vote_weight", + "type": "float64" + }, + { + "name": "is_proxy", + "type": "bool" + }, + { + "name": "flags1", + "type": "uint32" + }, + { + "name": "reserved2", + "type": "uint32" + }, + { + "name": "reserved3", + "type": "asset" + } + ] + }, + { + "name": "voteupdate", + "base": "", + "fields": [ + { + "name": "voter_name", + "type": "name" + } + ] + }, + { + "name": "wait_weight", + "base": "", + "fields": [ + { + "name": "wait_sec", + "type": "uint32" + }, + { + "name": "weight", + "type": "uint16" + } + ] + }, + { + "name": "withdraw", + "base": "", + "fields": [ + { + "name": "owner", + "type": "name" + }, + { + "name": "amount", + "type": "asset" + } + ] + }, + { + "name": "limit_auth_change", + "base": "", + "fields": [ + { + "name": "version", + "type": "uint8" + }, + { + "name": "account", + "type": "name" + }, + { + "name": "allow_perms", + "type": "name[]" + }, + { + "name": "disallow_perms", + "type": "name[]" + } + ] + } + ], + "actions": [ + { + "name": "actfinkey", + "type": "actfinkey", + "ricardian_contract": "" + }, + { + "name": "activate", + "type": "activate", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Activate Protocol Feature\nsummary: 'Activate protocol feature {{nowrap feature_digest}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} activates the protocol feature with a digest of {{feature_digest}}." + }, + { + "name": "bidname", + "type": "bidname", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Bid On a Premium Account Name\nsummary: '{{nowrap bidder}} bids on the premium account name {{nowrap newname}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\n{{bidder}} bids {{bid}} on an auction to own the premium account name {{newname}}.\n\n{{bidder}} transfers {{bid}} to the system to cover the cost of the bid, which will be returned to {{bidder}} only if {{bidder}} is later outbid in the auction for {{newname}} by another account.\n\nIf the auction for {{newname}} closes with {{bidder}} remaining as the highest bidder, {{bidder}} will be authorized to create the account with name {{newname}}.\n\n## Bid refund behavior\n\nIf {{bidder}}’s bid on {{newname}} is later outbid by another account, {{bidder}} will be able to claim back the transferred amount of {{bid}}. The system will attempt to automatically do this on behalf of {{bidder}}, but the automatic refund may occasionally fail which will then require {{bidder}} to manually claim the refund with the bidrefund action.\n\n## Auction close criteria\n\nThe system should automatically close the auction for {{newname}} if it satisfies the condition that over a period of two minutes the following two properties continuously hold:\n\n- no one has bid on {{newname}} within the last 24 hours;\n- and, the value of the latest bid on {{newname}} is greater than the value of the bids on each of the other open auctions.\n\nBe aware that the condition to close the auction described above are sufficient but not necessary. The auction for {{newname}} cannot close unless both of the properties are simultaneously satisfied, but it may be closed without requiring the properties to hold for a period of 2 minutes." + }, + { + "name": "bidrefund", + "type": "bidrefund", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Claim Refund on Name Bid\nsummary: 'Claim refund on {{nowrap newname}} bid'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\n{{bidder}} claims refund on {{newname}} bid after being outbid by someone else." + }, + { + "name": "buyram", + "type": "buyram", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Buy RAM\nsummary: '{{nowrap payer}} buys RAM on behalf of {{nowrap receiver}} by paying {{nowrap quant}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\n{{payer}} buys RAM on behalf of {{receiver}} by paying {{quant}}. This transaction will incur a 0.5% fee out of {{quant}} and the amount of RAM delivered will depend on market rates." + }, + { + "name": "buyramburn", + "type": "buyramburn", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Buy and Burn RAM\nsummary: 'Buy and immediately Burn {{quantity}} of RAM from {{nowrap payer}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\nBuy and Burn {{quantity}} of RAM from account {{payer}}.\n\n{{#if memo}}There is a memo attached to the action stating:\n{{memo}}\n{{/if}}" + }, + { + "name": "buyrambytes", + "type": "buyrambytes", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Buy RAM\nsummary: '{{nowrap payer}} buys {{nowrap bytes}} bytes of RAM on behalf of {{nowrap receiver}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\n{{payer}} buys approximately {{bytes}} bytes of RAM on behalf of {{receiver}} by paying market rates for RAM. This transaction will incur a 0.5% fee and the cost will depend on market rates." + }, + { + "name": "buyramself", + "type": "buyramself", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Buy RAM self\nsummary: '{{nowrap account}} buys RAM to self by paying {{nowrap quant}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\n{{account}} buys RAM to self by paying {{quant}}. This transaction will incur a 0.5% fee out of {{quant}} and the amount of RAM delivered will depend on market rates." + }, + { + "name": "buyrex", + "type": "buyrex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Buy REX Tokens\nsummary: '{{nowrap from}} buys REX tokens in exchange for {{nowrap amount}} and their vote stake increases by {{nowrap amount}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{amount}} is taken out of {{from}}’s REX fund and used to purchase REX tokens at the current market exchange rate. In order for the action to succeed, {{from}} must have voted for a proxy or at least 21 block producers. {{amount}} is added to {{from}}’s vote stake.\n\nA sell order of the purchased amount can only be initiated after waiting for the maturity period of 4 to 5 days to pass. Even then, depending on the market conditions, the initiated sell order may not be executed immediately." + }, + { + "name": "canceldelay", + "type": "canceldelay", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Cancel Delayed Transaction\nsummary: '{{nowrap canceling_auth.actor}} cancels a delayed transaction'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\n{{canceling_auth.actor}} cancels the delayed transaction with id {{trx_id}}." + }, + { + "name": "cfgpowerup", + "type": "cfgpowerup", + "ricardian_contract": "" + }, + { + "name": "claimrewards", + "type": "claimrewards", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Claim Block Producer Rewards\nsummary: '{{nowrap owner}} claims block and vote rewards'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{owner}} claims block and vote rewards from the system." + }, + { + "name": "closerex", + "type": "closerex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Cleanup Unused REX Data\nsummary: 'Delete REX related DB entries and free associated RAM'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nDelete REX related DB entries and free associated RAM for {{owner}}.\n\nTo fully delete all REX related DB entries, {{owner}} must ensure that their REX balance and REX fund amounts are both zero and they have no outstanding loans." + }, + { + "name": "cnclrexorder", + "type": "cnclrexorder", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Cancel Scheduled REX Sell Order\nsummary: '{{nowrap owner}} cancels a scheduled sell order if not yet filled'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{owner}} cancels their open sell order." + }, + { + "name": "consolidate", + "type": "consolidate", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Consolidate REX Maturity Buckets Into One\nsummary: 'Consolidate REX maturity buckets into one'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nConsolidate REX maturity buckets into one bucket that {{owner}} will not be able to sell until 4 to 5 days later." + }, + { + "name": "defcpuloan", + "type": "defcpuloan", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Withdraw from the Fund of a Specific CPU Loan\nsummary: '{{nowrap from}} transfers {{nowrap amount}} from the fund of CPU loan number {{nowrap loan_num}} back to REX fund'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from}} transfers {{amount}} from the fund of CPU loan number {{loan_num}} back to REX fund." + }, + { + "name": "defnetloan", + "type": "defnetloan", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Withdraw from the Fund of a Specific NET Loan\nsummary: '{{nowrap from}} transfers {{nowrap amount}} from the fund of NET loan number {{nowrap loan_num}} back to REX fund'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from}} transfers {{amount}} from the fund of NET loan number {{loan_num}} back to REX fund." + }, + { + "name": "delegatebw", + "type": "delegatebw", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Stake Tokens for NET and/or CPU\nsummary: 'Stake tokens for NET and/or CPU and optionally transfer ownership'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\n{{#if transfer}} {{from}} stakes on behalf of {{receiver}} {{stake_net_quantity}} for NET bandwidth and {{stake_cpu_quantity}} for CPU bandwidth.\n\nStaked tokens will also be transferred to {{receiver}}. The sum of these two quantities will be deducted from {{from}}’s liquid balance and add to the vote weight of {{receiver}}.\n{{else}}\n{{from}} stakes to self and delegates to {{receiver}} {{stake_net_quantity}} for NET bandwidth and {{stake_cpu_quantity}} for CPU bandwidth.\n\nThe sum of these two quantities add to the vote weight of {{from}}.\n{{/if}}" + }, + { + "name": "deleteauth", + "type": "deleteauth", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Delete Account Permission\nsummary: 'Delete the {{nowrap permission}} permission of {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\nDelete the {{permission}} permission of {{account}}." + }, + { + "name": "delfinkey", + "type": "delfinkey", + "ricardian_contract": "" + }, + { + "name": "delschedule", + "type": "delschedule", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Delete Annual Rate Schedule\nsummary: 'Delete annual rate schedule'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} to delete a pre-determined inflation schedule from {{start_time}} start time." + }, + { + "name": "deposit", + "type": "deposit", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Deposit Into REX Fund\nsummary: 'Add to {{nowrap owner}}’s REX fund by transferring {{nowrap amount}} from {{nowrap owner}}’s liquid balance'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nTransfer {{amount}} from {{owner}}’s liquid balance to {{owner}}’s REX fund. All proceeds and expenses related to REX are added to or taken out of this fund." + }, + { + "name": "donatetorex", + "type": "donatetorex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Donate system tokens to REX\nsummary: '{{nowrap payer}} donates {{nowrap quantity}} tokens to REX'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{quantity}} is taken out of {{payer}}’s token balance and given to REX with the included memo: \"{{memo}}\"." + }, + { + "name": "execschedule", + "type": "execschedule", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Execute Next Annual Rate Schedule\nsummary: 'Execute next annual rate schedule'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} to execute the next upcoming annual rate schedule." + }, + { + "name": "fundcpuloan", + "type": "fundcpuloan", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Deposit into the Fund of a Specific CPU Loan\nsummary: '{{nowrap from}} funds a CPU loan'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from}} transfers {{payment}} from REX fund to the fund of CPU loan number {{loan_num}} in order to be used in loan renewal at expiry. {{from}} can withdraw the total balance of the loan fund at any time." + }, + { + "name": "fundnetloan", + "type": "fundnetloan", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Deposit into the Fund of a Specific NET Loan\nsummary: '{{nowrap from}} funds a NET loan'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from}} transfers {{payment}} from REX fund to the fund of NET loan number {{loan_num}} in order to be used in loan renewal at expiry. {{from}} can withdraw the total balance of the loan fund at any time." + }, + { + "name": "init", + "type": "init", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Initialize System Contract\nsummary: 'Initialize system contract'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\nInitialize system contract. The core token symbol will be set to {{core}}." + }, + { + "name": "limitauthchg", + "type": "limitauthchg", + "ricardian_contract": "" + }, + { + "name": "linkauth", + "type": "linkauth", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Link Action to Permission\nsummary: '{{nowrap account}} sets the minimum required permission for the {{#if type}}{{nowrap type}} action of the{{/if}} {{nowrap code}} contract to {{nowrap requirement}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\n{{account}} sets the minimum required permission for the {{#if type}}{{type}} action of the{{/if}} {{code}} contract to {{requirement}}.\n\n{{#if type}}{{else}}Any links explicitly associated to specific actions of {{code}} will take precedence.{{/if}}" + }, + { + "name": "logbuyram", + "type": "logbuyram", + "ricardian_contract": "" + }, + { + "name": "logramchange", + "type": "logramchange", + "ricardian_contract": "" + }, + { + "name": "logsellram", + "type": "logsellram", + "ricardian_contract": "" + }, + { + "name": "logsystemfee", + "type": "logsystemfee", + "ricardian_contract": "" + }, + { + "name": "migrate", + "type": "migrate", + "ricardian_contract": "" + }, + { + "name": "mvfrsavings", + "type": "mvfrsavings", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Unlock REX Tokens\nsummary: '{{nowrap owner}} unlocks REX Tokens'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{owner}} unlocks {{rex}} by moving it out of the REX savings bucket. The unlocked REX tokens cannot be sold until 4 to 5 days later." + }, + { + "name": "mvtosavings", + "type": "mvtosavings", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Lock REX Tokens\nsummary: '{{nowrap owner}} locks REX Tokens'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{owner}} locks {{rex}} by moving it into the REX savings bucket. The locked REX tokens cannot be sold directly and will have to be unlocked explicitly before selling." + }, + { + "name": "newaccount", + "type": "newaccount", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Create New Account\nsummary: '{{nowrap creator}} creates a new account with the name {{nowrap name}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\n{{creator}} creates a new account with the name {{name}} and the following permissions:\n\nowner permission with authority:\n{{to_json owner}}\n\nactive permission with authority:\n{{to_json active}}" + }, + { + "name": "onblock", + "type": "onblock", + "ricardian_contract": "" + }, + { + "name": "onerror", + "type": "onerror", + "ricardian_contract": "" + }, + { + "name": "powerup", + "type": "powerup", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Powerup resources\nsummary: 'User may powerup to reserve resources'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\nUsers may use the powerup action to reserve resources." + }, + { + "name": "powerupexec", + "type": "powerupexec", + "ricardian_contract": "" + }, + { + "name": "ramburn", + "type": "ramburn", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Burn RAM from Account\nsummary: 'Burn unused RAM from {{nowrap owner}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\nBurn {{bytes}} bytes of unused RAM from account {{owner}}.\n\n{{#if memo}}There is a memo attached to the burn stating:\n{{memo}}\n{{/if}}" + }, + { + "name": "ramtransfer", + "type": "ramtransfer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Transfer RAM from Account\nsummary: 'Transfer unused RAM from {{nowrap from}} to {{nowrap to}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\nTransfer {{bytes}} bytes of unused RAM from account {{from}} to account {{to}}.\n\n{{#if memo}}There is a memo attached to the transfer stating:\n{{memo}}\n{{/if}}" + }, + { + "name": "refund", + "type": "refund", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Claim Unstaked Tokens\nsummary: 'Return previously unstaked tokens to {{nowrap owner}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\nReturn previously unstaked tokens to {{owner}} after the unstaking period has elapsed." + }, + { + "name": "regfinkey", + "type": "regfinkey", + "ricardian_contract": "" + }, + { + "name": "regproducer", + "type": "regproducer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Register as a Block Producer Candidate\nsummary: 'Register {{nowrap producer}} account as a block producer candidate'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/voting.png#db28cd3db6e62d4509af3644ce7d377329482a14bb4bfaca2aa5f1400d8e8a84\n---\n\nRegister {{producer}} account as a block producer candidate.\n\nURL: {{url}}\nLocation code: {{location}}\nBlock signing key: {{producer_key}}\n\n## Block Producer Agreement\n{{$clauses.BlockProducerAgreement}}" + }, + { + "name": "regproducer2", + "type": "regproducer2", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Register as a Block Producer Candidate\nsummary: 'Register {{nowrap producer}} account as a block producer candidate'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/voting.png#db28cd3db6e62d4509af3644ce7d377329482a14bb4bfaca2aa5f1400d8e8a84\n---\n\nRegister {{producer}} account as a block producer candidate.\n\nURL: {{url}}\nLocation code: {{location}}\nBlock signing authority:\n{{to_json producer_authority}}\n\n## Block Producer Agreement\n{{$clauses.BlockProducerAgreement}}" + }, + { + "name": "regproxy", + "type": "regproxy", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Register/unregister as a Proxy\nsummary: 'Register/unregister {{nowrap proxy}} as a proxy account'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/voting.png#db28cd3db6e62d4509af3644ce7d377329482a14bb4bfaca2aa5f1400d8e8a84\n---\n\n{{#if isproxy}}\n{{proxy}} registers as a proxy that can vote on behalf of accounts that appoint it as their proxy.\n{{else}}\n{{proxy}} unregisters as a proxy that can vote on behalf of accounts that appoint it as their proxy.\n{{/if}}" + }, + { + "name": "rentcpu", + "type": "rentcpu", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Rent CPU Bandwidth for 30 Days\nsummary: '{{nowrap from}} pays {{nowrap loan_payment}} to rent CPU bandwidth for {{nowrap receiver}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from}} pays {{loan_payment}} to rent CPU bandwidth on behalf of {{receiver}} for a period of 30 days.\n\n{{loan_payment}} is taken out of {{from}}’s REX fund. The market price determines the number of tokens to be staked to {{receiver}}’s CPU resources. In addition, {{from}} provides {{loan_fund}}, which is also taken out of {{from}}’s REX fund, to be used for automatic renewal of the loan.\n\nAt expiration, if the loan has less funds than {{loan_payment}}, it is closed and lent tokens that have been staked are taken out of {{receiver}}’s CPU bandwidth. Otherwise, it is renewed at the market price at the time of renewal, that is, the number of staked tokens is recalculated and {{receiver}}’s CPU bandwidth is updated accordingly. {{from}} can fund or defund a loan at any time before expiration. When the loan is closed, {{from}} is refunded any tokens remaining in the loan fund." + }, + { + "name": "rentnet", + "type": "rentnet", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Rent NET Bandwidth for 30 Days\nsummary: '{{nowrap from}} pays {{nowrap loan_payment}} to rent NET bandwidth for {{nowrap receiver}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from}} pays {{loan_payment}} to rent NET bandwidth on behalf of {{receiver}} for a period of 30 days.\n\n{{loan_payment}} is taken out of {{from}}’s REX fund. The market price determines the number of tokens to be staked to {{receiver}}’s NET resources for 30 days. In addition, {{from}} provides {{loan_fund}}, which is also taken out of {{from}}’s REX fund, to be used for automatic renewal of the loan.\n\nAt expiration, if the loan has less funds than {{loan_payment}}, it is closed and lent tokens that have been staked are taken out of {{receiver}}’s NET bandwidth. Otherwise, it is renewed at the market price at the time of renewal, that is, the number of staked tokens is recalculated and {{receiver}}’s NET bandwidth is updated accordingly. {{from}} can fund or defund a loan at any time before expiration. When the loan is closed, {{from}} is refunded any tokens remaining in the loan fund." + }, + { + "name": "rexexec", + "type": "rexexec", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Perform REX Maintenance\nsummary: 'Process sell orders and expired loans'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nPerforms REX maintenance by processing a maximum of {{max}} REX sell orders and expired loans. Any account can execute this action." + }, + { + "name": "rmvproducer", + "type": "rmvproducer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Forcibly Unregister a Block Producer Candidate\nsummary: '{{nowrap producer}} is unregistered as a block producer candidate'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} unregisters {{producer}} as a block producer candidate. {{producer}} account will retain its votes and those votes can change based on voter stake changes or votes removed from {{producer}}. However new voters will not be able to vote for {{producer}} while it remains unregistered." + }, + { + "name": "sellram", + "type": "sellram", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Sell RAM From Account\nsummary: 'Sell unused RAM from {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\nSell {{bytes}} bytes of unused RAM from account {{account}} at market price. This transaction will incur a 0.5% fee on the proceeds which depend on market rates." + }, + { + "name": "sellrex", + "type": "sellrex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Sell REX Tokens in Exchange for EOS\nsummary: '{{nowrap from}} sells {{nowrap rex}} tokens'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nThe 'rex' parameter no longer has an effect.\n\n{{from}} initiates a sell order to sell all of their matured REX tokens at the market exchange rate during the time at which the order is ultimately executed. \nIf {{from}} already has an open sell order in the sell queue, {{rex}} will be added to the amount of the sell order without change the position of the sell order within the queue. \nOnce the sell order is executed, proceeds are added to {{from}}’s REX fund, the value of sold REX tokens is deducted from {{from}}’s vote stake, and votes are updated accordingly.\n\nDepending on the market conditions, it may not be possible to fill the entire sell order immediately. In such a case, the sell order is added to the back of a sell queue. \nA sell order at the front of the sell queue will automatically be executed when the market conditions allow for the entire order to be filled. Regardless of the market conditions, \nthe system is designed to execute this sell order within 30 days. {{from}} can cancel the order at any time before it is filled using the cnclrexorder action." + }, + { + "name": "setabi", + "type": "setabi", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Deploy Contract ABI\nsummary: 'Deploy contract ABI on account {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\nDeploy the ABI file associated with the contract on account {{account}}." + }, + { + "name": "setacctcpu", + "type": "setacctcpu", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Explicitly Manage the CPU Quota of Account\nsummary: 'Explicitly manage the CPU bandwidth quota of account {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{#if_has_value cpu_weight}}\nExplicitly manage the CPU bandwidth quota of account {{account}} by pinning it to a weight of {{cpu_weight}}.\n\n{{account}} can stake and unstake, however, it will not change their CPU bandwidth quota as long as it remains pinned.\n{{else}}\nUnpin the CPU bandwidth quota of account {{account}}. The CPU bandwidth quota of {{account}} will be driven by the current tokens staked for CPU bandwidth by {{account}}.\n{{/if_has_value}}" + }, + { + "name": "setacctnet", + "type": "setacctnet", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Explicitly Manage the NET Quota of Account\nsummary: 'Explicitly manage the NET bandwidth quota of account {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{#if_has_value net_weight}}\nExplicitly manage the network bandwidth quota of account {{account}} by pinning it to a weight of {{net_weight}}.\n\n{{account}} can stake and unstake, however, it will not change their NET bandwidth quota as long as it remains pinned.\n{{else}}\nUnpin the NET bandwidth quota of account {{account}}. The NET bandwidth quota of {{account}} will be driven by the current tokens staked for NET bandwidth by {{account}}.\n{{/if_has_value}}" + }, + { + "name": "setacctram", + "type": "setacctram", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Explicitly Manage the RAM Quota of Account\nsummary: 'Explicitly manage the RAM quota of account {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{#if_has_value ram_bytes}}\nExplicitly manage the RAM quota of account {{account}} by pinning it to {{ram_bytes}} bytes.\n\n{{account}} can buy and sell RAM, however, it will not change their RAM quota as long as it remains pinned.\n{{else}}\nUnpin the RAM quota of account {{account}}. The RAM quota of {{account}} will be driven by the current RAM holdings of {{account}}.\n{{/if_has_value}}" + }, + { + "name": "setalimits", + "type": "setalimits", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Adjust Resource Limits of Account\nsummary: 'Adjust resource limits of account {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} updates {{account}}’s resource limits to have a RAM quota of {{ram_bytes}} bytes, a NET bandwidth quota of {{net_weight}} and a CPU bandwidth quota of {{cpu_weight}}." + }, + { + "name": "setcode", + "type": "setcode", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Deploy Contract Code\nsummary: 'Deploy contract code on account {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\nDeploy compiled contract code to the account {{account}}." + }, + { + "name": "setinflation", + "type": "setinflation", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set Inflation Parameters\nsummary: 'Set inflation parameters'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} sets the inflation parameters as follows:\n\n* Annual inflation rate (in units of a hundredth of a percent): {{annual_rate}}\n* Fraction of inflation used to reward block producers: 10000/{{inflation_pay_factor}}\n* Fraction of block producer rewards to be distributed proportional to blocks produced: 10000/{{votepay_factor}}" + }, + { + "name": "setinstant", + "type": "setinstant", + "ricardian_contract": "" + }, + { + "name": "setparams", + "type": "setparams", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set System Parameters\nsummary: 'Set System Parameters'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} sets system parameters to:\n{{to_json params}}" + }, + { + "name": "setpayfactor", + "type": "setpayfactor", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set Pay Factors\nsummary: 'Set pay factors'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} sets the inflation parameters as follows:\n\n* Fraction of inflation used to reward block producers: 10000/{{inflation_pay_factor}}\n* Fraction of block producer rewards to be distributed proportional to blocks produced: 10000/{{votepay_factor}}" + }, + { + "name": "setpriv", + "type": "setpriv", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Make an Account Privileged or Unprivileged\nsummary: '{{#if is_priv}}Make {{nowrap account}} privileged{{else}}Remove privileged status of {{nowrap account}}{{/if}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{#if is_priv}}\n{{$action.account}} makes {{account}} privileged.\n{{else}}\n{{$action.account}} removes privileged status of {{account}}.\n{{/if}}" + }, + { + "name": "setram", + "type": "setram", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Configure the Available RAM\nsummary: 'Configure the available RAM'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} configures the available RAM to {{max_ram_size}} bytes." + }, + { + "name": "setramrate", + "type": "setramrate", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set the Rate of Increase of RAM\nsummary: 'Set the rate of increase of RAM per block'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} sets the rate of increase of RAM to {{bytes_per_block}} bytes/block." + }, + { + "name": "setrex", + "type": "setrex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Adjust REX Pool Virtual Balance\nsummary: 'Adjust REX Pool Virtual Balance'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} adjusts REX loan rate by setting REX pool virtual balance to {{balance}}. No token transfer or issue is executed in this action." + }, + { + "name": "setrexlimit", + "type": "setrexlimit", + "ricardian_contract": "" + }, + { + "name": "setrexmature", + "type": "setrexmature", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set REX Maturity Settings\nsummary: 'Sets the options for REX maturity buckets'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{#if num_of_maturity_buckets}}\n Sets the numbers of maturity buckets to '{{num_of_maturity_buckets}}'\n{{/if}}\n\n{{#if sell_matured_rex}}\n Sets whether or not to immediately sell matured REX to '{{sell_matured_rex}}'\n{{/if}}\n\n{{#if buy_rex_to_savings}}\n Sets whether or not to immediately move purchased REX to savings to '{{buy_rex_to_savings}}'\n{{/if}}" + }, + { + "name": "setschedule", + "type": "setschedule", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Set Annual Rate Schedule\nsummary: 'Set annual rate parameters'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} sets a pre-determined inflation schedule to adjust parameters as follows:\n\n* Start time of the schedule: {{start_time}}\n* The continuous rate of inflation: {{continuous_rate}}" + }, + { + "name": "setundlimit", + "type": "setundlimit", + "ricardian_contract": "" + }, + { + "name": "switchtosvnn", + "type": "switchtosvnn", + "ricardian_contract": "" + }, + { + "name": "undelegatebw", + "type": "undelegatebw", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Unstake Tokens for NET and/or CPU\nsummary: 'Unstake tokens for NET and/or CPU from {{nowrap receiver}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/resource.png#3830f1ce8cb07f7757dbcf383b1ec1b11914ac34a1f9d8b065f07600fa9dac19\n---\n\n{{from}} unstakes from {{receiver}} {{unstake_net_quantity}} for NET bandwidth and {{unstake_cpu_quantity}} for CPU bandwidth.\n\nThe sum of these two quantities will be removed from the vote weight of {{receiver}} and will be made available to {{from}} after an uninterrupted 3 day period without further unstaking by {{from}}. After the uninterrupted 3 day period passes, the system will attempt to automatically return the funds to {{from}}’s regular token balance. However, this automatic refund may occasionally fail which will then require {{from}} to manually claim the funds with the refund action." + }, + { + "name": "unlinkauth", + "type": "unlinkauth", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Unlink Action from Permission\nsummary: '{{nowrap account}} unsets the minimum required permission for the {{#if type}}{{nowrap type}} action of the{{/if}} {{nowrap code}} contract'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\n{{account}} removes the association between the {{#if type}}{{type}} action of the{{/if}} {{code}} contract and its minimum required permission.\n\n{{#if type}}{{else}}This will not remove any links explicitly associated to specific actions of {{code}}.{{/if}}" + }, + { + "name": "unregprod", + "type": "unregprod", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Unregister as a Block Producer Candidate\nsummary: '{{nowrap producer}} unregisters as a block producer candidate'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/voting.png#db28cd3db6e62d4509af3644ce7d377329482a14bb4bfaca2aa5f1400d8e8a84\n---\n\n{{producer}} unregisters as a block producer candidate. {{producer}} account will retain its votes and those votes can change based on voter stake changes or votes removed from {{producer}}. However new voters will not be able to vote for {{producer}} while it remains unregistered." + }, + { + "name": "unstaketorex", + "type": "unstaketorex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Buy REX Tokens Using Staked Tokens\nsummary: '{{nowrap owner}} buys REX tokens in exchange for tokens currently staked to NET and/or CPU'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\n{{from_net}} and {{from_cpu}} are withdrawn from {{receiver}}’s NET and CPU bandwidths respectively. These funds are used to purchase REX tokens at the current market exchange rate. In order for the action to succeed, {{owner}} must have voted for a proxy or at least 21 block producers.\n\nA sell order of the purchased amount can only be initiated after waiting for the maturity period of 4 to 5 days to pass. Even then, depending on the market conditions, the initiated sell order may not be executed immediately." + }, + { + "name": "unvest", + "type": "unvest", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Unvest Tokens\nsummary: 'Reclaim and retire unvested tokens'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\nReclaim and retire {{$action.unvest_net_quantity}} and {{$action.unvest_cpu_quantity}} worth of unvested tokens from the account {{$action.account}}." + }, + { + "name": "updateauth", + "type": "updateauth", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Modify Account Permission\nsummary: 'Add or update the {{nowrap permission}} permission of {{nowrap account}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/account.png#3d55a2fc3a5c20b456f5657faf666bc25ffd06f4836c5e8256f741149b0b294f\n---\n\nModify, and create if necessary, the {{permission}} permission of {{account}} to have a parent permission of {{parent}} and the following authority:\n{{to_json auth}}" + }, + { + "name": "updaterex", + "type": "updaterex", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Update REX Owner Vote Weight\nsummary: 'Update vote weight to current value of held REX tokens'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nUpdate vote weight of {{owner}} account to current value of held REX tokens." + }, + { + "name": "updtrevision", + "type": "updtrevision", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Update System Contract Revision Number\nsummary: 'Update system contract revision number'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/admin.png#9bf1cec664863bd6aaac0f814b235f8799fb02c850e9aa5da34e8a004bd6518e\n---\n\n{{$action.account}} advances the system contract revision number to {{revision}}." + }, + { + "name": "voteproducer", + "type": "voteproducer", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Vote for Block Producers\nsummary: '{{nowrap voter}} votes for {{#if proxy}}the proxy {{nowrap proxy}}{{else}}up to 30 block producer candidates{{/if}}'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/voting.png#db28cd3db6e62d4509af3644ce7d377329482a14bb4bfaca2aa5f1400d8e8a84\n---\n\n{{#if proxy}}\n{{voter}} votes for the proxy {{proxy}}.\nAt the time of voting the full weight of voter’s staked (CPU + NET) tokens will be cast towards each of the producers voted by {{proxy}}.\n{{else}}\n{{voter}} votes for the following block producer candidates:\n\n{{#each producers}}\n + {{this}}\n{{/each}}\n\nAt the time of voting the full weight of voter’s staked (CPU + NET) tokens will be cast towards each of the above producers.\n{{/if}}" + }, + { + "name": "voteupdate", + "type": "voteupdate", + "ricardian_contract": "" + }, + { + "name": "withdraw", + "type": "withdraw", + "ricardian_contract": "---\nspec_version: \"0.2.0\"\ntitle: Withdraw from REX Fund\nsummary: 'Withdraw {{nowrap amount}} from {{nowrap owner}}’s REX fund by transferring to {{owner}}’s liquid balance'\nicon: https://raw.githubusercontent.com/AntelopeIO/reference-contracts/main/contracts/icons/rex.png#d229837fa62a464b9c71e06060aa86179adf0b3f4e3b8c4f9702f4f4b0c340a8\n---\n\nWithdraws {{amount}} from {{owner}}’s REX fund and transfer them to {{owner}}’s liquid balance." + } + ], + "tables": [ + { + "name": "abihash", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "abi_hash" + }, + { + "name": "bidrefunds", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "bid_refund" + }, + { + "name": "blockinfo", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "block_info_record" + }, + { + "name": "cpuloan", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_loan" + }, + { + "name": "delband", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "delegated_bandwidth" + }, + { + "name": "finalizers", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "finalizer_info" + }, + { + "name": "finkeyidgen", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "fin_key_id_generator_info" + }, + { + "name": "finkeys", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "finalizer_key_info" + }, + { + "name": "global", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "eosio_global_state" + }, + { + "name": "global2", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "eosio_global_state2" + }, + { + "name": "global3", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "eosio_global_state3" + }, + { + "name": "global4", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "eosio_global_state4" + }, + { + "name": "instantund", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "instant_unstake_info" + }, + { + "name": "lastpropfins", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "last_prop_finalizers_info" + }, + { + "name": "namebids", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "name_bid" + }, + { + "name": "netloan", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_loan" + }, + { + "name": "powup.order", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "powerup_order" + }, + { + "name": "powup.state", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "powerup_state" + }, + { + "name": "producers", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "producer_info" + }, + { + "name": "producers2", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "producer_info2" + }, + { + "name": "rammarket", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "exchange_state" + }, + { + "name": "refunds", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "refund_request" + }, + { + "name": "retbuckets", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_return_buckets" + }, + { + "name": "rexbal", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_balance" + }, + { + "name": "rexfund", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_fund" + }, + { + "name": "rexmaturity", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_maturity" + }, + { + "name": "rexpool", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_pool" + }, + { + "name": "rexqueue", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_order" + }, + { + "name": "rexretpool", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "rex_return_pool" + }, + { + "name": "schedules", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "schedules_info" + }, + { + "name": "userres", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "user_resources" + }, + { + "name": "vexlimits", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "vex_limits_state" + }, + { + "name": "voters", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "voter_info" + }, + { + "name": "limitauthchg", + "index_type": "i64", + "key_names": [], + "key_types": [], + "type": "limit_auth_change" + } + ], + "ricardian_clauses": [ + { + "id": "UserAgreement", + "body": "User agreement for the chain can go here." + }, + { + "id": "BlockProducerAgreement", + "body": "I, {{producer}}, hereby nominate myself for consideration as an elected block producer.\n\nAdditional conditions for block producer agreement can go here." + } + ], + "error_messages": [], + "abi_extensions": [], + "variants": [ + { + "name": "variant_block_signing_authority_v0", + "types": [ + "block_signing_authority_v0" + ] + } + ], + "action_results": [ + { + "name": "buyram", + "result_type": "action_return_buyram" + }, + { + "name": "buyrambytes", + "result_type": "action_return_buyram" + }, + { + "name": "buyramself", + "result_type": "action_return_buyram" + }, + { + "name": "ramburn", + "result_type": "action_return_ramtransfer" + }, + { + "name": "ramtransfer", + "result_type": "action_return_ramtransfer" + }, + { + "name": "sellram", + "result_type": "action_return_sellram" + } + ] +} From 4e873155abd8e0158317e99e9b5668f9f33f23fc Mon Sep 17 00:00:00 2001 From: Windcrypto Date: Mon, 7 Sep 2026 04:48:58 +0200 Subject: [PATCH 47/49] fix(antelope): harden transaction signing and sessions --- packages/antelope/README.md | 4 + packages/antelope/package.json | 41 ++++- packages/antelope/src/index.ts | 26 +-- packages/antelope/src/networks.ts | 21 --- packages/antelope/tsconfig.json | 14 +- packages/session/README.md | 4 + packages/session/package.json | 48 ++++-- packages/session/src/client.ts | 204 ------------------------ packages/session/src/compat.ts | 256 ------------------------------ packages/session/src/index.ts | 1 - packages/session/src/native.ts | 167 ++++++++++++------- packages/session/src/scopes.ts | 19 --- packages/session/src/types.ts | 23 --- packages/session/tsconfig.json | 15 +- scripts/test-antelope-client.mjs | 170 ++++++++++++++++++++ scripts/test-native-antelope.mjs | 27 +--- scripts/test-session.mjs | 183 +++++++++++++++++++++ 17 files changed, 581 insertions(+), 642 deletions(-) delete mode 100644 packages/antelope/src/networks.ts delete mode 100644 packages/session/src/client.ts delete mode 100644 packages/session/src/compat.ts delete mode 100644 packages/session/src/scopes.ts delete mode 100644 packages/session/src/types.ts create mode 100644 scripts/test-antelope-client.mjs create mode 100644 scripts/test-session.mjs diff --git a/packages/antelope/README.md b/packages/antelope/README.md index acabff8..418d7a2 100644 --- a/packages/antelope/README.md +++ b/packages/antelope/README.md @@ -71,6 +71,10 @@ const signer: Signer = { The built-in K1 signer uses the compatibility public-key representation expected by older Vexanium node software when resolving required keys. Applications can request current K1 public-key strings with `k1PublicKeyFormat: "modern"`. +## Security + +The configured chain ID is checked before signing. Signer output is parsed, counted, recovered, and matched to the keys requested by the node before broadcast. Use `broadcast: false` when an application needs signed bytes without submission. + ## Runtime The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. It uses Web-standard byte and networking APIs and does not require Node.js `Buffer` for transaction construction or signing. diff --git a/packages/antelope/package.json b/packages/antelope/package.json index 2e62628..0b0c9fa 100644 --- a/packages/antelope/package.json +++ b/packages/antelope/package.json @@ -6,11 +6,25 @@ "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { - ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, - "./vexanium": { "types": "./dist/vexanium.d.ts", "import": "./dist/vexanium.js" } + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./vexanium": { + "types": "./dist/vexanium.d.ts", + "import": "./dist/vexanium.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, "dependencies": { "@windstack/account": "1.0.0", "@windstack/abi": "1.0.0", @@ -21,7 +35,20 @@ "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/antelope" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/antelope" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/antelope#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/antelope/src/index.ts b/packages/antelope/src/index.ts index 1188e05..9f1aa2b 100644 --- a/packages/antelope/src/index.ts +++ b/packages/antelope/src/index.ts @@ -43,11 +43,11 @@ export type SignRequest = { serializedTransaction: Uint8Array; serializedContextFreeData: Uint8Array; digest: Uint8Array; - requiredKeys: string[]; + requiredKeys: readonly string[]; }; export interface Signer { - getAvailableKeys(): Promise; - sign(request: SignRequest): Promise>; + getAvailableKeys(): Promise; + sign(request: SignRequest): Promise; } export type TransactArgs = { actions: Action[]; @@ -156,7 +156,7 @@ export function serializeContextFreeData(items: Uint8Array[]): Uint8Array { export function transactionDigest( chainId: string, serializedTransaction: Uint8Array, - contextFreeDataHash = new Uint8Array(32), + contextFreeDataHash: Uint8Array = new Uint8Array(32), ): Uint8Array { if (!/^[0-9a-f]{64}$/i.test(chainId)) { throw new TypeError("Antelope chain id must be exactly 64 hexadecimal characters"); @@ -182,6 +182,10 @@ export class PrivateKeySigner implements Signer { constructor(keys: PrivateKey[], options: PrivateKeySignerOptions = {}) { if (!keys.length) throw new TypeError("At least one private key is required"); + const publicKeys = keys.map((key) => key.toPublicKey().toString()); + if (new Set(publicKeys).size !== publicKeys.length) { + throw new TypeError("PrivateKeySigner cannot contain duplicate keys"); + } this.#keys = [...keys]; this.#k1PublicKeyFormat = options.k1PublicKeyFormat ?? "legacy"; } @@ -196,9 +200,7 @@ export class PrivateKeySigner implements Signer { } async sign(request: SignRequest): Promise { - const keyMap = new Map( - this.#keys.map((key) => [key.toPublicKey().toString(), key] as const), - ); + const keyMap = new Map(this.#keys.map((key) => [key.toPublicKey().toString(), key] as const)); return request.requiredKeys.map((requiredKey) => { const normalized = PublicKey.fromString(requiredKey).toString(); const key = keyMap.get(normalized); @@ -310,9 +312,13 @@ export class AntelopeClient { digest, requiredKeys, }); - const parsedSignatures = signed.map((value) => - typeof value === "string" ? Signature.fromString(value) : value, - ); + if (!Array.isArray(signed)) throw new TypeError("Signer returned an invalid signature list"); + const parsedSignatures = signed.map((value) => { + if (typeof value === "string") return Signature.fromString(value); + if (!(value instanceof Signature)) + throw new TypeError("Signer returned an invalid signature value"); + return value; + }); if (parsedSignatures.length !== requiredKeys.length) { throw new Error( `Signer returned ${parsedSignatures.length} signatures for ${requiredKeys.length} required keys`, diff --git a/packages/antelope/src/networks.ts b/packages/antelope/src/networks.ts deleted file mode 100644 index 97a8eac..0000000 --- a/packages/antelope/src/networks.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * WindStack Antelope SDK - * Created by Gilang Ramadan - * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI - * SPDX-License-Identifier: MIT - */ - -export const VEXANIUM_MAINNET = Object.freeze({ - name: "Vexanium Mainnet", - chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", - endpoints: Object.freeze(["https://api.windcrypto.com"]), - contracts: Object.freeze({ - system: "vexcore", - token: "vex.token", - }), - nativeToken: Object.freeze({ - symbol: "VEX", - precision: 4, - contract: "vex.token", - }), -}); diff --git a/packages/antelope/tsconfig.json b/packages/antelope/tsconfig.json index 0123358..e75c984 100644 --- a/packages/antelope/tsconfig.json +++ b/packages/antelope/tsconfig.json @@ -1,6 +1,16 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, "include": ["src/**/*.ts"], - "references": [{ "path": "../crypto" }, { "path": "../abi" }, { "path": "../rpc" }, { "path": "../contract" }, { "path": "../account" }] + "references": [ + { "path": "../crypto" }, + { "path": "../abi" }, + { "path": "../rpc" }, + { "path": "../contract" }, + { "path": "../account" } + ] } diff --git a/packages/session/README.md b/packages/session/README.md index 0c218e8..a508811 100644 --- a/packages/session/README.md +++ b/packages/session/README.md @@ -45,6 +45,10 @@ A wallet plugin implements `login()` and returns a validated identity plus an An Stored session data contains identity and routing information, not private keys. Invalid or malformed stored data is ignored instead of being treated as a valid session. +## Security + +Chain IDs, identities, plugin IDs, and signer interfaces are validated before a session becomes active. If persistence fails after wallet login, the plugin is logged out as a rollback. Logout clears local state even when wallet-side logout fails. + ## Runtime The package is ESM-first and requires Node.js 20.19 or newer when used directly in Node.js. Browser and React Native applications can provide storage implementations appropriate for their security model. diff --git a/packages/session/package.json b/packages/session/package.json index 029b6bd..bc94e96 100644 --- a/packages/session/package.json +++ b/packages/session/package.json @@ -5,24 +5,42 @@ "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", - "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, - "files": ["dist", "README.md", "LICENSE", "package.json"], - "scripts": { "build": "tsc -p tsconfig.json", "prepack": "npm run build" }, - "dependencies": { "@windstack/antelope": "1.0.0" }, - "peerDependencies": { - "@windstack/evm": ">=0.6.2", - "@windstack/solana": ">=0.6.2", - "@windstack/vexanium": ">=0.6.2" + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "peerDependenciesMeta": { - "@windstack/evm": { "optional": true }, - "@windstack/solana": { "optional": true }, - "@windstack/vexanium": { "optional": true } + "files": [ + "dist", + "README.md", + "LICENSE", + "package.json" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build" + }, + "dependencies": { + "@windstack/antelope": "1.0.0" }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan", - "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/session" }, - "publishConfig": { "access": "public" }, - "engines": { "node": ">=20.19.0" } + "repository": { + "type": "git", + "url": "git+https://github.com/windvex/windstack-sdk.git", + "directory": "packages/session" + }, + "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/session#readme", + "bugs": { + "url": "https://github.com/windvex/windstack-sdk/issues" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=20.19.0" + } } diff --git a/packages/session/src/client.ts b/packages/session/src/client.ts deleted file mode 100644 index f9ed213..0000000 --- a/packages/session/src/client.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { - WISP_ERROR_CODES, - WispProviderError, - resolveDappMetadata, - resolveDappRequestContext, - type WispScope, - type WispSession, -} from "@windstack/core"; -import { createEVMClient, type EVMClient } from "@windstack/evm"; -import { createSolanaClient, type SolanaClient } from "@windstack/solana"; -import { - createVexaniumClient, - sameVexaniumChain, - VEXANIUM_METHODS, - type VexaniumClient, -} from "@windstack/vexanium"; -import { createSessionId, isEVMScope, isSolanaScope, isVexaniumScope } from "./scopes.js"; -import type { WispInvokeArgs, WispSessionClient, WispSessionClientOptions } from "./types.js"; - -function withWalletSessionParams(params: TParams | undefined, walletSessionId?: string): TParams | undefined { - if (!walletSessionId) return params; - if (typeof params === "object" && params !== null && !Array.isArray(params)) { - return { ...params, sessionId: (params as { sessionId?: string }).sessionId ?? walletSessionId } as TParams; - } - if (params === undefined) return { sessionId: walletSessionId } as TParams; - return params; -} - -const WALLET_SESSION_METHODS = new Set([ - VEXANIUM_METHODS.SIGNING_REQUEST, - VEXANIUM_METHODS.SIGN_MESSAGE, - VEXANIUM_METHODS.SIGN_DIGEST, - VEXANIUM_METHODS.SIGN_TRANSACTION, - VEXANIUM_METHODS.DISCONNECT, -]); - -function assertRequestedScopes(scopes: WispScope[]): void { - if (scopes.length === 0) { - throw new WispProviderError(WISP_ERROR_CODES.INVALID_PARAMS, "At least one wallet scope is required"); - } - if (!scopes.every((scope) => isEVMScope(scope) || isSolanaScope(scope) || isVexaniumScope(scope))) { - throw new WispProviderError(WISP_ERROR_CODES.INVALID_PARAMS, "One or more wallet scopes are invalid"); - } - const families = [isEVMScope, isSolanaScope, isVexaniumScope]; - if (families.some((isFamily) => scopes.filter(isFamily).length > 1)) { - throw new WispProviderError( - WISP_ERROR_CODES.INVALID_PARAMS, - "Only one scope per chain family can be connected in a session", - ); - } -} - -function evmScopeFromHexChainId(chainId: string): `eip155:${number}` { - if (!/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(chainId)) { - throw new WispProviderError(WISP_ERROR_CODES.INTERNAL_ERROR, `Provider returned invalid EVM chain ID: ${chainId}`); - } - return `eip155:${BigInt(chainId).toString()}` as `eip155:${number}`; -} - -function cloneWispSession(value: WispSession | null): WispSession | null { - if (!value) return null; - return { - ...value, - dapp: { - ...value.dapp, - icons: value.dapp.icons ? [...value.dapp.icons] : undefined, - }, - scopes: [...value.scopes], - accounts: value.accounts.map((account) => ({ ...account })), - }; -} - -export async function createWispSessionClient(options: WispSessionClientOptions = {}): Promise { - const dapp = resolveDappMetadata(options.dapp); - const requestContext = resolveDappRequestContext(); - let evmClient: EVMClient | undefined = options.evm; - let solanaClient: SolanaClient | undefined = options.solana; - let vexaniumClient: VexaniumClient | undefined = options.vexanium; - let session: WispSession | null = null; - - const getEVMClient = async () => { - evmClient = evmClient ?? await createEVMClient(); - return evmClient; - }; - - const getSolanaClient = async () => { - solanaClient = solanaClient ?? await createSolanaClient(); - return solanaClient; - }; - - const getVexaniumClient = async () => { - vexaniumClient = vexaniumClient ?? await createVexaniumClient({ dapp }); - return vexaniumClient; - }; - - const disconnectClients = async (): Promise => { - await Promise.allSettled([ - evmClient?.request({ method: "wallet_revokePermissions", params: [{ eth_accounts: {} }] }), - solanaClient?.disconnect(), - vexaniumClient?.disconnect(), - ]); - }; - - return { - async connect(scopes: WispScope[]): Promise { - const uniqueScopes = [...new Set(scopes)]; - assertRequestedScopes(uniqueScopes); - if (session) { - throw new WispProviderError(WISP_ERROR_CODES.REQUEST_PENDING, "A Wisp session is already active"); - } - const accounts: WispSession["accounts"] = []; - - try { - const evmScope = uniqueScopes.find(isEVMScope); - if (evmScope) { - const client = await getEVMClient(); - const evmAccounts = await client.connect(); - const activeScope = evmScopeFromHexChainId(await client.getChainId()); - if (activeScope !== evmScope) { - throw new WispProviderError( - WISP_ERROR_CODES.INVALID_PARAMS, - `EVM provider is connected to ${activeScope}, not requested scope ${evmScope}`, - ); - } - for (const address of evmAccounts) accounts.push({ scope: evmScope, address }); - } - - const solanaScope = uniqueScopes.find(isSolanaScope); - if (solanaScope) { - const client = await getSolanaClient(); - const solanaAccounts = await client.connect(); - for (const account of solanaAccounts) { - accounts.push({ scope: solanaScope, address: account.publicKey, label: account.label }); - } - } - - const vexaniumScope = uniqueScopes.find(isVexaniumScope); - if (vexaniumScope) { - const client = await getVexaniumClient(); - const vexaniumAccounts = await client.connect({ chainId: vexaniumScope, dapp }); - const vexaniumSession = client.getSession(); - if (!vexaniumSession || !sameVexaniumChain(vexaniumSession.chainId, vexaniumScope)) { - throw new WispProviderError(WISP_ERROR_CODES.INTERNAL_ERROR, "Vexanium provider returned the wrong chain"); - } - for (const account of vexaniumAccounts) { - accounts.push({ scope: vexaniumScope, address: account.permissionLevel, label: account.label }); - } - } - } catch (error) { - await disconnectClients(); - throw error; - } - - if (accounts.length === 0) { - await disconnectClients(); - throw new WispProviderError(WISP_ERROR_CODES.UNAUTHORIZED, "No wallet accounts were authorized"); - } - - const now = Date.now(); - session = { - id: createSessionId(uniqueScopes), - dapp, - origin: requestContext.origin, - scopes: uniqueScopes, - accounts, - createdAt: now, - updatedAt: now, - }; - - return cloneWispSession(session)!; - }, - - getSession() { - return cloneWispSession(session); - }, - - async invoke(args: WispInvokeArgs): Promise { - if (!session) { - throw new WispProviderError(WISP_ERROR_CODES.INVALID_PARAMS, "No active Wisp session. Call connect() first."); - } - if (!session.scopes.includes(args.scope)) { - throw new WispProviderError(WISP_ERROR_CODES.INVALID_PARAMS, `Scope is not authorized: ${args.scope}`); - } - - if (isEVMScope(args.scope)) return await (await getEVMClient()).request(args.request); - if (isSolanaScope(args.scope)) return await (await getSolanaClient()).request(args.request); - if (isVexaniumScope(args.scope)) { - const client = await getVexaniumClient(); - const walletSessionId = client.getSession()?.walletSessionId; - const request = WALLET_SESSION_METHODS.has(args.request.method) - ? { ...args.request, params: withWalletSessionParams(args.request.params, walletSessionId) } - : args.request; - return await client.request(request as typeof args.request); - } - - throw new WispProviderError(WISP_ERROR_CODES.INVALID_PARAMS, `Unsupported scope: ${args.scope}`); - }, - - async disconnect() { - await disconnectClients(); - session = null; - }, - }; -} diff --git a/packages/session/src/compat.ts b/packages/session/src/compat.ts deleted file mode 100644 index 3c02265..0000000 --- a/packages/session/src/compat.ts +++ /dev/null @@ -1,256 +0,0 @@ -/** - * WindStack Antelope SDK - * Created by Gilang Ramadan - * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI - * SPDX-License-Identifier: MIT - */ - -export type LegacyWispScope = `eip155:${number}` | `antelope:${string}` | `solana:${string}`; -export type LegacyRequestArguments = { method: string; params?: TParams }; -export type LegacySessionAccount = { scope: LegacyWispScope; address: string; label?: string }; -export type LegacyWispSession = { - id: string; - dapp?: unknown; - origin?: string; - scopes: LegacyWispScope[]; - accounts: LegacySessionAccount[]; - createdAt: number; - updatedAt: number; -}; - -type EVMCompatClient = { - connect(): Promise; - getChainId(): Promise; - request( - args: LegacyRequestArguments, - ): Promise; - disconnect?: () => Promise; -}; -type SolanaCompatClient = { - connect(): Promise>; - request( - args: LegacyRequestArguments, - ): Promise; - disconnect(): Promise; -}; -type VexaniumCompatClient = { - connect(args: { - chainId: string; - dapp?: unknown; - }): Promise>; - getSession(): { chainId: string; walletSessionId?: string } | null; - request( - args: LegacyRequestArguments, - ): Promise; - disconnect(): Promise; -}; -export type LegacyWispSessionClientOptions = { - dapp?: unknown; - evm?: EVMCompatClient; - solana?: SolanaCompatClient; - vexanium?: VexaniumCompatClient; -}; -export type LegacyWispInvokeArgs = { - scope: LegacyWispScope; - request: LegacyRequestArguments; -}; - -function providerError(code: number, message: string): Error & { code: number } { - return Object.assign(new Error(message), { code }); -} - -function evmScopeFromHexChainId(chainId: string): `eip155:${number}` { - if (!/^0x(?:0|[1-9a-f][0-9a-f]*)$/i.test(chainId)) { - throw providerError(-32603, `Provider returned invalid EVM chain ID: ${chainId}`); - } - return `eip155:${BigInt(chainId).toString()}` as `eip155:${number}`; -} - -function cloneSession(session: LegacyWispSession | null): LegacyWispSession | null { - return session - ? { - ...session, - scopes: [...session.scopes], - accounts: session.accounts.map((account) => ({ ...account })), - } - : null; -} - -function secureSessionEntropy(): string { - const crypto = globalThis.crypto; - if (crypto?.randomUUID) return crypto.randomUUID(); - if (crypto?.getRandomValues) { - const bytes = new Uint8Array(16); - crypto.getRandomValues(bytes); - return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); - } - throw new Error("Secure platform randomness is required to create a wallet session id"); -} - -export function isEVMScope(scope: string): scope is `eip155:${number}` { - return /^eip155:(?:0|[1-9]\d*)$/.test(scope); -} - -export function isVexaniumScope(scope: string): scope is `antelope:${string}` { - return /^antelope:[0-9a-f]{32}$/.test(scope); -} - -export function isSolanaScope(scope: string): scope is `solana:${string}` { - return /^solana:[a-zA-Z0-9_-]+$/.test(scope); -} - -export function createSessionId(scopes: string[]): string { - return `wisp:${Date.now().toString(36)}:${scopes.join(",")}:${secureSessionEntropy()}`; -} - -export async function createWispSessionClient(options: LegacyWispSessionClientOptions = {}) { - let evm = options.evm; - let solana = options.solana; - let vexanium = options.vexanium; - let session: LegacyWispSession | null = null; - - const getEVM = async (): Promise => { - if (!evm) { - const module = await import("@windstack/evm"); - evm = (await module.createEVMClient()) as EVMCompatClient; - } - return evm; - }; - - const getSolana = async (): Promise => { - if (!solana) { - const module = await import("@windstack/solana"); - solana = (await module.createSolanaClient()) as SolanaCompatClient; - } - return solana; - }; - - const getVexanium = async (): Promise => { - if (!vexanium) { - const module = await import("@windstack/vexanium"); - vexanium = (await module.createVexaniumClient({ - dapp: options.dapp as never, - })) as VexaniumCompatClient; - } - return vexanium; - }; - - const disconnectAll = async (): Promise => { - await Promise.allSettled( - [evm?.disconnect?.(), solana?.disconnect(), vexanium?.disconnect()].filter( - Boolean, - ) as Promise[], - ); - }; - - return { - async connect(scopes: LegacyWispScope[]): Promise { - const unique = [...new Set(scopes)]; - if ( - !unique.length || - !unique.every( - (scope) => isEVMScope(scope) || isSolanaScope(scope) || isVexaniumScope(scope), - ) - ) { - throw providerError(-32602, "One or more wallet scopes are invalid"); - } - if (session) throw providerError(-32002, "A Wisp session is already active"); - - const accounts: LegacySessionAccount[] = []; - try { - const evmScope = unique.find(isEVMScope); - if (evmScope) { - const client = await getEVM(); - const connected = await client.connect(); - const activeScope = evmScopeFromHexChainId(await client.getChainId()); - if (activeScope !== evmScope) { - throw providerError( - -32602, - `EVM provider is connected to ${activeScope}, not requested scope ${evmScope}`, - ); - } - for (const address of connected) accounts.push({ scope: evmScope, address }); - } - - const solanaScope = unique.find(isSolanaScope); - if (solanaScope) { - for (const account of await (await getSolana()).connect()) { - accounts.push({ - scope: solanaScope, - address: account.publicKey, - label: account.label, - }); - } - } - - const vexScope = unique.find(isVexaniumScope); - if (vexScope) { - const client = await getVexanium(); - const connected = await client.connect({ chainId: vexScope, dapp: options.dapp }); - const active = client.getSession()?.chainId; - if ( - !active || - !(active === vexScope || active.endsWith(vexScope.slice("antelope:".length))) - ) { - throw providerError(-32603, "Vexanium provider returned the wrong chain"); - } - for (const account of connected) { - accounts.push({ - scope: vexScope, - address: account.permissionLevel, - label: account.label, - }); - } - } - } catch (error) { - await disconnectAll(); - throw error; - } - - if (!accounts.length) { - await disconnectAll(); - throw providerError(4100, "No wallet accounts were authorized"); - } - - const now = Date.now(); - session = { - id: createSessionId(unique), - dapp: options.dapp, - origin: - typeof globalThis.location?.origin === "string" ? globalThis.location.origin : undefined, - scopes: unique, - accounts, - createdAt: now, - updatedAt: now, - }; - return cloneSession(session)!; - }, - - getSession(): LegacyWispSession | null { - return cloneSession(session); - }, - - async invoke( - args: LegacyWispInvokeArgs, - ): Promise { - if (!session || !session.scopes.includes(args.scope)) { - throw providerError(-32602, "No active session for requested scope"); - } - if (isEVMScope(args.scope)) { - return (await getEVM()).request(args.request); - } - if (isSolanaScope(args.scope)) { - return (await getSolana()).request(args.request); - } - if (isVexaniumScope(args.scope)) { - return (await getVexanium()).request(args.request); - } - throw providerError(-32602, `Unsupported scope: ${args.scope}`); - }, - - async disconnect(): Promise { - await disconnectAll(); - session = null; - }, - }; -} diff --git a/packages/session/src/index.ts b/packages/session/src/index.ts index 0e5a8bd..d933ee9 100644 --- a/packages/session/src/index.ts +++ b/packages/session/src/index.ts @@ -5,4 +5,3 @@ * SPDX-License-Identifier: MIT */ export * from "./native.js"; -export * from "./compat.js"; diff --git a/packages/session/src/native.ts b/packages/session/src/native.ts index a537dae..29f7f5c 100644 --- a/packages/session/src/native.ts +++ b/packages/session/src/native.ts @@ -6,6 +6,7 @@ */ import { AntelopeClient, + PublicKey, nameToBigInt, type Action, type ChainContracts, @@ -15,8 +16,8 @@ import { export type SessionChain = { id: string; - url: string | string[]; - contracts?: ChainContracts; + url: string | readonly string[]; + contracts?: Readonly; }; export type SessionIdentity = { actor: string; permission: string; publicKey?: string }; export type WalletLoginContext = { chain: SessionChain; appName?: string }; @@ -64,6 +65,7 @@ function validateIdentity(identity: SessionIdentity): SessionIdentity { if (identity.publicKey !== undefined && typeof identity.publicKey !== "string") { throw new TypeError("Wallet identity publicKey must be a string"); } + if (identity.publicKey !== undefined) PublicKey.fromString(identity.publicKey); return Object.freeze({ ...identity, actor, permission }); } @@ -79,13 +81,23 @@ function validateSigner(signer: Signer): Signer { return signer; } +function identitiesMatch(expected: SessionIdentity, actual: SessionIdentity): boolean { + if (expected.actor !== actual.actor || expected.permission !== actual.permission) return false; + if (expected.publicKey === undefined) return true; + if (actual.publicKey === undefined) return false; + return PublicKey.fromString(expected.publicKey).equals(PublicKey.fromString(actual.publicKey)); +} + function validateChain(chain: SessionChain): SessionChain { if (!/^[0-9a-f]{64}$/i.test(chain.id)) { throw new TypeError("Session chain id must be a 64-character Antelope chain id"); } const urls = Array.isArray(chain.url) ? chain.url : [chain.url]; - if (!urls.length || urls.some((url) => typeof url !== "string" || !url.trim())) { - throw new TypeError("Session chain requires at least one RPC URL"); + if ( + !urls.length || + urls.some((url) => typeof url !== "string" || !/^https?:\/\/[^\s]+$/i.test(url.trim())) + ) { + throw new TypeError("Session chain requires at least one HTTP(S) RPC URL"); } const contracts = chain.contracts ? Object.freeze({ @@ -194,10 +206,13 @@ export class SessionKit { readonly storage: SessionStorage; readonly storageKey: string; #session: Session | null = null; + #loginPending = false; + #restorePending = false; constructor(options: SessionKitOptions) { if (!options.chains.length) throw new TypeError("SessionKit requires at least one chain"); - if (!options.walletPlugins.length) throw new TypeError("SessionKit requires at least one wallet plugin"); + if (!options.walletPlugins.length) + throw new TypeError("SessionKit requires at least one wallet plugin"); const chains = options.chains.map(validateChain); const plugins = options.walletPlugins.map(validatePlugin); const chainIds = new Set(chains.map((chain) => chain.id)); @@ -221,7 +236,7 @@ export class SessionKit { } async login(options: { chainId?: string; walletPluginId?: string } = {}): Promise { - if (this.#session) { + if (this.#session || this.#loginPending || this.#restorePending) { throw new Error("A wallet session is already active; logout before starting another session"); } const requestedChainId = options.chainId?.toLowerCase(); @@ -234,66 +249,99 @@ export class SessionKit { if (!chain) throw new TypeError(`Unknown chain: ${options.chainId}`); if (!plugin) throw new TypeError(`Unknown wallet plugin: ${options.walletPluginId}`); - const result = await plugin.login({ chain, appName: this.appName }); - const session = new Session({ - chain, - identity: validateIdentity(result.identity), - walletPlugin: plugin, - signer: validateSigner(result.signer), - }); + this.#loginPending = true; try { - await this.storage.set( - this.storageKey, - JSON.stringify({ - chainId: chain.id, - walletPluginId: plugin.id, - identity: session.identity, - }), - ); - } catch (error) { - if (plugin.logout) { - await plugin - .logout({ chain, appName: this.appName, identity: session.identity }) - .catch(() => undefined); + const result = await plugin.login({ chain, appName: this.appName }); + const identity = validateIdentity(result.identity); + const session = new Session({ + chain, + identity, + walletPlugin: plugin, + signer: validateSigner(result.signer), + }); + try { + await this.storage.set( + this.storageKey, + JSON.stringify({ + chainId: chain.id, + walletPluginId: plugin.id, + identity: session.identity, + }), + ); + } catch (error) { + if (plugin.logout) { + await plugin + .logout({ chain, appName: this.appName, identity: session.identity }) + .catch(() => undefined); + } + throw error; } - throw error; + this.#session = session; + return session; + } finally { + this.#loginPending = false; } - this.#session = session; - return session; } async restore(): Promise { if (this.#session) return this.#session; - const stored = await this.getStoredSession(); - if (!stored) return null; - const chain = this.chains.find((item) => item.id === stored.chainId.toLowerCase()); - const plugin = this.walletPlugins.find((item) => item.id === stored.walletPluginId); - if (!chain || !plugin?.restore) return null; - const result = await plugin.restore({ - chain, - appName: this.appName, - identity: stored.identity, - }); - if (!result) return null; - const session = new Session({ - chain, - identity: validateIdentity(result.identity), - walletPlugin: plugin, - signer: validateSigner(result.signer), - }); - await this.storage.set( - this.storageKey, - JSON.stringify({ - chainId: chain.id, - walletPluginId: plugin.id, - identity: session.identity, - }), - ); - this.#session = session; - return session; + if (this.#loginPending || this.#restorePending) { + throw new Error("A wallet session operation is already in progress"); + } + this.#restorePending = true; + try { + const stored = await this.getStoredSession(); + if (!stored) return null; + const chain = this.chains.find((item) => item.id === stored.chainId.toLowerCase()); + const plugin = this.walletPlugins.find((item) => item.id === stored.walletPluginId); + if (!chain || !plugin?.restore) { + await this.storage.remove(this.storageKey); + return null; + } + const result = await plugin.restore({ + chain, + appName: this.appName, + identity: stored.identity, + }); + if (!result) return null; + const restoredIdentity = validateIdentity(result.identity); + if (!identitiesMatch(stored.identity, restoredIdentity)) { + throw new Error("Wallet restored an identity that does not match the stored session"); + } + const session = new Session({ + chain, + identity: restoredIdentity, + walletPlugin: plugin, + signer: validateSigner(result.signer), + }); + try { + await this.storage.set( + this.storageKey, + JSON.stringify({ + chainId: chain.id, + walletPluginId: plugin.id, + identity: session.identity, + }), + ); + } catch (error) { + if (plugin.logout) { + await plugin + .logout({ chain, appName: this.appName, identity: session.identity }) + .catch(() => undefined); + } + throw error; + } + this.#session = session; + return session; + } finally { + this.#restorePending = false; + } } async logout(): Promise { + if (this.#loginPending || this.#restorePending) { + throw new Error("Cannot logout while a wallet session operation is in progress"); + } const session = this.#session; let logoutError: unknown; try { @@ -317,7 +365,10 @@ export class SessionKit { } if (logoutError && storageError) { - throw new AggregateError([logoutError, storageError], "Wallet logout and session cleanup failed"); + throw new AggregateError( + [logoutError, storageError], + "Wallet logout and session cleanup failed", + ); } if (logoutError) throw logoutError; if (storageError) throw storageError; @@ -336,6 +387,7 @@ export class SessionKit { typeof value.identity !== "object" || !value.identity ) { + await this.storage.remove(this.storageKey).catch(() => undefined); return null; } return { @@ -344,6 +396,7 @@ export class SessionKit { identity: validateIdentity(value.identity), }; } catch { + await this.storage.remove(this.storageKey).catch(() => undefined); return null; } } diff --git a/packages/session/src/scopes.ts b/packages/session/src/scopes.ts deleted file mode 100644 index b862079..0000000 --- a/packages/session/src/scopes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { WispScope } from "@windstack/core"; - -export function isEVMScope(scope: WispScope): scope is `eip155:${number}` { - return /^eip155:(?:0|[1-9]\d*)$/.test(scope); -} - -export function isVexaniumScope(scope: WispScope): scope is `antelope:${string}` { - return /^antelope:[0-9a-f]{32}$/.test(scope); -} - -export function isSolanaScope(scope: WispScope): scope is Extract { - return /^solana:[a-zA-Z0-9_-]+$/.test(scope); -} - -export function createSessionId(scopes: WispScope[]): string { - const cryptoSource = globalThis.crypto; - const randomPart = cryptoSource?.randomUUID?.() ?? Math.random().toString(36).slice(2); - return `wisp:${Date.now().toString(36)}:${scopes.join(",")}:${randomPart}`; -} diff --git a/packages/session/src/types.ts b/packages/session/src/types.ts deleted file mode 100644 index e61be0e..0000000 --- a/packages/session/src/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { DappMetadataInput, RequestArguments, WispScope, WispSession } from "@windstack/core"; -import type { EVMClient } from "@windstack/evm"; -import type { SolanaClient } from "@windstack/solana"; -import type { VexaniumClient } from "@windstack/vexanium"; - -export type WispSessionClientOptions = { - dapp?: DappMetadataInput; - evm?: EVMClient; - solana?: SolanaClient; - vexanium?: VexaniumClient; -}; - -export type WispInvokeArgs = { - scope: WispScope; - request: RequestArguments; -}; - -export type WispSessionClient = { - connect(scopes: WispScope[]): Promise; - getSession(): WispSession | null; - invoke(args: WispInvokeArgs): Promise; - disconnect(): Promise; -}; diff --git a/packages/session/tsconfig.json b/packages/session/tsconfig.json index f7dc5a6..408de5f 100644 --- a/packages/session/tsconfig.json +++ b/packages/session/tsconfig.json @@ -1,11 +1,10 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, - "include": ["src/index.ts", "src/native.ts", "src/compat.ts"], - "references": [ - { "path": "../antelope" }, - { "path": "../evm" }, - { "path": "../solana" }, - { "path": "../vexanium" } - ] + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "./tsconfig.tsbuildinfo" + }, + "include": ["src/index.ts", "src/native.ts"], + "references": [{ "path": "../antelope" }] } diff --git a/scripts/test-antelope-client.mjs b/scripts/test-antelope-client.mjs new file mode 100644 index 0000000..9461c29 --- /dev/null +++ b/scripts/test-antelope-client.mjs @@ -0,0 +1,170 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + AntelopeClient, + PrivateKeySigner, + serializeContextFreeData, + serializeTransaction, + transactionDigest, +} from "../packages/antelope/dist/index.js"; +import { + concatBytes, + hexToBytes, + PrivateKey, + sha256Digest, +} from "../packages/crypto/dist/index.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const tokenAbi = JSON.parse( + await readFile(path.join(root, "test/fixtures/vexanium/vex.token.abi.json"), "utf8"), +); +const chainId = "11".repeat(32); +const blockId = `00000063${"00".repeat(4)}15cd5b07${"00".repeat(20)}`; +const privateKey = PrivateKey.fromBytes( + "K1", + Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)), +); +const otherKey = PrivateKey.fromBytes( + "K1", + Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 2 : 0)), +); +const publicKey = privateKey.toPublicKey().toLegacyString(); +let pushes = 0; +let signerCalls = 0; +let lastSignRequest; + +const fetch = async (input, init) => { + const url = String(input); + const body = JSON.parse(String(init?.body ?? "{}")); + if (url.endsWith("/get_info")) { + return Response.json({ + chain_id: chainId, + head_block_num: 100, + last_irreversible_block_num: 99, + head_block_id: blockId, + head_block_time: "2026-09-07T00:00:00.000", + }); + } + if (url.endsWith("/get_block")) { + assert.equal(body.block_num_or_id, 99); + return Response.json({ id: blockId, block_num: 99, timestamp: "2026-09-06T23:59:00.000" }); + } + if (url.endsWith("/get_abi")) return Response.json({ account_name: "vex.token", abi: tokenAbi }); + if (url.endsWith("/get_required_keys")) { + assert.equal(body.transaction.ref_block_num, 99); + return Response.json({ required_keys: [publicKey] }); + } + if (url.endsWith("/push_transaction")) { + pushes += 1; + return Response.json({ transaction_id: "ab".repeat(32) }); + } + return Response.json({ message: "not found" }, { status: 404 }); +}; + +const client = new AntelopeClient({ + endpoints: "https://unit.test", + fetch, + chainId, + contracts: { system: "vexcore", token: "vex.token" }, +}); +const action = await client.account("alice").transfer("bob", "1.0000 VEX", "WindStack"); +const signer = { + async getAvailableKeys() { + return [publicKey]; + }, + async sign(request) { + signerCalls += 1; + lastSignRequest = request; + return [privateKey.signDigest(request.digest)]; + }, +}; +const contextFreeData = [Uint8Array.of(1, 2, 3), new Uint8Array(0)]; +const result = await client.transact({ + actions: [action], + signer, + contextFreeData, + transactionExtensions: [[7, "aabb"]], + broadcast: false, +}); +assert.equal(pushes, 0); +assert.equal(signerCalls, 1); +assert.equal(result.transaction.expiration, "2026-09-07T00:02:00"); +assert.equal(result.transaction.ref_block_num, 99); +assert.equal(result.transaction.ref_block_prefix, 123456789); +assert.deepEqual(result.serializedContextFreeData, serializeContextFreeData(contextFreeData)); +assert.deepEqual(result.serializedTransaction, serializeTransaction(result.transaction)); +const expectedDigest = sha256Digest( + concatBytes( + hexToBytes(chainId), + result.serializedTransaction, + sha256Digest(result.serializedContextFreeData), + ), +); +assert.deepEqual(lastSignRequest.digest, expectedDigest); +assert.deepEqual( + transactionDigest( + chainId, + result.serializedTransaction, + sha256Digest(result.serializedContextFreeData), + ), + expectedDigest, +); + +const broadcast = await client.transact({ + actions: [action], + signer: new PrivateKeySigner([privateKey]), +}); +assert.equal(broadcast.response.transaction_id, "ab".repeat(32)); +assert.equal(pushes, 1); +assert.throws(() => new PrivateKeySigner([privateKey, privateKey]), /duplicate keys/); + +const mismatch = new AntelopeClient({ + endpoints: "https://unit.test", + fetch, + chainId: "22".repeat(32), +}); +await assert.rejects(() => mismatch.transact({ actions: [action], signer }), /RPC chain mismatch/); +assert.equal(signerCalls, 1, "chain mismatch must stop before signing"); + +const badSigner = { + async getAvailableKeys() { + return [publicKey]; + }, + async sign(request) { + return [otherKey.signDigest(request.digest)]; + }, +}; +await assert.rejects( + () => client.transact({ actions: [action], signer: badSigner, broadcast: false }), + /does not match the required keys/, +); +await assert.rejects( + () => + client.transact({ + actions: [action], + signer: { getAvailableKeys: async () => [publicKey], sign: async () => [] }, + broadcast: false, + }), + /0 signatures for 1 required keys/, +); +await assert.rejects( + () => + client.transact({ + actions: [action], + signer: { getAvailableKeys: async () => [publicKey], sign: async () => [{}] }, + broadcast: false, + }), + /invalid signature value/, +); +assert.throws(() => serializeContextFreeData(["not bytes"]), /Uint8Array/); +assert.throws(() => transactionDigest("bad", new Uint8Array()), /chain id/); + +console.log("Antelope transaction and signer tests passed"); diff --git a/scripts/test-native-antelope.mjs b/scripts/test-native-antelope.mjs index 26fd1d6..5cbc5d1 100644 --- a/scripts/test-native-antelope.mjs +++ b/scripts/test-native-antelope.mjs @@ -1,23 +1,9 @@ import assert from "node:assert/strict"; -import { - AbiSerializer, - bigIntToName, - nameToBigInt, -} from "../packages/abi/dist/index.js"; -import { - AntelopeClient, - PrivateKeySigner, -} from "../packages/antelope/dist/index.js"; -import { - PrivateKey, - PublicKey, - sha256Digest, -} from "../packages/crypto/dist/index.js"; +import { AbiSerializer, bigIntToName, nameToBigInt } from "../packages/abi/dist/index.js"; +import { AntelopeClient, PrivateKeySigner } from "../packages/antelope/dist/index.js"; +import { PrivateKey, PublicKey, sha256Digest } from "../packages/crypto/dist/index.js"; import { RpcClient } from "../packages/rpc/dist/index.js"; -import { - MemorySessionStorage, - SessionKit, -} from "../packages/session/dist/index.js"; +import { MemorySessionStorage, SessionKit } from "../packages/session/dist/index.js"; const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); const privateKey = PrivateKey.fromBytes("K1", scalarOne); @@ -202,7 +188,10 @@ await storage.set("windstack:session", "{broken-json"); const walletPlugin = { id: "test-wallet", async login() { - return { identity: { actor: "alice", permission: "active" }, signer: new PrivateKeySigner([privateKey]) }; + return { + identity: { actor: "alice", permission: "active" }, + signer: new PrivateKeySigner([privateKey]), + }; }, }; const kit = new SessionKit({ diff --git a/scripts/test-session.mjs b/scripts/test-session.mjs new file mode 100644 index 0000000..c4b5261 --- /dev/null +++ b/scripts/test-session.mjs @@ -0,0 +1,183 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import assert from "node:assert/strict"; +import { PrivateKeySigner } from "../packages/antelope/dist/index.js"; +import { PrivateKey } from "../packages/crypto/dist/index.js"; +import { MemorySessionStorage, SessionKit } from "../packages/session/dist/index.js"; + +const chainId = "11".repeat(32); +const chain = { + id: chainId, + url: ["https://one.test", "https://two.test"], + contracts: { system: "vexcore", token: "vex.token" }, +}; +const privateKey = PrivateKey.fromBytes( + "K1", + Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)), +); +const signer = new PrivateKeySigner([privateKey]); +const identity = { + actor: "alice", + permission: "active", + publicKey: privateKey.toPublicKey().toString(), +}; + +const storage = new MemorySessionStorage(); +let logoutCalls = 0; +const plugin = { + id: "test-wallet", + async login() { + return { identity, signer }; + }, + async restore() { + return { identity, signer }; + }, + async logout() { + logoutCalls += 1; + }, +}; +const kit = new SessionKit({ chains: [chain], walletPlugins: [plugin], storage }); +const session = await kit.login(); +assert.equal(session.actor, "alice"); +assert.equal(session.permission, "active"); +assert.equal(Object.isFrozen(session.chain), true); +assert.equal(Object.isFrozen(session.chain.url), true); +assert.equal(Object.isFrozen(session.chain.contracts), true); +const stored = await storage.get("windstack:session"); +assert.ok(stored); +assert.equal( + stored.includes(privateKey.toString()), + false, + "stored sessions must never contain private keys", +); +await assert.rejects(() => kit.login(), /already active/); +await kit.logout(); +assert.equal(kit.getSession(), null); +assert.equal(await storage.get("windstack:session"), null); +assert.equal(logoutCalls, 1); + +await storage.set( + "windstack:session", + JSON.stringify({ chainId, walletPluginId: plugin.id, identity }), +); +assert.equal((await kit.restore())?.actor, "alice"); +await kit.logout(); + +await storage.set( + "windstack:session", + JSON.stringify({ chainId, walletPluginId: "missing-wallet", identity }), +); +assert.equal(await kit.restore(), null); +assert.equal(await storage.get("windstack:session"), null); + +await storage.set( + "windstack:session", + JSON.stringify({ chainId, walletPluginId: plugin.id, identity }), +); +const mismatchedRestoreKit = new SessionKit({ + chains: [chain], + walletPlugins: [ + { + ...plugin, + async restore() { + return { identity: { ...identity, actor: "bob" }, signer }; + }, + }, + ], + storage, +}); +await assert.rejects(() => mismatchedRestoreKit.restore(), /does not match/); +assert.equal(mismatchedRestoreKit.getSession(), null); +await storage.remove("windstack:session"); + +await storage.set("windstack:session", "{malformed"); +assert.equal(await kit.getStoredSession(), null); +assert.equal(await storage.get("windstack:session"), null); + +let rollbackCalls = 0; +const failingStorage = { + async get() { + return null; + }, + async set() { + throw new Error("storage full"); + }, + async remove() {}, +}; +const rollbackPlugin = { + ...plugin, + id: "rollback-wallet", + async logout() { + rollbackCalls += 1; + }, +}; +const rollbackKit = new SessionKit({ + chains: [chain], + walletPlugins: [rollbackPlugin], + storage: failingStorage, +}); +await assert.rejects(() => rollbackKit.login(), /storage full/); +assert.equal(rollbackCalls, 1); +assert.equal(rollbackKit.getSession(), null); + +const cleanupStorage = new MemorySessionStorage(); +const logoutFailureKit = new SessionKit({ + chains: [chain], + walletPlugins: [ + { + ...plugin, + id: "logout-failure", + async logout() { + throw new Error("wallet offline"); + }, + }, + ], + storage: cleanupStorage, +}); +await logoutFailureKit.login(); +await assert.rejects(() => logoutFailureKit.logout(), /wallet offline/); +assert.equal(logoutFailureKit.getSession(), null); +assert.equal(await cleanupStorage.get("windstack:session"), null); + +let releaseLogin; +const pendingPlugin = { + id: "pending-wallet", + login: async () => + new Promise((resolve) => { + releaseLogin = () => resolve({ identity, signer }); + }), +}; +const pendingKit = new SessionKit({ chains: [chain], walletPlugins: [pendingPlugin] }); +const pendingLogin = pendingKit.login(); +await assert.rejects(() => pendingKit.login(), /already active/); +releaseLogin(); +await pendingLogin; + +assert.throws( + () => new SessionKit({ chains: [chain, chain], walletPlugins: [plugin] }), + /chain ids must be unique/, +); +assert.throws( + () => new SessionKit({ chains: [chain], walletPlugins: [plugin, plugin] }), + /plugin ids must be unique/, +); +const invalidIdentityKit = new SessionKit({ + chains: [chain], + walletPlugins: [ + { + id: "invalid-identity", + login: async () => ({ identity: { actor: "bad-name", permission: "active" }, signer }), + }, + ], +}); +await assert.rejects(() => invalidIdentityKit.login(), /Invalid Antelope name character/); +const invalidSignerKit = new SessionKit({ + chains: [chain], + walletPlugins: [{ id: "invalid-signer", login: async () => ({ identity, signer: {} }) }], +}); +await assert.rejects(() => invalidSignerKit.login(), /invalid signer/); +console.log("Session lifecycle tests passed"); From efaf9a14a2b027ea49301ebf66cdcb96602f3ff8 Mon Sep 17 00:00:00 2001 From: Windcrypto Date: Mon, 7 Sep 2026 04:49:05 +0200 Subject: [PATCH 48/49] refactor(vexanium): use native WindStack primitives and sessions --- packages/core/package.json | 2 +- packages/core/src/errors.ts | 13 +- packages/core/src/index.ts | 4 +- packages/core/src/metadata.ts | 28 ++- packages/core/src/provider-contract.ts | 3 +- packages/core/src/types.ts | 15 +- packages/evm/package.json | 2 +- packages/evm/src/client.ts | 35 ++-- packages/evm/src/discovery.ts | 13 +- packages/evm/src/types.ts | 25 ++- packages/solana/package.json | 2 +- packages/solana/src/accounts.ts | 13 +- packages/solana/src/client.ts | 25 ++- packages/solana/src/types.ts | 25 ++- packages/vexanium/package.json | 5 +- packages/vexanium/src/accounts.ts | 41 ++-- packages/vexanium/src/asset.ts | 25 +-- packages/vexanium/src/client.ts | 175 +++++++++++------ packages/vexanium/src/decoder.ts | 35 ++-- packages/vexanium/src/discovery.ts | 20 +- packages/vexanium/src/errors.ts | 20 +- packages/vexanium/src/explorer.ts | 29 ++- packages/vexanium/src/index.ts | 6 +- packages/vexanium/src/models.ts | 8 +- packages/vexanium/src/signing-request.ts | 10 +- packages/vexanium/src/standard.ts | 17 +- packages/vexanium/src/types.ts | 46 +++-- packages/vexanium/src/validation.ts | 16 +- packages/vexanium/tsconfig.json | 2 +- packages/wallet-plugin-wisp/README.md | 30 ++- packages/wallet-plugin-wisp/package.json | 16 +- .../src/WispWalletPlugin.ts | 184 +++++++++--------- packages/wallet-plugin-wisp/tsconfig.json | 2 + scripts/test-provider-contract.mjs | 16 +- scripts/test-provider-spec.mjs | 30 +-- scripts/test-sdk-behavior.mjs | 71 +++---- scripts/test-signing.mjs | 77 ++++---- 37 files changed, 634 insertions(+), 452 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 77719f8..99273f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -35,7 +35,7 @@ "web3" ], "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", + "author": "Gilang Ramadan", "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/core#readme", "repository": { "type": "git", diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index f162f50..a5f36f4 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -33,12 +33,13 @@ export class WispProviderError extends Error { } export function isWispProviderError(value: unknown): value is WispProviderError { - return value instanceof WispProviderError || ( - typeof value === "object" && - value !== null && - "code" in value && - typeof (value as { code: unknown }).code === "number" && - Number.isInteger((value as { code: number }).code) + return ( + value instanceof WispProviderError || + (typeof value === "object" && + value !== null && + "code" in value && + typeof (value as { code: unknown }).code === "number" && + Number.isInteger((value as { code: number }).code)) ); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3629981..198f06b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,9 +17,7 @@ export { resolveDappRequestContext, sameDappRequestOrigin, } from "./metadata.js"; -export { - WISP_PROVIDER_CONTRACT, -} from "./provider-contract.js"; +export { WISP_PROVIDER_CONTRACT } from "./provider-contract.js"; export type { WispProviderContract } from "./provider-contract.js"; export type { EventHandler, EventMap } from "./events.js"; export type { diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index 1a92808..37bce73 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -51,7 +51,10 @@ function normalizeUrl( } } -function normalizeImageUrl(value: string | undefined, base: string | undefined): string | undefined { +function normalizeImageUrl( + value: string | undefined, + base: string | undefined, +): string | undefined { if (typeof value === "string" && /^data:image\/(?:gif|jpeg|png|webp);base64,/i.test(value)) { return value; } @@ -77,8 +80,12 @@ export function readDappMetadataFromDocument(): DappMetadataInput { const origin = getLocationOrigin(); const url = getLocationHref(); const base = origin ?? url; - const favicon = getLinkHref('link[rel~="icon"]') ?? getLinkHref('link[rel="shortcut icon"]') ?? getLinkHref('link[rel="apple-touch-icon"]'); - const image = getMetaContent('meta[property="og:image"]') ?? getMetaContent('meta[name="twitter:image"]'); + const favicon = + getLinkHref('link[rel~="icon"]') ?? + getLinkHref('link[rel="shortcut icon"]') ?? + getLinkHref('link[rel="apple-touch-icon"]'); + const image = + getMetaContent('meta[property="og:image"]') ?? getMetaContent('meta[name="twitter:image"]'); const icon = normalizeImageUrl(favicon ?? image, base); const imageUrl = normalizeImageUrl(image, base); @@ -109,10 +116,12 @@ export function resolveDappMetadata(input: DappMetadataInput = {}): DappMetadata const inputUrl = safeTrim(input.url); const detectedUrl = safeTrim(detected.url); const runtimeOrigin = getLocationOrigin(); - const baseOrigin = runtimeOrigin ?? originFromUrl(inputUrl) ?? originFromUrl(detectedUrl) ?? DEFAULT_ORIGIN; - const url = normalizeUrl(inputUrl, baseOrigin, ["http:", "https:"]) - ?? normalizeUrl(detectedUrl, baseOrigin, ["http:", "https:"]) - ?? baseOrigin; + const baseOrigin = + runtimeOrigin ?? originFromUrl(inputUrl) ?? originFromUrl(detectedUrl) ?? DEFAULT_ORIGIN; + const url = + normalizeUrl(inputUrl, baseOrigin, ["http:", "https:"]) ?? + normalizeUrl(detectedUrl, baseOrigin, ["http:", "https:"]) ?? + baseOrigin; const icon = normalizeImageUrl(safeTrim(input.icon) ?? safeTrim(detected.icon), baseOrigin); const icons = uniq([ icon, @@ -140,6 +149,9 @@ export function resolveDappRequestContext(): DappRequestContext { return { origin: DEFAULT_ORIGIN, source: "unknown" }; } -export function sameDappRequestOrigin(left: DappRequestContext, right: DappRequestContext): boolean { +export function sameDappRequestOrigin( + left: DappRequestContext, + right: DappRequestContext, +): boolean { return left.origin === right.origin; } diff --git a/packages/core/src/provider-contract.ts b/packages/core/src/provider-contract.ts index 4beefc7..14bd3ee 100644 --- a/packages/core/src/provider-contract.ts +++ b/packages/core/src/provider-contract.ts @@ -24,8 +24,7 @@ export const WISP_PROVIDER_CONTRACT = { global: "vexanium", standard: "VexaniumProvider", version: "1.0.0", - chainId: - "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", + chainId: "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", scope: "antelope:f9f432b1851b5c179d2091a96f593aae", capabilities: [ "vex.accounts", diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index afa141d..e1ed475 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -73,7 +73,16 @@ export type WispSession = { export type WispProviderLike = Record> = { request(args: RequestArguments): Promise; - on?(event: TEvent, handler: (payload: TEvents[TEvent]) => void): void; - off?(event: TEvent, handler: (payload: TEvents[TEvent]) => void): void; - removeListener?(event: TEvent, handler: (payload: TEvents[TEvent]) => void): void; + on?( + event: TEvent, + handler: (payload: TEvents[TEvent]) => void, + ): void; + off?( + event: TEvent, + handler: (payload: TEvents[TEvent]) => void, + ): void; + removeListener?( + event: TEvent, + handler: (payload: TEvents[TEvent]) => void, + ): void; }; diff --git a/packages/evm/package.json b/packages/evm/package.json index ad468d4..e5ff71e 100644 --- a/packages/evm/package.json +++ b/packages/evm/package.json @@ -39,7 +39,7 @@ "web3" ], "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", + "author": "Gilang Ramadan", "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/evm#readme", "repository": { "type": "git", diff --git a/packages/evm/src/client.ts b/packages/evm/src/client.ts index d6aec3b..063e518 100644 --- a/packages/evm/src/client.ts +++ b/packages/evm/src/client.ts @@ -19,13 +19,16 @@ const HEX_CHAIN_ID_PATTERN = /^0x(?:0|[1-9a-f][0-9a-f]*)$/i; function assertHexChainId(chainId: string): void { if (typeof chainId !== "string" || !HEX_CHAIN_ID_PATTERN.test(chainId)) { - throw invalidParams("EVM chainId must be a canonical 0x-prefixed hexadecimal integer", { chainId }); + throw invalidParams("EVM chainId must be a canonical 0x-prefixed hexadecimal integer", { + chainId, + }); } } function assertSecureUrls(values: string[] | undefined, field: string): void { if (!values) return; - if (!Array.isArray(values) || values.length === 0) throw invalidParams(`${field} must be a non-empty array`); + if (!Array.isArray(values) || values.length === 0) + throw invalidParams(`${field} must be a non-empty array`); for (const value of values) { try { const url = new URL(value); @@ -39,7 +42,8 @@ function assertSecureUrls(values: string[] | undefined, field: string): void { } function assertAddChainParams(params: AddEthereumChainParameter): void { - if (typeof params !== "object" || params === null) throw invalidParams("Chain parameters are required"); + if (typeof params !== "object" || params === null) + throw invalidParams("Chain parameters are required"); assertHexChainId(params.chainId); assertSecureUrls(params.rpcUrls, "rpcUrls"); assertSecureUrls(params.blockExplorerUrls, "blockExplorerUrls"); @@ -50,16 +54,17 @@ function assertAddChainParams(params: AddEthereumChainParameter): void { if (params.nativeCurrency && params.nativeCurrency.decimals < 0) { throw invalidParams("nativeCurrency.decimals must be a non-negative integer"); } - if (params.nativeCurrency && ( - !params.nativeCurrency.name?.trim() || - !params.nativeCurrency.symbol?.trim() - )) { + if ( + params.nativeCurrency && + (!params.nativeCurrency.name?.trim() || !params.nativeCurrency.symbol?.trim()) + ) { throw invalidParams("nativeCurrency name and symbol are required"); } } export async function createEVMClient(options: EVMClientOptions = {}): Promise { - let provider: EIP1193Provider | null = options.provider ?? await getEVMProvider(options.discoveryTimeoutMs); + let provider: EIP1193Provider | null = + options.provider ?? (await getEVMProvider(options.discoveryTimeoutMs)); const requireProvider = (): EIP1193Provider => { provider = provider ?? getInjectedEVMProvider(); @@ -69,7 +74,9 @@ export async function createEVMClient(options: EVMClientOptions = {}): Promise(args: RequestArguments): Promise => { + const request = async ( + args: RequestArguments, + ): Promise => { try { return await requireProvider().request(args); } catch (error) { @@ -103,10 +110,16 @@ export async function createEVMClient(options: EVMClientOptions = {}): Promise(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void) { + on( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ) { requireProvider().on(event, handler); }, - off(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void) { + off( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ) { const current = requireProvider(); if (current.off) current.off(event, handler); else current.removeListener?.(event, handler); diff --git a/packages/evm/src/discovery.ts b/packages/evm/src/discovery.ts index 9fc4683..413d917 100644 --- a/packages/evm/src/discovery.ts +++ b/packages/evm/src/discovery.ts @@ -7,14 +7,11 @@ import { EVM_PROVIDER_GLOBAL, WISP_EVM_PROVIDER_RDNS, } from "./constants.js"; -import type { - EIP1193Provider, - EIP6963ProviderDetail, - EIP6963ProviderInfo, -} from "./types.js"; +import type { EIP1193Provider, EIP6963ProviderDetail, EIP6963ProviderInfo } from "./types.js"; const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; -const RDNS_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; +const RDNS_PATTERN = + /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; type ProviderRegistry = { details: EIP6963ProviderDetail[]; @@ -82,7 +79,9 @@ function getProviderRegistry(runtimeWindow: RuntimeWindow): ProviderRegistry { function assertDiscoveryTimeout(timeoutMs: number): void { if (!Number.isFinite(timeoutMs) || timeoutMs < 0) { - throw invalidParams("EVM discovery timeout must be a non-negative finite number", { timeoutMs }); + throw invalidParams("EVM discovery timeout must be a non-negative finite number", { + timeoutMs, + }); } } diff --git a/packages/evm/src/types.ts b/packages/evm/src/types.ts index a57ac00..b2dcdfe 100644 --- a/packages/evm/src/types.ts +++ b/packages/evm/src/types.ts @@ -13,9 +13,18 @@ export type EIP1193Provider = { selectedAddress?: string | null; chainId?: string; request(args: RequestArguments): Promise; - on(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void): void; - off?(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void): void; - removeListener(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void): void; + on( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ): void; + off?( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ): void; + removeListener( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ): void; }; export type EIP6963ProviderInfo = { @@ -54,6 +63,12 @@ export type EVMClient = { getChainId(): Promise; switchChain(chainId: string): Promise; addChain(params: AddEthereumChainParameter): Promise; - on(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void): void; - off(event: TEvent, handler: (payload: EVMProviderEventMap[TEvent]) => void): void; + on( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ): void; + off( + event: TEvent, + handler: (payload: EVMProviderEventMap[TEvent]) => void, + ): void; }; diff --git a/packages/solana/package.json b/packages/solana/package.json index 5393b12..7db8d48 100644 --- a/packages/solana/package.json +++ b/packages/solana/package.json @@ -38,7 +38,7 @@ "web3" ], "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", + "author": "Gilang Ramadan", "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/solana#readme", "repository": { "type": "git", diff --git a/packages/solana/src/accounts.ts b/packages/solana/src/accounts.ts index 38cf9dc..6f59904 100644 --- a/packages/solana/src/accounts.ts +++ b/packages/solana/src/accounts.ts @@ -13,8 +13,12 @@ export function isSolanaScope(value: unknown): value is SolanaScope { return typeof value === "string" && SOLANA_SCOPE_PATTERN.test(value); } -export function normalizeSolanaAccount(value: unknown, fallbackScope: SolanaScope = DEFAULT_SOLANA_SCOPE): SolanaAccount { - if (!isSolanaScope(fallbackScope)) throw invalidParams("Invalid fallback Solana scope", fallbackScope); +export function normalizeSolanaAccount( + value: unknown, + fallbackScope: SolanaScope = DEFAULT_SOLANA_SCOPE, +): SolanaAccount { + if (!isSolanaScope(fallbackScope)) + throw invalidParams("Invalid fallback Solana scope", fallbackScope); if (typeof value === "string" && isSolanaPublicKey(value)) { return { scope: fallbackScope, publicKey: value }; } @@ -33,7 +37,10 @@ export function normalizeSolanaAccount(value: unknown, fallbackScope: SolanaScop throw invalidParams("Invalid Solana account payload", value); } -export function normalizeSolanaAccounts(value: unknown, fallbackScope: SolanaScope = DEFAULT_SOLANA_SCOPE): SolanaAccount[] { +export function normalizeSolanaAccounts( + value: unknown, + fallbackScope: SolanaScope = DEFAULT_SOLANA_SCOPE, +): SolanaAccount[] { if (!Array.isArray(value)) throw invalidParams("Invalid Solana accounts payload", value); return value.map((account) => normalizeSolanaAccount(account, fallbackScope)); } diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index eae48a9..7e2886c 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -17,11 +17,17 @@ export async function createSolanaClient(options: SolanaClientOptions = {}): Pro const requireProvider = (): SolanaProvider => { provider = provider ?? getInjectedSolanaProvider(); - if (!provider?.request) throw new WispProviderError(WISP_ERROR_CODES.INTERNAL_ERROR, "No Solana provider is available"); + if (!provider?.request) + throw new WispProviderError( + WISP_ERROR_CODES.INTERNAL_ERROR, + "No Solana provider is available", + ); return provider; }; - const request = async (args: RequestArguments): Promise => { + const request = async ( + args: RequestArguments, + ): Promise => { try { return await requireProvider().request(args); } catch (error) { @@ -31,7 +37,10 @@ export async function createSolanaClient(options: SolanaClientOptions = {}): Pro const assertBase64Transaction = (value: string): void => { if (value.length === 0 || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) { - throw new WispProviderError(WISP_ERROR_CODES.INVALID_PARAMS, "Transaction must be standard base64"); + throw new WispProviderError( + WISP_ERROR_CODES.INVALID_PARAMS, + "Transaction must be standard base64", + ); } }; @@ -73,10 +82,16 @@ export async function createSolanaClient(options: SolanaClientOptions = {}): Pro async disconnect() { await request({ method: SOLANA_METHODS.DISCONNECT }); }, - on(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void) { + on( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ) { requireProvider().on?.(event, handler); }, - off(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void) { + off( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ) { const current = requireProvider(); if (current.off) current.off(event, handler); else current.removeListener?.(event, handler); diff --git a/packages/solana/src/types.ts b/packages/solana/src/types.ts index 5e3df7b..b390981 100644 --- a/packages/solana/src/types.ts +++ b/packages/solana/src/types.ts @@ -23,9 +23,18 @@ export type SolanaProvider = { icon?: string; }; request(args: RequestArguments): Promise; - on?(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void): void; - off?(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void): void; - removeListener?(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void): void; + on?( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ): void; + off?( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ): void; + removeListener?( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ): void; }; export type SolanaClientOptions = { @@ -51,6 +60,12 @@ export type SolanaClient = { signTransaction(transactionBase64: string): Promise; signAndSendTransaction(transactionBase64: string): Promise; disconnect(): Promise; - on(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void): void; - off(event: TEvent, handler: (payload: SolanaProviderEventMap[TEvent]) => void): void; + on( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ): void; + off( + event: TEvent, + handler: (payload: SolanaProviderEventMap[TEvent]) => void, + ): void; }; diff --git a/packages/vexanium/package.json b/packages/vexanium/package.json index 5d496f1..f824b5b 100644 --- a/packages/vexanium/package.json +++ b/packages/vexanium/package.json @@ -23,9 +23,10 @@ "prepack": "npm run build" }, "dependencies": { - "@wharfkit/antelope": "^1.2.0", "@wharfkit/signing-request": "^3.4.0", + "@windstack/abi": "1.0.0", "@windstack/core": "0.6.2", + "@windstack/crypto": "1.0.0", "pako": "^2.2.0" }, "sideEffects": false, @@ -46,7 +47,7 @@ "vexanium-provider" ], "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", + "author": "Gilang Ramadan", "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/vexanium#readme", "repository": { "type": "git", diff --git a/packages/vexanium/src/accounts.ts b/packages/vexanium/src/accounts.ts index faf6dd6..185b4ca 100644 --- a/packages/vexanium/src/accounts.ts +++ b/packages/vexanium/src/accounts.ts @@ -1,15 +1,20 @@ -import { PermissionLevel } from "@wharfkit/antelope"; import type { VexaniumAccount, VexaniumChainId, VexaniumPermissionLevel } from "./types.js"; import { vexaniumInvalidParams } from "./errors.js"; import { isAntelopeName, isVexaniumChainId } from "./validation.js"; -export function parsePermissionLevel(value: string): VexaniumPermissionLevel & { permissionLevel: `${string}@${string}` } { +export function parsePermissionLevel( + value: string, +): VexaniumPermissionLevel & { permissionLevel: `${string}@${string}` } { const candidate = value.includes("@") ? value : `${value}@active`; try { - const parsed = PermissionLevel.from(candidate); - const actor = parsed.actor.toString(); - const permission = parsed.permission.toString(); - if (!isAntelopeName(actor) || !isAntelopeName(permission) || `${actor}@${permission}` !== candidate) { + const parts = candidate.split("@"); + if (parts.length !== 2) throw new Error("Permission level must contain one separator"); + const [actor, permission] = parts as [string, string]; + if ( + !isAntelopeName(actor) || + !isAntelopeName(permission) || + `${actor}@${permission}` !== candidate + ) { throw new Error("Permission level is not canonically encoded"); } return { actor, permission, permissionLevel: `${actor}@${permission}` }; @@ -18,21 +23,24 @@ export function parsePermissionLevel(value: string): VexaniumPermissionLevel & { } } -export function normalizeVexaniumAccount(value: unknown, fallbackChainId: VexaniumChainId): VexaniumAccount { +export function normalizeVexaniumAccount( + value: unknown, + fallbackChainId: VexaniumChainId, +): VexaniumAccount { if (typeof value === "string") { return { chainId: fallbackChainId, ...parsePermissionLevel(value) }; } if (typeof value === "object" && value !== null) { const source = value as Partial & { account?: string; name?: string }; - const aliases = [source.permissionLevel, source.account, source.name] - .filter((candidate) => candidate !== undefined); + const aliases = [source.permissionLevel, source.account, source.name].filter( + (candidate) => candidate !== undefined, + ); if (!aliases.every((candidate) => typeof candidate === "string")) { throw vexaniumInvalidParams("Invalid Vexanium account permission fields", value); } - const permissionLevel = aliases[0] ?? ( - source.actor ? `${source.actor}@${source.permission ?? "active"}` : undefined - ); + const permissionLevel = + aliases[0] ?? (source.actor ? `${source.actor}@${source.permission ?? "active"}` : undefined); const parsed = parsePermissionLevel(permissionLevel ?? ""); const actor = source.actor?.trim() ?? parsed.actor; const permission = source.permission?.trim() ?? parsed.permission; @@ -41,7 +49,9 @@ export function normalizeVexaniumAccount(value: unknown, fallbackChainId: Vexani throw vexaniumInvalidParams("Invalid Vexanium account payload", value); } const canonicalPermission: `${string}@${string}` = `${actor}@${permission}`; - if (aliases.some((alias) => parsePermissionLevel(alias).permissionLevel !== canonicalPermission)) { + if ( + aliases.some((alias) => parsePermissionLevel(alias).permissionLevel !== canonicalPermission) + ) { throw vexaniumInvalidParams("Conflicting Vexanium account permission fields", value); } return { @@ -57,7 +67,10 @@ export function normalizeVexaniumAccount(value: unknown, fallbackChainId: Vexani throw vexaniumInvalidParams("Invalid Vexanium account payload", value); } -export function normalizeVexaniumAccounts(value: unknown, fallbackChainId: VexaniumChainId): VexaniumAccount[] { +export function normalizeVexaniumAccounts( + value: unknown, + fallbackChainId: VexaniumChainId, +): VexaniumAccount[] { if (!Array.isArray(value)) return []; return value.map((account) => normalizeVexaniumAccount(account, fallbackChainId)); } diff --git a/packages/vexanium/src/asset.ts b/packages/vexanium/src/asset.ts index 27a21ee..c044ffe 100644 --- a/packages/vexanium/src/asset.ts +++ b/packages/vexanium/src/asset.ts @@ -1,5 +1,3 @@ -import { Asset } from "@wharfkit/antelope"; - export type VexAsset = { amount: bigint; precision: number; @@ -10,28 +8,21 @@ export type VexAsset = { export function parseAsset(asset: string): VexAsset { const trimmed = asset.trim(); try { - const parsed = Asset.from(trimmed); - if (parsed.toString() !== trimmed) { - throw new Error("Asset is not canonically encoded"); - } - return { - amount: BigInt(parsed.units.toString()), - precision: parsed.symbol.precision, - symbol: parsed.symbol.name, - value: parsed.toString(), - }; + return { ...parseAbiAsset(trimmed) }; } catch { throw new Error(`Invalid Vexanium asset: ${asset}`); } } -export function formatAsset(amount: bigint | number | string, precision: number, symbol: string): string { - return Asset.fromUnits( - BigInt(amount).toString(), - Asset.Symbol.fromParts(symbol, precision), - ).toString(); +export function formatAsset( + amount: bigint | number | string, + precision: number, + symbol: string, +): string { + return formatAbiAsset(amount, precision, symbol); } export function assetToNumber(asset: VexAsset): number { return Number(asset.amount) / 10 ** asset.precision; } +import { formatAsset as formatAbiAsset, parseAsset as parseAbiAsset } from "@windstack/abi"; diff --git a/packages/vexanium/src/client.ts b/packages/vexanium/src/client.ts index e677d34..ba1622b 100644 --- a/packages/vexanium/src/client.ts +++ b/packages/vexanium/src/client.ts @@ -1,8 +1,4 @@ -import { - getRuntimeWindow, - resolveDappMetadata, - resolveDappRequestContext, -} from "@windstack/core"; +import { getRuntimeWindow, resolveDappMetadata, resolveDappRequestContext } from "@windstack/core"; import type { DappMetadata, RequestArguments } from "@windstack/core"; import { VEXANIUM_CAPABILITIES, @@ -48,7 +44,7 @@ import type { VexSigningRequestParams, VexSigningRequestResult, } from "./types.js"; -import { Signature } from "@wharfkit/antelope"; +import { Signature } from "@windstack/crypto"; import { isAntelopeName, isChecksum256, @@ -117,8 +113,21 @@ function cloneSession(value: VexaniumDappSession | null): VexaniumDappSession | }; } -function createLocalSessionId(origin: string, chainId: VexaniumChainId, account: VexaniumAccount): string { - const entropy = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`; +function createLocalSessionId( + origin: string, + chainId: VexaniumChainId, + account: VexaniumAccount, +): string { + const crypto = globalThis.crypto; + let entropy: string; + if (crypto?.randomUUID) entropy = crypto.randomUUID(); + else if (crypto?.getRandomValues) { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + entropy = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); + } else { + throw new Error("Secure platform randomness is required to create a Vexanium session id"); + } return `vex:${origin}:${chainId}:${account.permissionLevel}:${entropy}`; } @@ -126,8 +135,11 @@ function compactParams>(params: T): T { return Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined)) as T; } -function normalizeSyncOptions(value: VexaniumClientOptions["autoSync"]): Required { - if (value === false) return { providerEvents: false, windowFocus: false, visibilityChange: false }; +function normalizeSyncOptions( + value: VexaniumClientOptions["autoSync"], +): Required { + if (value === false) + return { providerEvents: false, windowFocus: false, visibilityChange: false }; if (value === true || value === undefined) return DEFAULT_SYNC_OPTIONS; return { ...DEFAULT_SYNC_OPTIONS, ...value }; } @@ -150,7 +162,11 @@ function assertAccountsResponse(value: unknown): asserts value is VexaniumAccoun } function assertValidSignatures(value: unknown, method: string): asserts value is string[] { - if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string")) { + if ( + !Array.isArray(value) || + value.length === 0 || + !value.every((item) => typeof item === "string") + ) { throw new VexaniumProviderError( VEXANIUM_ERROR_CODES.INVALID_REQUEST, `Malformed ${method} response: expected at least one signature`, @@ -158,7 +174,7 @@ function assertValidSignatures(value: unknown, method: string): asserts value is ); } try { - for (const signature of value) Signature.from(signature); + for (const signature of value) Signature.fromString(signature); } catch { throw new VexaniumProviderError( VEXANIUM_ERROR_CODES.INVALID_REQUEST, @@ -170,7 +186,10 @@ function assertValidSignatures(value: unknown, method: string): asserts value is function assertSignTransactionParams(params: VexSignTransactionParams): void { if (!isVexaniumFullChainId(params.chainId)) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.INVALID_PARAMS, "Invalid Antelope chain ID"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_PARAMS, + "Invalid Antelope chain ID", + ); } if (!isHexBytes(params.serializedTransaction)) { throw new VexaniumProviderError( @@ -204,7 +223,9 @@ function addClientListener( event: TEvent, handler: Listener, ): void { - const current = listeners.get(event) ?? new Set>(); + const current = + listeners.get(event) ?? + new Set>(); current.add(handler as Listener); listeners.set(event, current); } @@ -214,7 +235,9 @@ function removeClientListener( event: TEvent, handler: Listener, ): void { - listeners.get(event)?.delete(handler as Listener); + listeners + .get(event) + ?.delete(handler as Listener); } function emitClientEvent( @@ -229,11 +252,15 @@ function emitClientEvent( } } -export async function createVexaniumClient(options: VexaniumClientOptions = {}): Promise { - let provider: VexaniumProvider | null = options.provider ?? await getVexaniumProvider({ - timeoutMs: options.discoveryTimeoutMs, - rdns: options.providerRdns, - }); +export async function createVexaniumClient( + options: VexaniumClientOptions = {}, +): Promise { + let provider: VexaniumProvider | null = + options.provider ?? + (await getVexaniumProvider({ + timeoutMs: options.discoveryTimeoutMs, + rdns: options.providerRdns, + })); if (provider && !isVexaniumProvider(provider)) { throw new VexaniumProviderError( @@ -264,10 +291,15 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): return provider; }; - const request = async (args: RequestArguments): Promise => { + const request = async ( + args: RequestArguments, + ): Promise => { try { if (destroyed) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.DISCONNECTED, "Vexanium client has been destroyed"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.DISCONNECTED, + "Vexanium client has been destroyed", + ); } return await requireProvider().request(args); } catch (error) { @@ -280,7 +312,8 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): ): Promise => { if (negotiation) { for (const capability of requiredCapabilities) { - if (!negotiation.capabilities.includes(capability)) throw vexaniumUnsupportedCapability(capability); + if (!negotiation.capabilities.includes(capability)) + throw vexaniumUnsupportedCapability(capability); } return negotiation; } @@ -293,36 +326,41 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): version: VEXANIUM_PROVIDER_VERSION, requiredCapabilities, }, - }).then((response) => { - assertVexaniumCapabilitiesResponse(response, requiredCapabilities); - assertCapabilityMethods(response); - const info = requireProvider().providerInfo; - for (const capability of response.capabilities) { - if (!info.capabilities.includes(capability)) { - throw new VexaniumProviderError( - VEXANIUM_ERROR_CODES.INVALID_REQUEST, - `Provider negotiated undeclared capability: ${capability}`, - ); + }) + .then((response) => { + assertVexaniumCapabilitiesResponse(response, requiredCapabilities); + assertCapabilityMethods(response); + const info = requireProvider().providerInfo; + for (const capability of response.capabilities) { + if (!info.capabilities.includes(capability)) { + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_REQUEST, + `Provider negotiated undeclared capability: ${capability}`, + ); + } } - } - for (const chainId of response.chains) { - if (!info.chains.some((declaredChainId) => sameVexaniumChain(chainId, declaredChainId))) { - throw new VexaniumProviderError( - VEXANIUM_ERROR_CODES.INVALID_REQUEST, - `Provider negotiated undeclared chain: ${chainId}`, - ); + for (const chainId of response.chains) { + if ( + !info.chains.some((declaredChainId) => sameVexaniumChain(chainId, declaredChainId)) + ) { + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_REQUEST, + `Provider negotiated undeclared chain: ${chainId}`, + ); + } } - } - negotiation = response; - return response; - }).finally(() => { - negotiationInFlight = null; - }); + negotiation = response; + return response; + }) + .finally(() => { + negotiationInFlight = null; + }); } const response = await negotiationInFlight; for (const capability of requiredCapabilities) { - if (!response.capabilities.includes(capability)) throw vexaniumUnsupportedCapability(capability); + if (!response.capabilities.includes(capability)) + throw vexaniumUnsupportedCapability(capability); } return response; }; @@ -348,9 +386,10 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): const sessionDapp = cloneDappMetadata(nextDapp ?? session?.dapp ?? dapp); const sessionAccounts = cloneAccounts(accounts); session = { - id: session?.walletSessionId === walletSessionId - ? session.id - : createLocalSessionId(requestContext.origin, chainId, accounts[0]), + id: + session?.walletSessionId === walletSessionId + ? session.id + : createLocalSessionId(requestContext.origin, chainId, accounts[0]), walletSessionId, dapp: sessionDapp, origin: session?.origin ?? requestContext.origin, @@ -375,7 +414,10 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): const getChain = async (): Promise => { const chainId = await request({ method: VEXANIUM_METHODS.GET_CHAIN }); if (!isVexaniumChainId(chainId)) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.INVALID_REQUEST, "Malformed vex_getChain response"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_REQUEST, + "Malformed vex_getChain response", + ); } return chainId; }; @@ -384,7 +426,9 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): if (syncInFlight) return syncInFlight; syncInFlight = (async () => { await negotiate([VEXANIUM_CAPABILITIES.ACCOUNTS]); - const rawResponse = await request({ method: VEXANIUM_METHODS.GET_ACCOUNTS }); + const rawResponse = await request({ + method: VEXANIUM_METHODS.GET_ACCOUNTS, + }); assertAccountsResponse(rawResponse); const accounts = normalizeVexaniumAccounts(rawResponse.accounts, rawResponse.chainId); return updateSession({ @@ -405,7 +449,10 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): const requestDapp = params.dapp ?? dapp; const requiredCapabilities = params.requiredCapabilities ?? DEFAULT_CONNECT_CAPABILITIES; if (params.chainId && !isVexaniumChainId(params.chainId)) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.INVALID_PARAMS, "Invalid Vexanium chain ID"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_PARAMS, + "Invalid Vexanium chain ID", + ); } await negotiate(requiredCapabilities); @@ -439,7 +486,8 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): ); } for (const capability of requiredCapabilities) { - if (!rawResponse.capabilities.includes(capability)) throw vexaniumUnsupportedCapability(capability); + if (!rawResponse.capabilities.includes(capability)) + throw vexaniumUnsupportedCapability(capability); } const accounts = normalizeVexaniumAccounts(rawResponse.accounts, rawResponse.chainId); @@ -554,7 +602,9 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): if (runtimeWindow.document.visibilityState === "visible") syncSilently(); }; runtimeWindow.document.addEventListener("visibilitychange", onVisibilityChange); - cleanupCallbacks.add(() => runtimeWindow.document.removeEventListener("visibilitychange", onVisibilityChange)); + cleanupCallbacks.add(() => + runtimeWindow.document.removeEventListener("visibilitychange", onVisibilityChange), + ); } }; @@ -661,7 +711,10 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): async signMessage(message: string | Uint8Array, account?: string) { if (account && !isAntelopeName(account)) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.INVALID_PARAMS, "Invalid Antelope account name"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_PARAMS, + "Invalid Antelope account name", + ); } await negotiate([VEXANIUM_CAPABILITIES.MESSAGE_SIGNING]); const params: VexSignMessageParams = compactParams({ @@ -675,10 +728,16 @@ export async function createVexaniumClient(options: VexaniumClientOptions = {}): async signDigest(digest: string, account?: string) { if (!isChecksum256(digest)) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.INVALID_PARAMS, "Digest must be 32-byte hexadecimal"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_PARAMS, + "Digest must be 32-byte hexadecimal", + ); } if (account && !isAntelopeName(account)) { - throw new VexaniumProviderError(VEXANIUM_ERROR_CODES.INVALID_PARAMS, "Invalid Antelope account name"); + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_PARAMS, + "Invalid Antelope account name", + ); } await negotiate([VEXANIUM_CAPABILITIES.DIGEST_SIGNING]); const params: VexSignDigestParams = compactParams({ diff --git a/packages/vexanium/src/decoder.ts b/packages/vexanium/src/decoder.ts index 091791f..5eabd18 100644 --- a/packages/vexanium/src/decoder.ts +++ b/packages/vexanium/src/decoder.ts @@ -1,4 +1,8 @@ -import type { VexaniumActionModel, VexaniumTransactionModel, VexaniumTransactionStatus } from "./models.js"; +import type { + VexaniumActionModel, + VexaniumTransactionModel, + VexaniumTransactionStatus, +} from "./models.js"; import type { VexaniumPermissionLevel } from "./types.js"; export type ExplorerActionLike = { @@ -33,9 +37,12 @@ export type ExplorerTransactionLike = { signatures?: unknown; }; -const asString = (value: unknown, fallback = ""): string => (typeof value === "string" ? value : fallback); -const asNumber = (value: unknown): number | undefined => (typeof value === "number" && Number.isFinite(value) ? value : undefined); -const asOptionalString = (value: unknown): string | undefined => typeof value === "string" ? value : undefined; +const asString = (value: unknown, fallback = ""): string => + typeof value === "string" ? value : fallback; +const asNumber = (value: unknown): number | undefined => + typeof value === "number" && Number.isFinite(value) ? value : undefined; +const asOptionalString = (value: unknown): string | undefined => + typeof value === "string" ? value : undefined; function normalizeStatus(value: unknown): VexaniumTransactionStatus { switch (value) { @@ -63,7 +70,9 @@ function normalizeAuthorization(value: unknown): VexaniumPermissionLevel[] { .filter((item): item is VexaniumPermissionLevel => Boolean(item)); } -export function mapExplorerAction(input: ExplorerActionLike): VexaniumActionModel { +export function mapExplorerAction( + input: ExplorerActionLike, +): VexaniumActionModel { const source = input.act && typeof input.act === "object" ? input.act : input; return { account: asString(source.account), @@ -74,7 +83,9 @@ export function mapExplorerAction(input: ExplorerActionLike): V }; } -export function mapExplorerTransaction(input: ExplorerTransactionLike): VexaniumTransactionModel { +export function mapExplorerTransaction( + input: ExplorerTransactionLike, +): VexaniumTransactionModel { const actionSource = Array.isArray(input.actions) ? input.actions : Array.isArray(input.action_traces) @@ -84,11 +95,11 @@ export function mapExplorerTransaction(input: ExplorerTra : []; const netWords = asNumber(input.net_usage_words); - const netBytes = asNumber(input.net_usage_bytes) ?? (netWords === undefined ? undefined : netWords * 8); + const netBytes = + asNumber(input.net_usage_bytes) ?? (netWords === undefined ? undefined : netWords * 8); const cpuUs = asNumber(input.cpu_usage_us); - const resourceUsage = cpuUs === undefined && netBytes === undefined - ? undefined - : { cpuUs, netBytes }; + const resourceUsage = + cpuUs === undefined && netBytes === undefined ? undefined : { cpuUs, netBytes }; return { id: asString(input.id ?? input.trx_id ?? input.transaction_id), @@ -96,7 +107,9 @@ export function mapExplorerTransaction(input: ExplorerTra blockNum: asNumber(input.blockNum ?? input.block_num), blockTime: asOptionalString(input.blockTime ?? input.block_time), producer: asOptionalString(input.producer), - actions: actionSource.map((action) => mapExplorerAction(action as ExplorerActionLike)), + actions: actionSource.map((action) => + mapExplorerAction(action as ExplorerActionLike), + ), resourceUsage, console: asOptionalString(input.console), returnValue: input.returnValue ?? input.return_value, diff --git a/packages/vexanium/src/discovery.ts b/packages/vexanium/src/discovery.ts index 7bbeafd..ae22b55 100644 --- a/packages/vexanium/src/discovery.ts +++ b/packages/vexanium/src/discovery.ts @@ -19,7 +19,11 @@ function providerDetail(provider: VexaniumProvider): VexaniumProviderDetail { } function hasRequest(value: unknown): value is Pick { - return typeof value === "object" && value !== null && typeof (value as { request?: unknown }).request === "function"; + return ( + typeof value === "object" && + value !== null && + typeof (value as { request?: unknown }).request === "function" + ); } /** Runtime guard for the formal VexaniumProvider v1 contract. */ @@ -63,7 +67,9 @@ export async function discoverVexaniumProviders( timeoutMs = DEFAULT_PROVIDER_DISCOVERY_TIMEOUT_MS, ): Promise { if (!Number.isFinite(timeoutMs) || timeoutMs < 0) { - throw vexaniumInvalidParams("Provider discovery timeout must be a non-negative finite number", { timeoutMs }); + throw vexaniumInvalidParams("Provider discovery timeout must be a non-negative finite number", { + timeoutMs, + }); } const runtimeWindow = getRuntimeWindow(); if (!runtimeWindow) return []; @@ -88,7 +94,10 @@ export async function discoverVexaniumProviders( runtimeWindow.addEventListener(VEXANIUM_ANNOUNCE_PROVIDER_EVENT, onAnnounce as EventListener); requestVexaniumProviders(); setTimeout(() => { - runtimeWindow.removeEventListener(VEXANIUM_ANNOUNCE_PROVIDER_EVENT, onAnnounce as EventListener); + runtimeWindow.removeEventListener( + VEXANIUM_ANNOUNCE_PROVIDER_EVENT, + onAnnounce as EventListener, + ); resolve(); }, timeoutMs); }); @@ -105,9 +114,8 @@ export type GetVexaniumProviderOptions = { export async function getVexaniumProvider( optionsOrTimeout: GetVexaniumProviderOptions | number = DEFAULT_PROVIDER_DISCOVERY_TIMEOUT_MS, ): Promise { - const options = typeof optionsOrTimeout === "number" - ? { timeoutMs: optionsOrTimeout } - : optionsOrTimeout; + const options = + typeof optionsOrTimeout === "number" ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; const timeoutMs = options.timeoutMs ?? DEFAULT_PROVIDER_DISCOVERY_TIMEOUT_MS; const providers = await discoverVexaniumProviders(timeoutMs); diff --git a/packages/vexanium/src/errors.ts b/packages/vexanium/src/errors.ts index 6856722..7406932 100644 --- a/packages/vexanium/src/errors.ts +++ b/packages/vexanium/src/errors.ts @@ -2,8 +2,7 @@ import { WISP_ERROR_CODES } from "@windstack/core"; export const VEXANIUM_ERROR_CODES = WISP_ERROR_CODES; -export type VexaniumErrorCode = - (typeof VEXANIUM_ERROR_CODES)[keyof typeof VEXANIUM_ERROR_CODES]; +export type VexaniumErrorCode = (typeof VEXANIUM_ERROR_CODES)[keyof typeof VEXANIUM_ERROR_CODES]; export class VexaniumProviderError extends Error { readonly code: VexaniumErrorCode | number; @@ -18,12 +17,13 @@ export class VexaniumProviderError extends Error { } export function isVexaniumProviderError(value: unknown): value is VexaniumProviderError { - return value instanceof VexaniumProviderError || ( - typeof value === "object" && - value !== null && - "code" in value && - typeof (value as { code: unknown }).code === "number" && - Number.isInteger((value as { code: number }).code) + return ( + value instanceof VexaniumProviderError || + (typeof value === "object" && + value !== null && + "code" in value && + typeof (value as { code: unknown }).code === "number" && + Number.isInteger((value as { code: number }).code)) ); } @@ -33,7 +33,9 @@ export function normalizeVexaniumProviderError(error: unknown): VexaniumProvider const candidate = error as { code: number; message?: unknown; data?: unknown }; return new VexaniumProviderError( candidate.code, - typeof candidate.message === "string" ? candidate.message : "Vexanium provider request failed", + typeof candidate.message === "string" + ? candidate.message + : "Vexanium provider request failed", candidate.data, ); } diff --git a/packages/vexanium/src/explorer.ts b/packages/vexanium/src/explorer.ts index bc69574..ce158e7 100644 --- a/packages/vexanium/src/explorer.ts +++ b/packages/vexanium/src/explorer.ts @@ -9,7 +9,8 @@ export type BuildExplorerUrlOptions = { const trimTrailingSlash = (value: string): string => value.replace(/\/+$/, ""); const encodePath = (value: string | number): string => encodeURIComponent(String(value)); -const nativeBase = (baseUrl?: string): string => trimTrailingSlash(baseUrl ?? vexNative.explorerUrl); +const nativeBase = (baseUrl?: string): string => + trimTrailingSlash(baseUrl ?? vexNative.explorerUrl); const evmBase = (baseUrl?: string): string => trimTrailingSlash(baseUrl ?? vexEvm.explorerUrl); const evmRoutes = (baseUrl?: string) => createVexaniumEvmExplorerRoutes(evmBase(baseUrl)); @@ -19,19 +20,29 @@ export function buildExplorerTxUrl(txId: string, options: BuildExplorerUrlOption : `${nativeBase(options.baseUrl)}/tx/${encodePath(txId)}`; } -export function buildExplorerBlockUrl(block: string | number, options: BuildExplorerUrlOptions = {}): string { +export function buildExplorerBlockUrl( + block: string | number, + options: BuildExplorerUrlOptions = {}, +): string { return options.target === "evm" ? evmRoutes(options.baseUrl).block(block) : `${nativeBase(options.baseUrl)}/block/${encodePath(block)}`; } -export function buildExplorerAccountUrl(account: string, options: BuildExplorerUrlOptions = {}): string { +export function buildExplorerAccountUrl( + account: string, + options: BuildExplorerUrlOptions = {}, +): string { return options.target === "evm" ? evmRoutes(options.baseUrl).account(account) : `${nativeBase(options.baseUrl)}/account/${encodePath(account)}`; } -export function buildExplorerTokenUrl(contract: string, symbol?: string, options: BuildExplorerUrlOptions = {}): string { +export function buildExplorerTokenUrl( + contract: string, + symbol?: string, + options: BuildExplorerUrlOptions = {}, +): string { if (options.target === "evm") { return evmRoutes(options.baseUrl).token(contract); } @@ -40,10 +51,16 @@ export function buildExplorerTokenUrl(contract: string, symbol?: string, options : `${nativeBase(options.baseUrl)}/token/${encodePath(contract)}`; } -export function buildExplorerProducerUrl(producer: string, options: Omit = {}): string { +export function buildExplorerProducerUrl( + producer: string, + options: Omit = {}, +): string { return `${nativeBase(options.baseUrl)}/producer/${encodePath(producer)}`; } -export function buildExplorerActionUrl(globalSequence: string | number, options: Omit = {}): string { +export function buildExplorerActionUrl( + globalSequence: string | number, + options: Omit = {}, +): string { return `${nativeBase(options.baseUrl)}/action/${encodePath(globalSequence)}`; } diff --git a/packages/vexanium/src/index.ts b/packages/vexanium/src/index.ts index 8821b91..5ca3ef8 100644 --- a/packages/vexanium/src/index.ts +++ b/packages/vexanium/src/index.ts @@ -1,6 +1,10 @@ import "./window.js"; -export { normalizeVexaniumAccount, normalizeVexaniumAccounts, parsePermissionLevel } from "./accounts.js"; +export { + normalizeVexaniumAccount, + normalizeVexaniumAccounts, + parsePermissionLevel, +} from "./accounts.js"; export { createVexaniumClient } from "./client.js"; export { DEFAULT_PROVIDER_DISCOVERY_TIMEOUT_MS, diff --git a/packages/vexanium/src/models.ts b/packages/vexanium/src/models.ts index 1b32683..d2bd2da 100644 --- a/packages/vexanium/src/models.ts +++ b/packages/vexanium/src/models.ts @@ -8,7 +8,13 @@ export type VexaniumActionModel = { hexData?: string; }; -export type VexaniumTransactionStatus = "executed" | "soft_fail" | "hard_fail" | "delayed" | "expired" | "unknown"; +export type VexaniumTransactionStatus = + | "executed" + | "soft_fail" + | "hard_fail" + | "delayed" + | "expired" + | "unknown"; export type VexaniumResourceUsage = { cpuUs?: number; diff --git a/packages/vexanium/src/signing-request.ts b/packages/vexanium/src/signing-request.ts index 7f958ea..2223356 100644 --- a/packages/vexanium/src/signing-request.ts +++ b/packages/vexanium/src/signing-request.ts @@ -1,7 +1,4 @@ -import { - SigningRequest, - type ZlibProvider, -} from "@wharfkit/signing-request"; +import { SigningRequest, type ZlibProvider } from "@wharfkit/signing-request"; import { deflateRaw, inflateRaw } from "pako"; import { ESR_SCHEME, VSR_SCHEME } from "./constants.js"; import type { @@ -37,9 +34,8 @@ export function encodeSigningRequest( options: Pick = {}, ): CanonicalSigningRequestUri { const zlib = options.zlib ?? defaultZlib; - const encodableRequest = options.compress === true - ? SigningRequest.from(request.encode(false), { zlib }) - : request; + const encodableRequest = + options.compress === true ? SigningRequest.from(request.encode(false), { zlib }) : request; return encodableRequest.encode( options.compress, diff --git a/packages/vexanium/src/standard.ts b/packages/vexanium/src/standard.ts index 9659262..6de85d8 100644 --- a/packages/vexanium/src/standard.ts +++ b/packages/vexanium/src/standard.ts @@ -18,10 +18,10 @@ import type { import { isVexaniumChainId, isVexaniumFullChainId } from "./validation.js"; const SEMVER_PATTERN = new RegExp( - "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + - "(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$", + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" + "(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$", ); -const RDNS_PATTERN = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; +const RDNS_PATTERN = + /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -32,9 +32,10 @@ function isNonEmptyString(value: unknown): value is string { } function isVexaniumCapability(value: unknown): value is VexaniumCapability { - return isNonEmptyString(value) && ( - Object.values(VEXANIUM_CAPABILITIES) as readonly string[] - ).includes(value); + return ( + isNonEmptyString(value) && + (Object.values(VEXANIUM_CAPABILITIES) as readonly string[]).includes(value) + ); } function allUnique(values: unknown[]): boolean { @@ -146,7 +147,9 @@ export function assertVexaniumCapabilitiesResponse( } } -export function assertVexaniumConnectResponse(value: unknown): asserts value is VexaniumConnectResponse { +export function assertVexaniumConnectResponse( + value: unknown, +): asserts value is VexaniumConnectResponse { if (!isRecord(value)) { throw new VexaniumProviderError( VEXANIUM_ERROR_CODES.INVALID_REQUEST, diff --git a/packages/vexanium/src/types.ts b/packages/vexanium/src/types.ts index a436c3e..b32987c 100644 --- a/packages/vexanium/src/types.ts +++ b/packages/vexanium/src/types.ts @@ -6,17 +6,16 @@ import type { ProviderDetail, RequestArguments, } from "@windstack/core"; -import type { SigningRequestCreateArguments, SigningRequestEncodingOptions } from "@wharfkit/signing-request"; -import { - VEXANIUM_CAPABILITIES, - VEXANIUM_PROVIDER_STANDARD, -} from "./constants.js"; +import type { + SigningRequestCreateArguments, + SigningRequestEncodingOptions, +} from "@wharfkit/signing-request"; +import { VEXANIUM_CAPABILITIES, VEXANIUM_PROVIDER_STANDARD } from "./constants.js"; export type VexaniumFullChainId = string; export type VexaniumCaip2ChainId = `antelope:${string}`; export type VexaniumChainId = VexaniumFullChainId | VexaniumCaip2ChainId; -export type VexaniumCapability = - (typeof VEXANIUM_CAPABILITIES)[keyof typeof VEXANIUM_CAPABILITIES]; +export type VexaniumCapability = (typeof VEXANIUM_CAPABILITIES)[keyof typeof VEXANIUM_CAPABILITIES]; export type VexaniumPermissionLevel = { actor: string; @@ -116,9 +115,18 @@ export type VexaniumProviderEventMap = { export type VexaniumProvider = { providerInfo: VexaniumProviderInfo; request(args: RequestArguments): Promise; - on?(event: TEvent, handler: (payload: VexaniumProviderEventMap[TEvent]) => void): void; - off?(event: TEvent, handler: (payload: VexaniumProviderEventMap[TEvent]) => void): void; - removeListener?(event: TEvent, handler: (payload: VexaniumProviderEventMap[TEvent]) => void): void; + on?( + event: TEvent, + handler: (payload: VexaniumProviderEventMap[TEvent]) => void, + ): void; + off?( + event: TEvent, + handler: (payload: VexaniumProviderEventMap[TEvent]) => void, + ): void; + removeListener?( + event: TEvent, + handler: (payload: VexaniumProviderEventMap[TEvent]) => void, + ): void; }; export type VexaniumClientSessionChangeReason = @@ -218,7 +226,9 @@ export type VexaniumClient = { getRequestContext(): DappRequestContext; getSession(): VexaniumDappSession | null; request(args: RequestArguments): Promise; - negotiate(requiredCapabilities?: readonly VexaniumCapability[]): Promise; + negotiate( + requiredCapabilities?: readonly VexaniumCapability[], + ): Promise; connect(params?: VexaniumConnectParams): Promise; connectOne(params?: VexaniumConnectParams): Promise; getAccounts(): Promise; @@ -229,8 +239,16 @@ export type VexaniumClient = { signDigest(digest: string, account?: string): Promise; signTransaction(params: VexSignTransactionParams): Promise; disconnect(): Promise; - on(event: TEvent, handler: (payload: VexaniumClientEventMap[TEvent]) => void): void; - off(event: TEvent, handler: (payload: VexaniumClientEventMap[TEvent]) => void): void; - subscribeSession(handler: (payload: VexaniumClientEventMap["sessionChanged"]) => void): () => void; + on( + event: TEvent, + handler: (payload: VexaniumClientEventMap[TEvent]) => void, + ): void; + off( + event: TEvent, + handler: (payload: VexaniumClientEventMap[TEvent]) => void, + ): void; + subscribeSession( + handler: (payload: VexaniumClientEventMap["sessionChanged"]) => void, + ): () => void; destroy(): void; }; diff --git a/packages/vexanium/src/validation.ts b/packages/vexanium/src/validation.ts index 1382cd9..1f75514 100644 --- a/packages/vexanium/src/validation.ts +++ b/packages/vexanium/src/validation.ts @@ -1,9 +1,5 @@ -import { Bytes, Checksum256, Name } from "@wharfkit/antelope"; -import type { - VexaniumCaip2ChainId, - VexaniumChainId, - VexaniumFullChainId, -} from "./types.js"; +import { hexToBytes, nameToBigInt } from "@windstack/abi"; +import type { VexaniumCaip2ChainId, VexaniumChainId, VexaniumFullChainId } from "./types.js"; const FULL_CHAIN_ID_PATTERN = /^[0-9a-f]{64}$/; const CAIP2_CHAIN_ID_PATTERN = /^antelope:[0-9a-f]{32}$/; @@ -38,7 +34,8 @@ export function sameVexaniumChain(left: VexaniumChainId, right: VexaniumChainId) export function isAntelopeName(value: unknown): value is string { if (typeof value !== "string" || value.length === 0) return false; try { - return Name.from(value).toString() === value; + nameToBigInt(value); + return true; } catch { return false; } @@ -47,8 +44,7 @@ export function isAntelopeName(value: unknown): value is string { export function isHexBytes(value: unknown): value is string { if (typeof value !== "string" || value.length === 0) return false; try { - const bytes = Bytes.from(value); - return bytes.length > 0 && bytes.hexString.toLowerCase() === value.toLowerCase(); + return hexToBytes(value).length > 0; } catch { return false; } @@ -57,7 +53,7 @@ export function isHexBytes(value: unknown): value is string { export function isChecksum256(value: unknown): value is string { if (typeof value !== "string") return false; try { - return Checksum256.from(value).hexString.toLowerCase() === value.toLowerCase(); + return /^[0-9a-f]{64}$/.test(value) && hexToBytes(value).length === 32; } catch { return false; } diff --git a/packages/vexanium/tsconfig.json b/packages/vexanium/tsconfig.json index 8838528..600fed7 100644 --- a/packages/vexanium/tsconfig.json +++ b/packages/vexanium/tsconfig.json @@ -6,5 +6,5 @@ "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, "include": ["src/**/*.ts"], - "references": [{ "path": "../core" }] + "references": [{ "path": "../abi" }, { "path": "../core" }, { "path": "../crypto" }] } diff --git a/packages/wallet-plugin-wisp/README.md b/packages/wallet-plugin-wisp/README.md index 2238beb..915b168 100644 --- a/packages/wallet-plugin-wisp/README.md +++ b/packages/wallet-plugin-wisp/README.md @@ -2,7 +2,7 @@ ## Overview -`@windstack/wallet-plugin-wisp` connects SessionKit applications to Wisp Wallet on Vexanium Mainnet. It handles wallet discovery, account authorization, exact transaction signing, chain validation, and conversion of wallet signatures into the format expected by the connected session. +`@windstack/wallet-plugin-wisp` connects `@windstack/session` applications to Wisp Wallet on Vexanium Mainnet. It handles wallet discovery, account authorization, exact transaction signing, chain validation, and signer integration. The plugin is restricted to Vexanium Mainnet and selects the Wisp provider identified by `com.wisp.wallet` unless a provider or client is supplied explicitly. @@ -15,30 +15,28 @@ npm install @windstack/wallet-plugin-wisp ## Usage ```ts -import { SessionKit } from "@wharfkit/session"; +import { VEXANIUM_MAINNET } from "@windstack/antelope/vexanium"; +import { SessionKit } from "@windstack/session"; import { WispWalletPlugin } from "@windstack/wallet-plugin-wisp"; -import { vexNative } from "@windstack/vexanium"; const sessionKit = new SessionKit({ appName: "My Vexanium App", - chains: [{ id: vexNative.chainId, url: vexNative.rpcUrl }], + chains: [{ + id: VEXANIUM_MAINNET.chainId, + url: VEXANIUM_MAINNET.endpoints, + contracts: VEXANIUM_MAINNET.contracts, + }], walletPlugins: [new WispWalletPlugin()], }); -const { session } = await sessionKit.login(); +const session = await sessionKit.login(); + +const action = await session + .account() + .transfer("receiver", "1.0000 VEX", "WindStack"); await session.transact({ - action: { - account: "vex.token", - name: "transfer", - authorization: [session.permissionLevel], - data: { - from: session.actor, - to: "receiver", - quantity: "1.0000 VEX", - memo: "WindStack", - }, - }, + actions: [action], }); ``` diff --git a/packages/wallet-plugin-wisp/package.json b/packages/wallet-plugin-wisp/package.json index 7a512a3..e0e99a6 100644 --- a/packages/wallet-plugin-wisp/package.json +++ b/packages/wallet-plugin-wisp/package.json @@ -1,7 +1,7 @@ { "name": "@windstack/wallet-plugin-wisp", "version": "0.6.2", - "description": "WharfKit SessionKit wallet plugin for exact Vexanium transaction signing with Wisp.", + "description": "WindStack session plugin for Vexanium transaction signing with Wisp Wallet.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -23,19 +23,11 @@ "prepack": "npm run build" }, "dependencies": { + "@windstack/antelope": "1.0.0", "@windstack/core": "0.6.2", + "@windstack/session": "1.0.0", "@windstack/vexanium": "0.6.2" }, - "peerDependencies": { - "@wharfkit/antelope": "^1.2.0", - "@wharfkit/session": "^1.6.1", - "@wharfkit/signing-request": "^3.4.0" - }, - "devDependencies": { - "@wharfkit/antelope": "^1.2.0", - "@wharfkit/session": "^1.6.1", - "@wharfkit/signing-request": "^3.4.0" - }, "sideEffects": false, "keywords": [ "windstack", @@ -49,7 +41,7 @@ "vaulta" ], "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", + "author": "Gilang Ramadan", "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/wallet-plugin-wisp#readme", "repository": { "type": "git", diff --git a/packages/wallet-plugin-wisp/src/WispWalletPlugin.ts b/packages/wallet-plugin-wisp/src/WispWalletPlugin.ts index f8823c9..3106419 100644 --- a/packages/wallet-plugin-wisp/src/WispWalletPlugin.ts +++ b/packages/wallet-plugin-wisp/src/WispWalletPlugin.ts @@ -1,47 +1,76 @@ -import { Bytes, PermissionLevel, Signature } from "@wharfkit/antelope"; -import { AbstractWalletPlugin, WalletPluginMetadata } from "@wharfkit/session"; -import type { - LoginContext, - TransactContext, - WalletPluginConfig, - WalletPluginLoginResponse, - WalletPluginSignResponse, -} from "@wharfkit/session"; -import type { ResolvedSigningRequest } from "@wharfkit/signing-request"; +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { bytesToHex, type SignRequest, type Signer } from "@windstack/antelope"; +import type { DappMetadataInput } from "@windstack/core"; +import type { WalletLoginContext, WalletLoginResult, WalletPlugin } from "@windstack/session"; import { - VEXANIUM_MAINNET_CHAIN_ID, VEXANIUM_ERROR_CODES, + VEXANIUM_MAINNET_CHAIN_ID, WISP_PROVIDER_RDNS, + VexaniumProviderError, createVexaniumClient, + type VexaniumAccount, type VexaniumClient, type VexaniumProvider, - VexaniumProviderError, - type VexSignTransactionResult, } from "@windstack/vexanium"; -import type { DappMetadataInput } from "@windstack/core"; + +export type WispWalletPluginMetadata = { + name?: string; + description?: string; + icon?: string; + homepage?: string; +}; export type WispWalletPluginOptions = { provider?: VexaniumProvider; client?: VexaniumClient; - metadata?: ConstructorParameters[0]; + metadata?: WispWalletPluginMetadata; dapp?: DappMetadataInput; }; -/** - * WharfKit WalletPlugin for Wisp on Vexanium Mainnet. - * - * SessionKit owns the dApp session lifecycle. This plugin only adapts - * SessionKit login/sign requests to the Wisp Vexanium provider. - */ -export class WispWalletPlugin extends AbstractWalletPlugin { +class WispSigner implements Signer { + constructor( + private readonly client: VexaniumClient, + private readonly account: VexaniumAccount, + ) {} + + async getAvailableKeys(): Promise { + if (!this.account.publicKey) { + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_REQUEST, + "Wisp account identity must include a public key for required-key discovery", + ); + } + return [this.account.publicKey]; + } + + async sign(request: SignRequest): Promise { + if (request.chainId !== VEXANIUM_MAINNET_CHAIN_ID) { + throw new VexaniumProviderError( + VEXANIUM_ERROR_CODES.INVALID_PARAMS, + `WispWalletPlugin cannot sign for unsupported chain: ${request.chainId}`, + ); + } + const result = await this.client.signTransaction({ + serializedTransaction: bytesToHex(request.serializedTransaction), + chainId: request.chainId, + account: this.account.actor, + permission: this.account.permission, + }); + return result.signatures; + } +} + +/** Native WindStack session plugin for Wisp on Vexanium Mainnet. */ +export class WispWalletPlugin implements WalletPlugin { readonly id = "wisp"; - readonly config: WalletPluginConfig = { - requiresChainSelect: false, - requiresPermissionSelect: false, - requiresPermissionEntry: false, - supportedChains: [VEXANIUM_MAINNET_CHAIN_ID], - }; - readonly metadata: WalletPluginMetadata; + readonly metadata: Readonly< + Required> & WispWalletPluginMetadata + >; private readonly suppliedClient?: VexaniumClient; private readonly suppliedProvider?: VexaniumProvider; @@ -49,11 +78,10 @@ export class WispWalletPlugin extends AbstractWalletPlugin { private clientPromise?: Promise; constructor(options: WispWalletPluginOptions = {}) { - super(); this.suppliedClient = options.client; this.suppliedProvider = options.provider; this.dapp = options.dapp; - this.metadata = new WalletPluginMetadata({ + this.metadata = Object.freeze({ name: "Wisp", description: "Connect and sign Vexanium transactions with Wisp.", ...options.metadata, @@ -62,88 +90,66 @@ export class WispWalletPlugin extends AbstractWalletPlugin { private getClient(): Promise { if (this.suppliedClient) return Promise.resolve(this.suppliedClient); - this.clientPromise ??= createVexaniumClient({ provider: this.suppliedProvider, providerRdns: this.suppliedProvider ? undefined : WISP_PROVIDER_RDNS, dapp: this.dapp, }).catch((error) => { - // A transient discovery failure must not permanently poison future login attempts. this.clientPromise = undefined; throw error; }); - return this.clientPromise; } - async login(context: LoginContext): Promise { - if (!context.chain) { - throw new VexaniumProviderError( - VEXANIUM_ERROR_CODES.INVALID_PARAMS, - "A SessionKit chain is required for Wisp login", - ); - } - - const chainId = context.chain.id.toString(); - if (chainId !== VEXANIUM_MAINNET_CHAIN_ID) { + private assertChain(context: WalletLoginContext): void { + if (context.chain.id !== VEXANIUM_MAINNET_CHAIN_ID) { throw new VexaniumProviderError( VEXANIUM_ERROR_CODES.INVALID_PARAMS, `WispWalletPlugin supports Vexanium Mainnet only: ${VEXANIUM_MAINNET_CHAIN_ID}`, - { requestedChainId: chainId }, + { requestedChainId: context.chain.id }, ); } + } - const client = await this.getClient(); - const account = await client.connectOne({ chainId }); - + private loginResult(client: VexaniumClient, account: VexaniumAccount): WalletLoginResult { return { - chain: context.chain.id, - permissionLevel: PermissionLevel.from(account.permissionLevel), + identity: { + actor: account.actor, + permission: account.permission, + publicKey: account.publicKey, + }, + signer: new WispSigner(client, account), }; } - async sign( - resolved: ResolvedSigningRequest, - _context: TransactContext, - ): Promise { - const chainId = resolved.chainId.toString(); - if (chainId !== VEXANIUM_MAINNET_CHAIN_ID) { - throw new VexaniumProviderError( - VEXANIUM_ERROR_CODES.INVALID_PARAMS, - `WispWalletPlugin cannot sign for unsupported chain: ${chainId}`, - ); - } - - // SessionKit has already resolved placeholders, ABI data, TAPOS, signer, and chain. - // Sign these exact serialized bytes directly; do not re-wrap the transaction as ESR/VSR. - const result = await (await this.getClient()).signTransaction({ - serializedTransaction: Bytes.from(resolved.serializedTransaction).hexString, - chainId, - account: resolved.signer.actor.toString(), - permission: resolved.signer.permission.toString(), - }); - - return { - resolved, - signatures: parseSignatures(result), - }; + async login(context: WalletLoginContext): Promise { + this.assertChain(context); + const client = await this.getClient(); + const account = await client.connectOne({ chainId: context.chain.id }); + return this.loginResult(client, account); } -} -function parseSignatures(result: Pick): Signature[] { - if (!Array.isArray(result.signatures) || result.signatures.length === 0) { - throw new VexaniumProviderError( - VEXANIUM_ERROR_CODES.INVALID_REQUEST, - "Wisp returned no transaction signatures", + async restore( + context: WalletLoginContext & { + identity: { actor: string; permission: string; publicKey?: string }; + }, + ): Promise { + this.assertChain(context); + const client = await this.getClient(); + const session = client.getSession(); + if (!session) return null; + const account = session.accounts.find( + (item) => + item.actor === context.identity.actor && item.permission === context.identity.permission, ); + if (!account) return null; + return this.loginResult(client, { + ...account, + publicKey: account.publicKey ?? context.identity.publicKey, + }); } - try { - return result.signatures.map((signature) => Signature.from(signature)); - } catch (error) { - throw new VexaniumProviderError( - VEXANIUM_ERROR_CODES.INVALID_REQUEST, - "Wisp returned an invalid Antelope signature", - error, - ); + + async logout(): Promise { + await (await this.getClient()).disconnect(); } } diff --git a/packages/wallet-plugin-wisp/tsconfig.json b/packages/wallet-plugin-wisp/tsconfig.json index 71b66fb..2a823b6 100644 --- a/packages/wallet-plugin-wisp/tsconfig.json +++ b/packages/wallet-plugin-wisp/tsconfig.json @@ -7,7 +7,9 @@ }, "include": ["src/**/*.ts"], "references": [ + { "path": "../antelope" }, { "path": "../core" }, + { "path": "../session" }, { "path": "../vexanium" } ] } diff --git a/scripts/test-provider-contract.mjs b/scripts/test-provider-contract.mjs index d548b13..41743c1 100644 --- a/scripts/test-provider-contract.mjs +++ b/scripts/test-provider-contract.mjs @@ -152,7 +152,9 @@ const incompatibleClient = await createVexaniumClient({ }); await assert.rejects( () => incompatibleClient.negotiate(), - (error) => error instanceof VexaniumProviderError && error.code === VEXANIUM_ERROR_CODES.INCOMPATIBLE_VERSION, + (error) => + error instanceof VexaniumProviderError && + error.code === VEXANIUM_ERROR_CODES.INCOMPATIBLE_VERSION, ); // Required unsupported capabilities use a standard error code. @@ -175,7 +177,9 @@ const { provider: limitedProvider } = makeProvider({ const limitedClient = await createVexaniumClient({ provider: limitedProvider, autoSync: false }); await assert.rejects( () => limitedClient.negotiate([VEXANIUM_CAPABILITIES.EXACT_TRANSACTION_SIGNING]), - (error) => error instanceof VexaniumProviderError && error.code === VEXANIUM_ERROR_CODES.UNSUPPORTED_CAPABILITY, + (error) => + error instanceof VexaniumProviderError && + error.code === VEXANIUM_ERROR_CODES.UNSUPPORTED_CAPABILITY, ); // Wallet errors keep their standard code through the SDK boundary. @@ -196,10 +200,14 @@ const { provider: rejectingProvider } = makeProvider({ throw new Error(`Unexpected method ${method}`); }, }); -const rejectingClient = await createVexaniumClient({ provider: rejectingProvider, autoSync: false }); +const rejectingClient = await createVexaniumClient({ + provider: rejectingProvider, + autoSync: false, +}); await assert.rejects( () => rejectingClient.connect(), - (error) => error instanceof VexaniumProviderError && error.code === VEXANIUM_ERROR_CODES.USER_REJECTED, + (error) => + error instanceof VexaniumProviderError && error.code === VEXANIUM_ERROR_CODES.USER_REJECTED, ); console.log("VexaniumProvider contract tests: PASS"); diff --git a/scripts/test-provider-spec.mjs b/scripts/test-provider-spec.mjs index 792c976..a74cd69 100644 --- a/scripts/test-provider-spec.mjs +++ b/scripts/test-provider-spec.mjs @@ -1,10 +1,7 @@ import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; -import { - WISP_ERROR_CODES, - WISP_PROVIDER_CONTRACT, -} from "../packages/core/dist/index.js"; +import { WISP_ERROR_CODES, WISP_PROVIDER_CONTRACT } from "../packages/core/dist/index.js"; import { EIP6963_ANNOUNCE_PROVIDER_EVENT, EIP6963_REQUEST_PROVIDER_EVENT, @@ -29,10 +26,7 @@ import { } from "../packages/vexanium/dist/index.js"; const contract = JSON.parse( - await readFile( - new URL("../specs/wisp-provider-contract.json", import.meta.url), - "utf8", - ), + await readFile(new URL("../specs/wisp-provider-contract.json", import.meta.url), "utf8"), ); assert.deepEqual(contract, WISP_PROVIDER_CONTRACT); @@ -50,27 +44,15 @@ assert.equal(contract.vex.chainId, VEXANIUM_MAINNET_CHAIN_ID); assert.equal(contract.vex.scope, VEXANIUM_MAINNET_SCOPE); assert.deepEqual(contract.vex.capabilities, Object.values(VEXANIUM_CAPABILITIES)); assert.deepEqual(Object.values(contract.vex.methods), Object.values(VEXANIUM_METHODS)); -assert.equal( - contract.vex.events.requestProvider, - VEXANIUM_REQUEST_PROVIDER_EVENT, -); -assert.equal( - contract.vex.events.announceProvider, - VEXANIUM_ANNOUNCE_PROVIDER_EVENT, -); +assert.equal(contract.vex.events.requestProvider, VEXANIUM_REQUEST_PROVIDER_EVENT); +assert.equal(contract.vex.events.announceProvider, VEXANIUM_ANNOUNCE_PROVIDER_EVENT); assert.equal(contract.evm.global, EVM_PROVIDER_GLOBAL); assert.equal(contract.evm.chainId, VEX_EVM_CHAIN_ID); assert.equal(contract.evm.chainIdHex, VEX_EVM_CHAIN_ID_HEX); assert.equal(contract.evm.scope, VEX_EVM_SCOPE); assert.deepEqual(Object.values(contract.evm.methods), Object.values(EVM_METHODS)); -assert.equal( - contract.evm.events.requestProvider, - EIP6963_REQUEST_PROVIDER_EVENT, -); -assert.equal( - contract.evm.events.announceProvider, - EIP6963_ANNOUNCE_PROVIDER_EVENT, -); +assert.equal(contract.evm.events.requestProvider, EIP6963_REQUEST_PROVIDER_EVENT); +assert.equal(contract.evm.events.announceProvider, EIP6963_ANNOUNCE_PROVIDER_EVENT); console.log("Canonical Wisp provider specification: PASS"); diff --git a/scripts/test-sdk-behavior.mjs b/scripts/test-sdk-behavior.mjs index 7e9ac5d..e65e70c 100644 --- a/scripts/test-sdk-behavior.mjs +++ b/scripts/test-sdk-behavior.mjs @@ -1,19 +1,12 @@ import assert from "node:assert/strict"; import { Asset, Name } from "@wharfkit/antelope"; -import { - resolveDappMetadata, -} from "../packages/core/dist/index.js"; +import { resolveDappMetadata } from "../packages/core/dist/index.js"; import { createEVMClient, discoverEVMProviders, getEVMProvider, isEIP6963ProviderDetail, } from "../packages/evm/dist/index.js"; -import { - createWispSessionClient, - isEVMScope, - isVexaniumScope, -} from "../packages/session/dist/index.js"; import { VEXANIUM_CAPABILITIES, VEXANIUM_MAINNET_CHAIN_ID, @@ -27,6 +20,7 @@ import { buildExplorerTxUrl, createVexaniumClient, formatAsset, + isVexaniumCaip2ChainId, normalizeVexaniumAccount, parseAsset, parsePermissionLevel, @@ -109,17 +103,19 @@ assert.equal(Object.isFrozen(discovered[0]), true); assert.equal(await getEVMProvider(0), firstProvider); const lateProvider = makeEIP1193Provider("0x1a50"); -runtimeWindow.dispatchEvent(new CustomEvent("eip6963:announceProvider", { - detail: { - info: { - uuid: "f6924454-a838-46af-a89c-2c6a8d7f8421", - name: "Late wallet", - icon: "data:image/svg+xml,", - rdns: "com.example.latewallet", +runtimeWindow.dispatchEvent( + new CustomEvent("eip6963:announceProvider", { + detail: { + info: { + uuid: "f6924454-a838-46af-a89c-2c6a8d7f8421", + name: "Late wallet", + icon: "data:image/svg+xml,", + rdns: "com.example.latewallet", + }, + provider: lateProvider, }, - provider: lateProvider, - }, -})); + }), +); discovered = await discoverEVMProviders(0); assert.equal(discovered.length, 2); @@ -143,7 +139,8 @@ assert.deepEqual(parsePermissionLevel("windstack"), { }); assert.throws(() => parsePermissionLevel("wind@active@owner"), VexaniumProviderError); assert.throws( - () => normalizeVexaniumAccount({ actor: "UPPER", permission: "active" }, VEXANIUM_MAINNET_CHAIN_ID), + () => + normalizeVexaniumAccount({ actor: "UPPER", permission: "active" }, VEXANIUM_MAINNET_CHAIN_ID), VexaniumProviderError, ); assert.equal(sameVexaniumChain(VEXANIUM_MAINNET_CHAIN_ID, VEXANIUM_MAINNET_SCOPE), true); @@ -165,10 +162,8 @@ assert.equal( ); assert.equal(buildExplorerTxUrl("0xabc", { target: "evm" }), vexEvm.routes.tx("0xabc")); assert.equal(buildExplorerAccountUrl("0x123", { target: "evm" }), vexEvm.routes.account("0x123")); -assert.equal(isVexaniumScope(VEXANIUM_MAINNET_SCOPE), true); -assert.equal(isVexaniumScope("antelope:not-a-chain"), false); -assert.equal(isEVMScope("eip155:6736"), true); -assert.equal(isEVMScope("eip155:-1"), false); +assert.equal(isVexaniumCaip2ChainId(VEXANIUM_MAINNET_SCOPE), true); +assert.equal(isVexaniumCaip2ChainId("antelope:not-a-chain"), false); const vexAccount = { actor: "windstack", @@ -224,34 +219,18 @@ sessionSnapshot.walletSessionId = "mutated-session"; assert.equal(vexClient.getSession().accounts[0].actor, "windstack"); assert.equal(vexClient.getSession().walletSessionId, "session-1"); await assert.rejects( - () => vexClient.signTransaction({ - chainId: VEXANIUM_MAINNET_CHAIN_ID, - serializedTransaction: "00", - account: "windstack", - permission: "active", - }), + () => + vexClient.signTransaction({ + chainId: VEXANIUM_MAINNET_CHAIN_ID, + serializedTransaction: "00", + account: "windstack", + permission: "active", + }), { code: -32600 }, ); const signCall = vexCalls.find(({ method }) => method === VEXANIUM_METHODS.SIGN_TRANSACTION); assert.equal(signCall.params.sessionId, "session-1"); -const wrongChainSession = await createWispSessionClient({ - evm: { - isAvailable: () => true, - getProvider: () => null, - request: async () => null, - connect: async () => ["0x0000000000000000000000000000000000000001"], - getAccounts: async () => [], - getChainId: async () => "0x1", - switchChain: async () => null, - addChain: async () => null, - on() {}, - off() {}, - }, -}); -await assert.rejects(() => wrongChainSession.connect(["eip155:6736"]), { code: -32602 }); -assert.equal(wrongChainSession.getSession(), null); - await vexClient.disconnect(); vexClient.destroy(); await assert.rejects(() => vexClient.getChain(), { code: 4900 }); diff --git a/scripts/test-signing.mjs b/scripts/test-signing.mjs index 39d321d..0aac288 100644 --- a/scripts/test-signing.mjs +++ b/scripts/test-signing.mjs @@ -1,12 +1,5 @@ import assert from "node:assert/strict"; -import { - Bytes, - Checksum256, - PermissionLevel, - PrivateKey, - Serializer, - Transaction, -} from "@wharfkit/antelope"; +import { Bytes, Checksum256, PrivateKey, Serializer, Transaction } from "@wharfkit/antelope"; import { SigningRequest } from "@wharfkit/signing-request"; import { deflateRaw, inflateRaw } from "pako"; import { @@ -83,10 +76,7 @@ const portableTransaction = Transaction.from({ transaction_extensions: [], }); const portableBytes = Serializer.encode({ object: portableTransaction }).array; -const wharfRequest = SigningRequest.fromTransaction( - VEXANIUM_MAINNET_CHAIN_ID, - portableBytes, -); +const wharfRequest = SigningRequest.fromTransaction(VEXANIUM_MAINNET_CHAIN_ID, portableBytes); const canonicalVsr = encodeSigningRequest(wharfRequest, { compress: false, slashes: true, @@ -133,11 +123,14 @@ const customZlib = { return inflateRaw(data); }, }; -const customCompressedVsr = await createSigningRequest({ - chainId: VEXANIUM_MAINNET_CHAIN_ID, - transaction: portableTransaction, - info: { note: "custom-zlib-provider-".repeat(64) }, -}, { compress: true, zlib: customZlib }); +const customCompressedVsr = await createSigningRequest( + { + chainId: VEXANIUM_MAINNET_CHAIN_ID, + transaction: portableTransaction, + info: { note: "custom-zlib-provider-".repeat(64) }, + }, + { compress: true, zlib: customZlib }, +); parseSigningRequest(customCompressedVsr, { zlib: customZlib }); assert.equal(customDeflateCalls, 1); @@ -146,37 +139,49 @@ assert.equal(customInflateCalls, 1); const client = await createVexaniumClient({ provider, autoSync: false }); await client.signSigningRequest({ request: esr, broadcast: false }); -const portableCalls = calls.filter( - (call) => call.method === VEXANIUM_METHODS.SIGNING_REQUEST, -); +const portableCalls = calls.filter((call) => call.method === VEXANIUM_METHODS.SIGNING_REQUEST); assert.equal(portableCalls.length, 1); assert.equal(portableCalls[0].params.request, esr); -// Connected SessionKit plugin path: exact serialized transaction bytes only. +// Native WindStack session plugin path: exact serialized transaction bytes only. calls.length = 0; -const plugin = new WispWalletPlugin({ provider }); +const pluginClient = { + async connectOne() { + return { + chainId: VEXANIUM_MAINNET_CHAIN_ID, + actor: "windstack", + permission: "active", + permissionLevel: "windstack@active", + publicKey: privateKey.toPublic().toString(), + }; + }, + async signTransaction(params) { + calls.push({ method: VEXANIUM_METHODS.SIGN_TRANSACTION, params }); + return { signatures: [signature] }; + }, + getSession() { + return null; + }, + async disconnect() {}, +}; +const plugin = new WispWalletPlugin({ client: pluginClient }); const exactBytes = new Uint8Array([0, 1, 2, 255]); -const resolved = { - chainId: Checksum256.from(VEXANIUM_MAINNET_CHAIN_ID), +const login = await plugin.login({ + chain: { id: VEXANIUM_MAINNET_CHAIN_ID, url: "https://api.windcrypto.com" }, +}); +const signed = await login.signer.sign({ + chainId: VEXANIUM_MAINNET_CHAIN_ID, serializedTransaction: exactBytes, - signer: PermissionLevel.from("windstack@active"), -}; - -const signed = await plugin.sign(resolved, {}); -assert.equal(signed.signatures.length, 1); +}); +assert.equal(signed.length, 1); -const exactCalls = calls.filter( - (call) => call.method === VEXANIUM_METHODS.SIGN_TRANSACTION, -); +const exactCalls = calls.filter((call) => call.method === VEXANIUM_METHODS.SIGN_TRANSACTION); assert.equal(exactCalls.length, 1); assert.equal( calls.some((call) => call.method === VEXANIUM_METHODS.SIGNING_REQUEST), false, ); -assert.equal( - exactCalls[0].params.serializedTransaction, - Bytes.from(exactBytes).hexString, -); +assert.equal(exactCalls[0].params.serializedTransaction, Bytes.from(exactBytes).hexString); assert.equal(exactCalls[0].params.chainId, VEXANIUM_MAINNET_CHAIN_ID); assert.equal(exactCalls[0].params.account, "windstack"); assert.equal(exactCalls[0].params.permission, "active"); From 3475e67450cc50482058842bcc010c5e5574094a Mon Sep 17 00:00:00 2001 From: Windcrypto Date: Mon, 7 Sep 2026 04:49:12 +0200 Subject: [PATCH 49/49] chore(release): enforce release validation and migration audit --- .github/workflows/validate.yml | 28 ++- docs/capability-matrix.md | 33 +++ docs/wharfkit-migration.md | 31 +++ package-lock.json | 369 ++++++++++++++++++++++++++----- package.json | 18 +- scripts/audit-native.mjs | 55 +++++ scripts/check-release.mjs | 158 +++++++++++-- scripts/publish-native.mjs | 50 ++++- scripts/verify-vexanium-live.mjs | 132 ++++++++++- 9 files changed, 758 insertions(+), 116 deletions(-) create mode 100644 docs/capability-matrix.md create mode 100644 docs/wharfkit-migration.md create mode 100644 scripts/audit-native.mjs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index e0caa9a..c1aded4 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,7 +5,7 @@ on: push: branches: - main - - feat/native-antelope-sdk-v1 + - audit/release-1.0.0-hardening pull_request: permissions: @@ -25,11 +25,25 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: "24" + node-version-file: ".nvmrc" package-manager-cache: false - name: Install dependencies - run: npm install --no-audit --no-fund - - name: Validate native Antelope packages - run: npm run validate:native - - name: Validate full SDK - run: npm run validate + run: npm ci + - name: Check formatting + run: npm run format:check + - name: Check release metadata + run: npm run check:release + - name: Build native Antelope packages + run: npm run build:native + - name: Test native Antelope packages + run: npm run test:native + - name: Test Vexanium ABI fixtures + run: npm run test:vexanium-abi + - name: Build all packages + run: npm run build + - name: Run regression tests + run: npm test + - name: Check native package tarballs + run: npm run pack:native + - name: Audit native production dependencies + run: npm run audit:native diff --git a/docs/capability-matrix.md b/docs/capability-matrix.md new file mode 100644 index 0000000..5e89c2d --- /dev/null +++ b/docs/capability-matrix.md @@ -0,0 +1,33 @@ +# WindStack Antelope Capability Matrix + +## Overview + +This matrix defines the public scope for WindStack SDK 1.0.0. It is based on Vexanium production ABIs, node RPC behavior, transaction security requirements, and the APIs used by WindCrypto wallets, explorers, dApps, bots, and services. + +| Area | 1.0 classification | WindStack API | Status | +| --- | --- | --- | --- | +| K1/R1 keys, recoverable signatures, WIF and legacy K1 input | MUST HAVE | `@windstack/crypto` | Complete | +| ABI primitives, structs, aliases, variants, extensions and inheritance | MUST HAVE | `@windstack/abi` | Complete | +| `vexcore` and `vex.token` production ABI compatibility | MUST HAVE | ABI fixture and live gates | Complete | +| Safe node RPC reads, endpoint failover and non-retried broadcast | MUST HAVE | `@windstack/rpc` | Complete | +| ABI cache, action encoding and account/symbol/numeric table scopes | MUST HAVE | `@windstack/contract` | Complete | +| Common token, resource, voting, producer, account and permission operations | MUST HAVE | `@windstack/account` | Complete | +| Generic contract action escape hatch | MUST HAVE | `Contract.action()` and `AccountClient.systemAction()` | Complete | +| TAPOS, digest, context-free data, required keys and signer validation | MUST HAVE | `@windstack/antelope` | Complete | +| Strict Vexanium preset | MUST HAVE | `@windstack/antelope/vexanium` | Complete | +| Wallet login, restore, logout, persistence and signer abstraction | MUST HAVE | `@windstack/session` | Complete | +| Wisp provider integration with native sessions | MUST HAVE | `@windstack/wallet-plugin-wisp` | Complete | +| Portable VSR/ESR compatibility for existing applications | MUST HAVE at compatibility boundary | `@windstack/vexanium` adapter | Retained and tested | +| Additional typed node RPC endpoints | SHOULD HAVE | Add only from demonstrated application use | Evaluated per consumer migration | +| Generated contract bindings | SHOULD HAVE | Application build tooling | Outside the core runtime | +| Native VSR/ESR parser, resolver and identity proof implementation | FUTURE / OPTIONAL | Separate audited module | Deferred until full protocol vectors and resolution behavior are covered | +| High-level wrappers for every system-administration action | NOT NEEDED | Use generic actions | Intentionally omitted | +| Indexed history APIs | NOT NEEDED in chain RPC | Use an indexer client | Outside node RPC scope | + +The portable signing-request boundary remains deliberately separate from the seven native packages. Those packages do not import or depend on WharfKit. A future native implementation must cover action, transaction and identity requests; chain constraints; TAPOS placeholders; callbacks; broadcast flags; compression; QR/deep links; parsing; encoding; and resolution before replacing the compatibility adapter. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/docs/wharfkit-migration.md b/docs/wharfkit-migration.md new file mode 100644 index 0000000..3b8143a --- /dev/null +++ b/docs/wharfkit-migration.md @@ -0,0 +1,31 @@ +# WindCrypto WharfKit Migration Inventory + +## Overview + +The inventory below records active source trees discovered under `/root/windcrypto`. Backups, recovery extracts, generated output, lockfiles, logs, and documentation-only references are excluded from migration decisions. + +| Repository | Representative files | Current API | WindStack replacement | Risk | Status | +| --- | --- | --- | --- | --- | --- | +| `windstack-sdk` | `packages/vexanium/src/{accounts,asset,client,validation}.ts` | Antelope names, assets, signatures and checksums | `@windstack/abi`, `@windstack/crypto` and local value helpers | Low | Migrated and tested | +| `windstack-sdk` | `packages/wallet-plugin-wisp/src/WispWalletPlugin.ts` | WharfKit SessionKit plugin | `@windstack/session` `WalletPlugin` and `Signer` | Medium | Migrated and tested | +| `windstack-sdk` | `packages/vexanium/src/signing-request.ts` | SigningRequest encode, parse and compression | Existing compatibility adapter | High | Retained; native protocol replacement is not yet justified | +| `wisp-wallet` | `src/modules/dapp/services/{antelopeDappService,antelopeSigningRequestShared,serializedVexTransactionDecoder}.ts`, `apps/extension/src/background/signing/*` | SigningRequest, identity proof, ABI cache, transaction decode/sign | Native ABI/crypto/RPC plus a future complete signing-request module | High | Pending signing-request and identity-proof parity | +| `wisp-wallet-telegram` | `src/modules/dapp/services/{antelopeDappService,antelopeSigningRequestShared,serializedVexTransactionDecoder}.ts` | SigningRequest, identity proof, ABI cache, transaction decode/sign | Native ABI/crypto/RPC plus a future complete signing-request module | High | Pending signing-request and identity-proof parity | +| `explorer-wind` | `src/features/wallet/context/WindWalletContext.tsx`, `src/features/msig/proposalModel.ts`, `src/lib/{publicKey,windAbiProvider,windVsr}.ts` | SessionKit, packed transactions, ABI and VSR | `@windstack/session`, `@windstack/contract`, `@windstack/crypto`; VSR remains at compatibility boundary | High | Pending controlled application migration | +| `wind-swap-v2` | `src/lib/{actions,signing}.ts`, `src/features/wallet/WindWalletProvider.tsx` | APIClient, ABI cache and action types | `@windstack/rpc`, `@windstack/contract`, `@windstack/session` | Medium | Pending repository test baseline | +| `wisp-tip-bot/server` | `src/antelope/{rpc,signer,tip-contract.gateway,tip-contract.reader}.ts`, `src/app/create-app.ts` | API client, ContractKit, serializer and transaction signing | `@windstack/rpc`, `@windstack/contract`, `@windstack/crypto`, `@windstack/antelope` | Medium | Pending contract-specific migration and tests | +| `wind-realms` | `server/src/{auth/auth.service,game/vex-chain.client}.ts`, `src/auth/AuthProvider.tsx` | Key verification, API client, serialization and transactions | `@windstack/crypto`, `@windstack/abi`, `@windstack/rpc`, `@windstack/antelope` | Medium | Pending coordinated client/server migration | +| `wisp-backend` | `src/services/{rewards,partnerCampaignRewardsAdapter,vexAccountCreation}.service.js` | Keys, signatures, checksums and account transactions | `@windstack/crypto`, `@windstack/account`, `@windstack/antelope` | Medium | Pending transaction API adaptation | +| `wind-wallet-web-vue` | `src/js/{nodes,wallet,chain-state}.js`, transaction and REX pages | APIClient, ContractKit, AccountKit-style resources, values and VSR | Native seven-package stack; VSR stays at compatibility boundary | High | Pending staged wallet migration | +| `wallet/wind-wallet-web-vue` | Same wallet modules and pages in the maintained fork | APIClient, ContractKit, value types and VSR | Native seven-package stack; VSR stays at compatibility boundary | High | Pending fork ownership decision | +| `wallet/wind-wallet-react-heroui-tanstack-fork-v3-flow-aligned` | `src/features/{chain,transactions,wallet}` | ABI cache, API client, keys, values and transactions | Native seven-package stack | Medium | Pending application completion and tests | +| `wisp-dapp-examples` | `react-vite/src/lib/wispNative.ts`, `vue-vite/src/lib/wispNative.ts` | APIClient and ABI cache | `@windstack/rpc` and `@windstack/contract` | Low | Pending release-consumer installation test | +| `evm-miner-vexanium-src` and `contracts/vex-evm-wind/miner` | `src/miner.ts` | Session, private-key wallet and resource helpers | `PrivateKeySigner`, `AntelopeClient`; specialized PowerUp/resource logic stays application-side | High | Pending operational transaction fixtures | + +Direct replacement is intentionally deferred where an application depends on SigningRequest resolution, identity proofs, packed-transaction models, REX/resource abstractions, or application-specific transaction composition that the 1.0 public API does not claim to emulate. Those migrations require repository-local tests and must not be performed as a global import rewrite. + +## License + +MIT License. + +Created by **Gilang Ramadan**. Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/package-lock.json b/package-lock.json index 0832003..953af01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,224 @@ { "name": "windstack-sdk", - "version": "0.6.2", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "windstack-sdk", - "version": "0.6.2", + "version": "1.0.0", "license": "MIT", "workspaces": [ "packages/*" ], "devDependencies": { + "@biomejs/biome": "2.5.12", "typescript": "^7.0.2" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.12.tgz", + "integrity": "sha512-Lw4VHZRebrReBBnlHa12JQjnIBm3JJAA55PDB9LbBVBF0q4RYphm6KfmIjqtPhf61MxZ5Q9KoK8R8x+7per5Aw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.12", + "@biomejs/cli-darwin-x64": "2.5.12", + "@biomejs/cli-linux-arm64": "2.5.12", + "@biomejs/cli-linux-arm64-musl": "2.5.12", + "@biomejs/cli-linux-x64": "2.5.12", + "@biomejs/cli-linux-x64-musl": "2.5.12", + "@biomejs/cli-win32-arm64": "2.5.12", + "@biomejs/cli-win32-x64": "2.5.12" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.12.tgz", + "integrity": "sha512-lCRY1rwgNeWNgTr4DI/u6ZwXTRwRLHAvbaio1YLLGS+4r1nhvB2ssyPqIpfUSmRveNfv0fn/N58C7CAdK2XVrg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.12.tgz", + "integrity": "sha512-vhPgwnh+6tN3ArdAXuET99xaNbFt7CG82Bqn+omHVLC5xdVx45JsYjGPmUIGNzjDek5XdNCP1HKksK7fn8+3bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.12.tgz", + "integrity": "sha512-2gp8aVwXYKdAtmBfRFCUuyDMcfN1ahHqUkGfLYrZlNRFmryMATLVvJgWKvyA8wu4Rwn5OSxM1UcUmOuOFNGeBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.12.tgz", + "integrity": "sha512-couHYjFLL5uuI8ne6zhT7KwEsXo5YP7ry/2xmEqah7qanu0YmfDi3mwJg47YXSuv/NpZj22CZzcRH/5c4gjPSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.12.tgz", + "integrity": "sha512-SnvOs3TSTiuia4SQOUNe1aWC9RT4+YkjcKnOhL/nsKOV0k5ycgBkDzF0lUxKn1V7Q8CLTRq6iV23ZAivHomRoA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.12.tgz", + "integrity": "sha512-8A0oDW58/w9f/PQNYuq0sGUZtGtGrkNF4Z6n0PUoXpLCshi85vtKTv1XSznQawhdE4MXJ8ufpzHXyLFe87M/+w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.12.tgz", + "integrity": "sha512-b9vtoZFsuZt1pdjNwJvXl0f+BpayRzV008uS2+JpmwIKdSE2qdu4A/l04FESwLoou5g2E/Qlec0xwJydZplH+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.12.tgz", + "integrity": "sha512-B1R/l+CwEpKFSuqiwePzPNRk1EiJN8kc0UhdafNz6MZN9v5OFP9HYP1irptvWzHrwVI4blVNGMbxc5zt70m3IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@noble/curves": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.4.0.tgz", + "integrity": "sha512-P4/62zrgfH33CneE3Dn4WhJVA22YUU0eR51wKIan4NVRvwsA0YnPTwWGpNbpuacSujmSFLvyzpyuR30+fbq2Ew==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.4.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", + "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@types/pako": { @@ -365,19 +568,6 @@ "node": ">=16.20.0" } }, - "node_modules/@wharfkit/abicache": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@wharfkit/abicache/-/abicache-1.2.2.tgz", - "integrity": "sha512-yOsYz2qQpQy7Nb8XZj62pZqp8YnmWDqFlrenYksBb9jl+1aWIpFhWd+14VEez4tUAezRH4UWW+w1SX5vhmUY9A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@wharfkit/antelope": "^1.0.2", - "@wharfkit/signing-request": "^3.1.0", - "pako": "^2.0.4", - "tslib": "^2.1.0" - } - }, "node_modules/@wharfkit/antelope": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@wharfkit/antelope/-/antelope-1.2.0.tgz", @@ -392,34 +582,6 @@ "tslib": "^2.0.3" } }, - "node_modules/@wharfkit/common": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@wharfkit/common/-/common-1.5.0.tgz", - "integrity": "sha512-eqXkOy+vshcEzK8kED+EsoTPJjlBKHYglgV9CBnZQgIlGrWIRXWH4YaXH3W7EbI/nCRJCaNqxm5fC+pgpFcp8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@wharfkit/antelope": "^1.0.0" - } - }, - "node_modules/@wharfkit/session": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@wharfkit/session/-/session-1.6.1.tgz", - "integrity": "sha512-k6ntDGOe8bvD/Ps0erTPTFMdYVFrw5cRvPcEwxytlmRRcNV/M8xWcpCYWdmGDxa8QYqynf/hAkbVh1PSwRGl5A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@wharfkit/abicache": "^1.2.1", - "@wharfkit/antelope": "^1.0.11", - "@wharfkit/common": "^1.2.0", - "@wharfkit/signing-request": "^3.1.0", - "pako": "^2.0.4", - "tslib": "^2.1.0" - } - }, "node_modules/@wharfkit/signing-request": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@wharfkit/signing-request/-/signing-request-3.4.0.tgz", @@ -430,14 +592,38 @@ "tslib": "^2.0.3" } }, + "node_modules/@windstack/abi": { + "resolved": "packages/abi", + "link": true + }, + "node_modules/@windstack/account": { + "resolved": "packages/account", + "link": true + }, + "node_modules/@windstack/antelope": { + "resolved": "packages/antelope", + "link": true + }, + "node_modules/@windstack/contract": { + "resolved": "packages/contract", + "link": true + }, "node_modules/@windstack/core": { "resolved": "packages/core", "link": true }, + "node_modules/@windstack/crypto": { + "resolved": "packages/crypto", + "link": true + }, "node_modules/@windstack/evm": { "resolved": "packages/evm", "link": true }, + "node_modules/@windstack/rpc": { + "resolved": "packages/rpc", + "link": true + }, "node_modules/@windstack/session": { "resolved": "packages/session", "link": true @@ -577,6 +763,57 @@ "@typescript/typescript-win32-x64": "7.0.2" } }, + "packages/abi": { + "name": "@windstack/abi", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@windstack/crypto": "1.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "packages/account": { + "name": "@windstack/account", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@windstack/contract": "1.0.0", + "@windstack/crypto": "1.0.0", + "@windstack/rpc": "1.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "packages/antelope": { + "name": "@windstack/antelope", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@windstack/abi": "1.0.0", + "@windstack/account": "1.0.0", + "@windstack/contract": "1.0.0", + "@windstack/crypto": "1.0.0", + "@windstack/rpc": "1.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "packages/contract": { + "name": "@windstack/contract", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@windstack/abi": "1.0.0", + "@windstack/rpc": "1.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "packages/core": { "name": "@windstack/core", "version": "0.6.2", @@ -585,6 +822,18 @@ "node": ">=18" } }, + "packages/crypto": { + "name": "@windstack/crypto", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@noble/curves": "^2.4.0", + "@noble/hashes": "^2.4.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, "packages/evm": { "name": "@windstack/evm", "version": "0.6.2", @@ -596,18 +845,23 @@ "node": ">=18" } }, + "packages/rpc": { + "name": "@windstack/rpc", + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "packages/session": { "name": "@windstack/session", - "version": "0.6.2", + "version": "1.0.0", "license": "MIT", "dependencies": { - "@windstack/core": "0.6.2", - "@windstack/evm": "0.6.2", - "@windstack/solana": "0.6.2", - "@windstack/vexanium": "0.6.2" + "@windstack/antelope": "1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "packages/solana": { @@ -626,9 +880,10 @@ "version": "0.6.2", "license": "MIT", "dependencies": { - "@wharfkit/antelope": "^1.2.0", "@wharfkit/signing-request": "^3.4.0", + "@windstack/abi": "1.0.0", "@windstack/core": "0.6.2", + "@windstack/crypto": "1.0.0", "pako": "^2.2.0" }, "devDependencies": { @@ -643,21 +898,13 @@ "version": "0.6.2", "license": "MIT", "dependencies": { + "@windstack/antelope": "1.0.0", "@windstack/core": "0.6.2", + "@windstack/session": "1.0.0", "@windstack/vexanium": "0.6.2" }, - "devDependencies": { - "@wharfkit/antelope": "^1.2.0", - "@wharfkit/session": "^1.6.1", - "@wharfkit/signing-request": "^3.4.0" - }, "engines": { "node": ">=18" - }, - "peerDependencies": { - "@wharfkit/antelope": "^1.2.0", - "@wharfkit/session": "^1.6.1", - "@wharfkit/signing-request": "^3.4.0" } } } diff --git a/package.json b/package.json index 3e22178..25c75e4 100644 --- a/package.json +++ b/package.json @@ -3,23 +3,27 @@ "version": "1.0.0", "private": true, "type": "module", - "workspaces": ["packages/*"], + "workspaces": [ + "packages/*" + ], "scripts": { "clean": "rm -rf packages/*/dist packages/*/*.tsbuildinfo", "format": "biome format --write packages/*/src scripts package.json package-lock.json tsconfig.json tsconfig.base.json packages/*/package.json packages/*/tsconfig.json specs/wisp-provider-contract.json biome.json", "format:check": "biome format packages/*/src scripts package.json package-lock.json tsconfig.json tsconfig.base.json packages/*/package.json packages/*/tsconfig.json specs/wisp-provider-contract.json biome.json", "check:release": "node scripts/check-release.mjs", + "audit:native": "node scripts/audit-native.mjs", "build:native": "tsc -b packages/crypto packages/abi packages/rpc packages/contract packages/account packages/antelope packages/session", "build": "tsc -b packages/core packages/evm packages/solana packages/crypto packages/abi packages/rpc packages/contract packages/account packages/antelope packages/vexanium packages/wallet-plugin-wisp packages/session", "typecheck": "npm run build", - "pack:native": "npm pack --dry-run -w @windstack/crypto -w @windstack/abi -w @windstack/rpc -w @windstack/contract -w @windstack/account -w @windstack/antelope -w @windstack/session", + "pack:native": "npm run clean && npm run build:native && npm pack --dry-run -w @windstack/crypto -w @windstack/abi -w @windstack/rpc -w @windstack/contract -w @windstack/account -w @windstack/antelope -w @windstack/session", "pack:dry-run": "npm pack --dry-run --workspaces", - "test": "node scripts/test-signing.mjs && node scripts/test-provider-contract.mjs && node scripts/test-provider-spec.mjs && node scripts/test-sdk-behavior.mjs && node scripts/test-native-antelope.mjs", - "test:native": "node scripts/test-native-antelope.mjs", + "test": "node scripts/test-signing.mjs && node scripts/test-provider-contract.mjs && node scripts/test-provider-spec.mjs && node scripts/test-sdk-behavior.mjs && npm run test:native", + "test:native": "node scripts/test-crypto.mjs && node scripts/test-abi.mjs && node scripts/test-rpc.mjs && node scripts/test-contract-account.mjs && node scripts/test-antelope-client.mjs && node scripts/test-session.mjs && node scripts/test-native-antelope.mjs && node scripts/test-vexanium-abi.mjs", + "test:vexanium-abi": "node scripts/test-vexanium-abi.mjs", "verify:vexanium": "node scripts/verify-vexanium-live.mjs", "validate:native": "npm run clean && npm run format:check && npm run check:release && npm run build:native && npm run test:native && npm run pack:native", "validate": "npm run clean && npm run format:check && npm run check:release && npm run build && npm test && npm run pack:dry-run", - "release:dry-run": "npm run validate:native && npm run verify:vexanium", + "release:dry-run": "npm run validate:native && npm run verify:vexanium && npm run audit:native", "release:npm": "node scripts/publish-native.mjs" }, "devDependencies": { @@ -31,7 +35,9 @@ }, "license": "MIT", "author": "Gilang Ramadan", - "contributors": ["PT WIND KRIPTOGRAFI TEKNOLOGI"], + "contributors": [ + "PT WIND KRIPTOGRAFI TEKNOLOGI" + ], "description": "TypeScript SDK packages for WindStack applications across Antelope, Vexanium, EVM, Solana, and Wisp Wallet.", "homepage": "https://github.com/windvex/windstack-sdk#readme", "repository": { diff --git a/scripts/audit-native.mjs b/scripts/audit-native.mjs new file mode 100644 index 0000000..03dc490 --- /dev/null +++ b/scripts/audit-native.mjs @@ -0,0 +1,55 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { spawnSync } from "node:child_process"; +import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const packages = ["crypto", "abi", "rpc", "contract", "account", "antelope", "session"]; +const directory = await mkdtemp(path.join(tmpdir(), "windstack-native-audit-")); + +try { + await writeFile( + path.join(directory, "package.json"), + `${JSON.stringify( + { + name: "windstack-native-audit", + version: "1.0.0", + private: true, + workspaces: ["packages/*"], + }, + null, + 2, + )}\n`, + ); + + for (const name of packages) { + const target = path.join(directory, "packages", name); + await mkdir(target, { recursive: true }); + await copyFile( + path.join(root, "packages", name, "package.json"), + path.join(target, "package.json"), + ); + } + + for (const args of [ + ["install", "--ignore-scripts", "--no-fund", "--no-audit"], + ["ls", "--all"], + ["audit", "--omit=dev"], + ]) { + const result = spawnSync("npm", args, { cwd: directory, encoding: "utf8", stdio: "inherit" }); + if (result.status !== 0) { + throw new Error(`Native dependency audit failed during npm ${args[0]}`); + } + } +} finally { + await rm(directory, { recursive: true, force: true }); +} + +console.log("Native production dependency audit passed"); diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs index 64becbe..fd1905a 100644 --- a/scripts/check-release.mjs +++ b/scripts/check-release.mjs @@ -1,16 +1,21 @@ import assert from "node:assert/strict"; -import { readdir, readFile } from "node:fs/promises"; +import { access, readdir, readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const nativePackages = ["crypto", "abi", "rpc", "contract", "account", "antelope", "session"]; -const forbiddenDependencies = ["elliptic", "bn.js", "crypto-browserify"]; +const forbiddenDependencies = ["elliptic", "bn.js", "crypto-browserify", "randombytes"]; const forbiddenMarkdown = [ - { pattern: /\bAI\b/i, label: "AI wording" }, - { pattern: /\bengineering\b/i, label: "engineering wording" }, - { pattern: /\btechnical\b/i, label: "technical wording" }, - { pattern: /\bteknis\b/i, label: "teknis wording" }, + { pattern: /\b(?:generated|written|built) by (?:an )?AI\b/i, label: "generated wording" }, + { + pattern: /\b(?:engineering|technical|teknis|internal) note\b/i, + label: "internal-note wording", + }, + { pattern: /\b(?:ChatGPT|Codex)\b/i, label: "assistant wording" }, + { pattern: /\bTODO release\b/i, label: "unfinished release wording" }, + { pattern: /\bmigration scratchpad\b/i, label: "migration-note wording" }, + { pattern: /\bdeveloper reminder\b/i, label: "developer-note wording" }, { pattern: /\bEOSIO\b/i, label: "EOSIO branding" }, { pattern: /\bEOS\b/i, label: "EOS branding" }, { pattern: /Publish native packages from VPS/i, label: "deployment note" }, @@ -26,7 +31,14 @@ const requiredReadmeSections = [ "## License", ]; const requiredMarkdownSections = ["## Overview", "## License"]; -const header = "Created by Gilang Ramadan"; +const creatorPattern = /Created by (?:\*\*)?Gilang Ramadan/; +const copyrightPattern = /Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI/; +const sourceHeader = `/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */`; async function readJson(relativePath) { return JSON.parse(await readFile(path.join(root, relativePath), "utf8")); @@ -51,6 +63,7 @@ assert.match(rootPackage.version, /^\d+\.\d+\.\d+$/, "Root version must be seman const manifests = new Map(); for (const packageDirectory of nativePackages) { const manifest = await readJson(`packages/${packageDirectory}/package.json`); + assert.equal(manifest.name, `@windstack/${packageDirectory}`); manifests.set(manifest.name, manifest); assert.equal( manifest.version, @@ -60,6 +73,32 @@ for (const packageDirectory of nativePackages) { assert.equal(manifest.author, "Gilang Ramadan", `${manifest.name} author must be Gilang Ramadan`); assert.equal(manifest.license, "MIT", `${manifest.name} must use MIT`); assert.equal(manifest.publishConfig?.access, "public", `${manifest.name} must publish as public`); + assert.equal( + manifest.publishConfig?.registry, + "https://registry.npmjs.org/", + `${manifest.name} must use the public npm registry`, + ); + assert.equal(typeof manifest.description, "string", `${manifest.name} must have a description`); + assert.ok(manifest.description.trim().length >= 20, `${manifest.name} description is too short`); + assert.equal(manifest.main, "./dist/index.js", `${manifest.name} main entry is invalid`); + assert.equal(manifest.types, "./dist/index.d.ts", `${manifest.name} type entry is invalid`); + assert.ok(manifest.exports?.["."], `${manifest.name} must export its primary entrypoint`); + assert.equal( + manifest.repository?.directory, + `packages/${packageDirectory}`, + `${manifest.name} repository directory is invalid`, + ); + assert.equal( + manifest.homepage, + `https://github.com/windvex/windstack-sdk/tree/main/packages/${packageDirectory}#readme`, + `${manifest.name} homepage is invalid`, + ); + assert.equal( + manifest.bugs?.url, + "https://github.com/windvex/windstack-sdk/issues", + `${manifest.name} bugs URL is invalid`, + ); + assert.equal(manifest.engines?.node, ">=20.19.0", `${manifest.name} Node requirement is invalid`); assert.equal(manifest.sideEffects, false, `${manifest.name} must declare sideEffects=false`); assert.ok( Array.isArray(manifest.files) && manifest.files.includes("dist"), @@ -75,13 +114,35 @@ for (const packageDirectory of nativePackages) { for (const section of requiredReadmeSections) { assert.ok(readme.includes(section), `${manifest.name} README is missing ${section}`); } - assert.ok(readme.includes(header), `${manifest.name} README must credit Gilang Ramadan`); + if (["crypto", "rpc", "antelope", "session"].includes(packageDirectory)) { + assert.ok(readme.includes("## Security"), `${manifest.name} README is missing ## Security`); + } + assert.match(readme, creatorPattern, `${manifest.name} README must credit Gilang Ramadan`); + assert.match(readme, copyrightPattern, `${manifest.name} README copyright is missing`); + await access(path.join(root, `packages/${packageDirectory}/LICENSE`)); +} + +const visiting = new Set(); +const visited = new Set(); +function visitNative(name) { + if (visiting.has(name)) throw new TypeError(`Circular native dependency detected at ${name}`); + if (visited.has(name)) return; + visiting.add(name); + for (const dependency of Object.keys(manifests.get(name)?.dependencies ?? {})) { + if (manifests.has(dependency)) visitNative(dependency); + } + visiting.delete(name); + visited.add(name); } +for (const name of manifests.keys()) visitNative(name); for (const [name, manifest] of manifests) { for (const [dependency, version] of Object.entries(manifest.dependencies ?? {})) { assert.ok(!dependency.startsWith("@wharfkit/"), `${name} cannot depend on ${dependency}`); - assert.ok(!forbiddenDependencies.includes(dependency), `${name} cannot depend on ${dependency}`); + assert.ok( + !forbiddenDependencies.includes(dependency), + `${name} cannot depend on ${dependency}`, + ); if (manifests.has(dependency)) { assert.equal( version, @@ -92,21 +153,40 @@ for (const [name, manifest] of manifests) { } } -const sourceChecks = [ - "packages/crypto/src/index.ts", - "packages/abi/src/index.ts", - "packages/rpc/src/index.ts", - "packages/contract/src/index.ts", - "packages/account/src/index.ts", - "packages/antelope/src/index.ts", - "packages/antelope/src/vexanium.ts", - "packages/session/src/index.ts", - "packages/session/src/native.ts", - "packages/session/src/compat.ts", -]; +const sourceChecks = ( + await Promise.all( + nativePackages.map(async (packageDirectory) => + ( + await walk(path.join(root, "packages", packageDirectory, "src")) + ) + .filter((file) => file.endsWith(".ts")) + .map((file) => path.relative(root, file)), + ), + ) +).flat(); for (const relativePath of sourceChecks) { const source = await readFile(path.join(root, relativePath), "utf8"); - assert.ok(source.includes(header), `${relativePath} must include creator attribution`); + assert.ok(source.startsWith(sourceHeader), `${relativePath} must include creator attribution`); + assert.doesNotMatch(source, /@wharfkit\//, `${relativePath} cannot import WharfKit`); + assert.doesNotMatch(source, /\bMath\.random\s*\(/, `${relativePath} cannot use Math.random`); + assert.doesNotMatch( + source, + /(?:from\s+["']node:|\bBuffer\b)/, + `${relativePath} must remain portable`, + ); +} + +const vexaniumPreset = await readFile(path.join(root, "packages/antelope/src/vexanium.ts"), "utf8"); +for (const expected of [ + "Vexanium Mainnet", + "f9f432b1851b5c179d2091a96f593aaed50ec7466b74f89301f957a83e56ce1f", + "https://api.windcrypto.com", + '"vexcore"', + '"vex.token"', + '"VEX"', + "VEXANIUM_NATIVE_PRECISION = 4", +]) { + assert.ok(vexaniumPreset.includes(expected), `Vexanium preset is missing ${expected}`); } const markdownFiles = (await walk(root)).filter((file) => file.endsWith(".md")); @@ -119,11 +199,16 @@ for (const file of markdownFiles) { for (const section of requiredMarkdownSections) { assert.ok(content.includes(section), `${relativePath} is missing ${section}`); } - assert.ok(content.includes(header), `${relativePath} must credit Gilang Ramadan`); + assert.match(content, creatorPattern, `${relativePath} must credit Gilang Ramadan`); + assert.match(content, copyrightPattern, `${relativePath} copyright is missing`); } const lock = await readJson("package-lock.json"); -assert.equal(lock.version, rootPackage.version, "package-lock root version must match package.json"); +assert.equal( + lock.version, + rootPackage.version, + "package-lock root version must match package.json", +); assert.equal( lock.packages?.[""]?.version, rootPackage.version, @@ -136,6 +221,31 @@ for (const packageDirectory of nativePackages) { manifest.version, `package-lock entry for ${manifest.name} is stale`, ); + assert.deepEqual( + lock.packages?.[`packages/${packageDirectory}`]?.dependencies ?? {}, + manifest.dependencies ?? {}, + `package-lock dependencies for ${manifest.name} are stale`, + ); +} + +const reachable = new Set(); +function visitDependency(name) { + if (reachable.has(name)) return; + reachable.add(name); + const manifest = manifests.get(name); + const dependencies = + manifest?.dependencies ?? lock.packages?.[`node_modules/${name}`]?.dependencies ?? {}; + for (const dependency of Object.keys(dependencies)) visitDependency(dependency); +} +for (const manifest of manifests.values()) { + for (const dependency of Object.keys(manifest.dependencies ?? {})) visitDependency(dependency); +} +for (const dependency of reachable) { + assert.ok(!dependency.startsWith("@wharfkit/"), `Native dependency graph reaches ${dependency}`); + assert.ok( + !forbiddenDependencies.includes(dependency), + `Native dependency graph reaches ${dependency}`, + ); } console.log(`Release guard passed for WindStack ${rootPackage.version}`); diff --git a/scripts/publish-native.mjs b/scripts/publish-native.mjs index ba6c925..0f74d9e 100644 --- a/scripts/publish-native.mjs +++ b/scripts/publish-native.mjs @@ -14,23 +14,43 @@ function run(command, args, options = {}) { stdio: options.capture ? "pipe" : "inherit", }); if (result.status !== 0 && !options.allowFailure) { - throw new Error(`${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}`); + throw new Error( + `${command} ${args.join(" ")} failed with exit code ${result.status ?? "unknown"}`, + ); } return result; } function requireCleanMain() { - const branch = execFileSync("git", ["branch", "--show-current"], { cwd: root, encoding: "utf8" }).trim(); - if (branch !== "main") throw new Error(`Release must run from main; current branch is ${branch || "detached"}`); - const status = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf8" }).trim(); + const branch = execFileSync("git", ["branch", "--show-current"], { + cwd: root, + encoding: "utf8", + }).trim(); + if (branch !== "main") + throw new Error(`Release must run from main; current branch is ${branch || "detached"}`); + const status = execFileSync("git", ["status", "--porcelain"], { + cwd: root, + encoding: "utf8", + }).trim(); if (status) throw new Error("Release requires a clean working tree"); } function publishedVersion(name, version) { - const result = run("npm", ["view", `${name}@${version}`, "version", "--json"], { - capture: true, - allowFailure: true, - }); + const result = run( + "npm", + [ + "view", + `${name}@${version}`, + "version", + "--json", + "--registry", + "https://registry.npmjs.org/", + ], + { + capture: true, + allowFailure: true, + }, + ); if (result.status === 0) { const parsed = JSON.parse(result.stdout || "null"); return parsed === version; @@ -45,7 +65,9 @@ run("npm", ["whoami"]); run("npm", ["run", "release:dry-run"]); for (const directory of packageDirectories) { - const manifest = JSON.parse(await readFile(path.join(root, "packages", directory, "package.json"), "utf8")); + const manifest = JSON.parse( + await readFile(path.join(root, "packages", directory, "package.json"), "utf8"), + ); const { name, version } = manifest; if (publishedVersion(name, version)) { console.log(`${name}@${version} is already published; skipping.`); @@ -53,7 +75,15 @@ for (const directory of packageDirectories) { } console.log(`Publishing ${name}@${version}...`); - run("npm", ["publish", "--workspace", name, "--access", "public"]); + run("npm", [ + "publish", + "--workspace", + name, + "--access", + "public", + "--registry", + "https://registry.npmjs.org/", + ]); let verified = false; for (let attempt = 0; attempt < 6; attempt += 1) { diff --git a/scripts/verify-vexanium-live.mjs b/scripts/verify-vexanium-live.mjs index 3bcb417..e1abbc3 100644 --- a/scripts/verify-vexanium-live.mjs +++ b/scripts/verify-vexanium-live.mjs @@ -13,7 +13,9 @@ const EXPECTED_ABI = { const scalarOne = Uint8Array.from({ length: 32 }, (_, index) => (index === 31 ? 1 : 0)); const privateKey = PrivateKey.fromBytes("K1", scalarOne); const publicKey = privateKey.toPublicKey().toString(); -const signature = privateKey.signDigest(sha256Digest(new TextEncoder().encode("WindStack Vexanium ABI"))).toString(); +const signature = privateKey + .signDigest(sha256Digest(new TextEncoder().encode("WindStack Vexanium ABI"))) + .toString(); async function post(path, body = {}) { const response = await fetch(`${RPC}${path}`, { @@ -24,7 +26,10 @@ async function post(path, body = {}) { }); const payload = await response.json().catch(() => null); if (!response.ok) { - throw new Error(`Vexanium RPC ${path} failed with HTTP ${response.status}`); + const detail = payload?.error?.what ?? payload?.message; + throw new Error( + `Vexanium RPC ${path} failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`, + ); } return payload; } @@ -53,7 +58,9 @@ function structuralAbi(abi) { } function structuralHash(abi) { - return createHash("sha256").update(stableStringify(structuralAbi(abi))).digest("hex"); + return createHash("sha256") + .update(stableStringify(structuralAbi(abi))) + .digest("hex"); } function makeSampler(abi) { @@ -78,7 +85,8 @@ function makeSampler(abi) { if (rawType.endsWith("$")) return undefined; const type = resolve(rawType); - if (stack.includes(type)) throw new Error(`Recursive ABI type cannot be sampled: ${[...stack, type].join(" -> ")}`); + if (stack.includes(type)) + throw new Error(`Recursive ABI type cannot be sampled: ${[...stack, type].join(" -> ")}`); const struct = structs.get(type); if (struct) { @@ -161,7 +169,11 @@ function makeSampler(abi) { function validateAbi(contract, abi) { assert.ok(abi && typeof abi === "object", `${contract} returned no ABI`); assert.equal(abi.version, "eosio::abi/1.2", `${contract} ABI version changed`); - assert.equal(structuralHash(abi), EXPECTED_ABI[contract], `${contract} ABI changed since release audit`); + assert.equal( + structuralHash(abi), + EXPECTED_ABI[contract], + `${contract} ABI changed since release audit`, + ); const serializer = new AbiSerializer(abi); const sample = makeSampler(abi); @@ -188,9 +200,105 @@ const system = await post("/v1/chain/get_abi", { account_name: "vexcore" }); validateAbi("vex.token", token.abi); validateAbi("vexcore", system.abi); +const block = await post("/v1/chain/get_block", { + block_num_or_id: info.last_irreversible_block_num, +}); +assert.equal(block.block_num, info.last_irreversible_block_num); +assert.match(block.id, /^[0-9a-f]{64}$/i); + +const account = await post("/v1/chain/get_account", { account_name: "vexcore" }); +assert.equal(account.account_name, "vexcore"); + +const rawAbi = await post("/v1/chain/get_raw_abi", { account_name: "vex.token" }); +assert.equal(rawAbi.account_name, "vex.token"); +assert.match(rawAbi.abi_hash, /^[0-9a-f]{64}$/i); + +const codeHash = await post("/v1/chain/get_code_hash", { account_name: "vex.token" }); +assert.equal(codeHash.account_name, "vex.token"); +assert.match(codeHash.code_hash, /^[0-9a-f]{64}$/i); + +const statRows = await post("/v1/chain/get_table_rows", { + json: true, + code: "vex.token", + scope: "VEX", + table: "stat", + limit: 1, +}); +assert.match(statRows.rows[0]?.supply, / VEX$/); + +const scopes = await post("/v1/chain/get_table_by_scope", { + code: "vex.token", + table: "accounts", + limit: 1, +}); +assert.ok(Array.isArray(scopes.rows)); + +const balance = await post("/v1/chain/get_currency_balance", { + code: "vex.token", + account: "vexcore", + symbol: "VEX", +}); +assert.ok(Array.isArray(balance)); +assert.ok(balance.every((value) => / VEX$/.test(value))); + +const stats = await post("/v1/chain/get_currency_stats", { + code: "vex.token", + symbol: "VEX", +}); +assert.match(stats.VEX?.supply, / VEX$/); + +const expiration = new Date(`${info.head_block_time}Z`); +expiration.setUTCSeconds(expiration.getUTCSeconds() + 60); +const producers = await post("/v1/chain/get_table_rows", { + json: true, + code: "vexcore", + scope: "vexcore", + table: "producers", + limit: 1, +}); +const actor = producers.rows[0]?.owner; +assert.match(actor, /^[.1-5a-z]{1,12}$/); +const actorAccount = await post("/v1/chain/get_account", { account_name: actor }); +const activePermission = actorAccount.permissions?.find( + (permission) => permission.perm_name === "active", +); +const activeKey = activePermission?.required_auth?.keys?.find( + (key) => key.weight >= activePermission.required_auth.threshold, +)?.key; +assert.equal(typeof activeKey, "string", `${actor} has no directly usable active key`); +const transferData = new AbiSerializer(token.abi).encodeAction("transfer", { + from: actor, + to: actor, + quantity: "0.0001 VEX", + memo: "read-only required-key probe", +}); +const requiredKeys = await post("/v1/chain/get_required_keys", { + transaction: { + expiration: expiration.toISOString().replace(/\.\d{3}Z$/, ""), + ref_block_num: block.block_num & 0xffff, + ref_block_prefix: block.ref_block_prefix, + max_net_usage_words: 0, + max_cpu_usage_ms: 0, + delay_sec: 0, + context_free_actions: [], + actions: [ + { + account: "vex.token", + name: "transfer", + authorization: [{ actor, permission: "active" }], + data: Array.from(transferData, (byte) => byte.toString(16).padStart(2, "0")).join(""), + }, + ], + transaction_extensions: [], + }, + available_keys: [activeKey], +}); +assert.deepEqual(requiredKeys.required_keys, [activeKey]); + const tokenActions = new Set(token.abi.actions.map((item) => item.name)); const tokenTables = new Set(token.abi.tables.map((item) => item.name)); -for (const name of ["transfer", "open", "close", "issue", "retire"]) assert.ok(tokenActions.has(name)); +for (const name of ["transfer", "open", "close", "issue", "retire"]) + assert.ok(tokenActions.has(name)); for (const name of ["accounts", "stat", "blacklist"]) assert.ok(tokenTables.has(name)); const systemActions = new Set(system.abi.actions.map((item) => item.name)); @@ -213,10 +321,18 @@ for (const name of [ ]) { assert.ok(systemActions.has(name), `vexcore is missing ${name}`); } -for (const name of ["producers", "voters", "refunds", "userres", "delband", "rammarket", "instantund"]) { +for (const name of [ + "producers", + "voters", + "refunds", + "userres", + "delband", + "rammarket", + "instantund", +]) { assert.ok(systemTables.has(name), `vexcore is missing table ${name}`); } console.log( - `Vexanium production ABI verified: ${token.abi.actions.length} vex.token actions, ${system.abi.actions.length} vexcore actions`, + `Vexanium read-only RPC and production ABI verified: ${token.abi.actions.length} vex.token actions, ${system.abi.actions.length} vexcore actions`, );