From 7ce2f6200b3b6c0bb72c470cb672b7f682fd6c03 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 06:48:15 +0700 Subject: [PATCH 1/6] feat(antelope): add native WindStack SDK v1 --- .github/workflows/validate.yml | 13 +-- README.md | 165 +++++++--------------------- package.json | 46 ++++---- packages/abi/LICENSE | 21 ++++ packages/abi/README.md | 7 ++ packages/abi/package.json | 18 +++ packages/abi/src/index.ts | 183 +++++++++++++++++++++++++++++++ packages/abi/tsconfig.json | 6 + packages/account/LICENSE | 21 ++++ packages/account/README.md | 7 ++ packages/account/package.json | 18 +++ packages/account/src/index.ts | 25 +++++ packages/account/tsconfig.json | 6 + packages/antelope/LICENSE | 21 ++++ packages/antelope/README.md | 19 ++++ packages/antelope/package.json | 18 +++ packages/antelope/src/index.ts | 95 ++++++++++++++++ packages/antelope/tsconfig.json | 6 + packages/contract/LICENSE | 21 ++++ packages/contract/README.md | 7 ++ packages/contract/package.json | 18 +++ packages/contract/src/index.ts | 43 ++++++++ packages/contract/tsconfig.json | 6 + packages/crypto/LICENSE | 21 ++++ packages/crypto/README.md | 20 ++++ packages/crypto/package.json | 18 +++ packages/crypto/src/index.ts | 174 +++++++++++++++++++++++++++++ packages/crypto/tsconfig.json | 5 + packages/rpc/LICENSE | 21 ++++ packages/rpc/README.md | 7 ++ packages/rpc/package.json | 17 +++ packages/rpc/src/index.ts | 69 ++++++++++++ packages/rpc/tsconfig.json | 5 + packages/session/README.md | 32 ++---- packages/session/package.json | 64 ++--------- packages/session/src/index.ts | 10 +- packages/session/src/native.ts | 58 ++++++++++ packages/session/tsconfig.json | 15 +-- scripts/test-native-antelope.mjs | 41 +++++++ tsconfig.base.json | 37 +++---- tsconfig.json | 6 + 41 files changed, 1134 insertions(+), 276 deletions(-) create mode 100644 packages/abi/LICENSE create mode 100644 packages/abi/README.md create mode 100644 packages/abi/package.json create mode 100644 packages/abi/src/index.ts create mode 100644 packages/abi/tsconfig.json create mode 100644 packages/account/LICENSE create mode 100644 packages/account/README.md create mode 100644 packages/account/package.json create mode 100644 packages/account/src/index.ts create mode 100644 packages/account/tsconfig.json create mode 100644 packages/antelope/LICENSE create mode 100644 packages/antelope/README.md create mode 100644 packages/antelope/package.json create mode 100644 packages/antelope/src/index.ts create mode 100644 packages/antelope/tsconfig.json create mode 100644 packages/contract/LICENSE create mode 100644 packages/contract/README.md create mode 100644 packages/contract/package.json create mode 100644 packages/contract/src/index.ts create mode 100644 packages/contract/tsconfig.json create mode 100644 packages/crypto/LICENSE create mode 100644 packages/crypto/README.md create mode 100644 packages/crypto/package.json create mode 100644 packages/crypto/src/index.ts create mode 100644 packages/crypto/tsconfig.json create mode 100644 packages/rpc/LICENSE create mode 100644 packages/rpc/README.md create mode 100644 packages/rpc/package.json create mode 100644 packages/rpc/src/index.ts create mode 100644 packages/rpc/tsconfig.json create mode 100644 packages/session/src/native.ts create mode 100644 scripts/test-native-antelope.mjs diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a38c36c..e0caa9a 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -5,6 +5,7 @@ on: push: branches: - main + - feat/native-antelope-sdk-v1 pull_request: permissions: @@ -17,20 +18,18 @@ concurrency: jobs: validate: runs-on: ubuntu-latest - timeout-minutes: 15 - + timeout-minutes: 20 steps: - name: Check out repository uses: actions/checkout@v6 - - name: Set up Node.js uses: actions/setup-node@v6 with: node-version: "24" package-manager-cache: false - - name: Install dependencies - run: npm ci - - - name: Validate SDK + 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 diff --git a/README.md b/README.md index 882e27d..37f6e49 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,66 @@ -# Wind Stack SDK +# WindStack SDK -TypeScript packages used by Wind Stack applications and Wisp Wallet integrations. +Modern TypeScript SDKs for Antelope/Vexanium, Wisp Wallet, EVM, and Solana. -The SDK keeps each chain integration separate. VEX Native applications use WharfKit SessionKit and the Wisp wallet plugin. EVM applications use the standard EIP-1193 provider interface, with EIP-6963 discovery when a wallet supports it. +**Created by Gilang Ramadan** · Copyright © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI · MIT. -## Packages +## Native Antelope v1 -| Package | Use it for | -| --- | --- | -| `@windstack/core` | Provider contract, errors, events, dApp metadata, and shared types | -| `@windstack/evm` | EIP-1193 requests and EIP-6963 wallet discovery | -| `@windstack/solana` | Wisp-compatible Solana provider requests | -| `@windstack/vexanium` | Vexanium provider access, chain metadata, VSR, and explorer helpers | -| `@windstack/wallet-plugin-wisp` | Wisp integration for WharfKit SessionKit | -| `@windstack/session` | An optional facade for apps that need more than one chain family | - -All published packages are ESM and require Node.js 18 or newer. +The new Antelope stack is built from protocol specifications and Web-standard APIs; it is **not a WharfKit fork**. -## VEX Native +| 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 | -Install the Vexanium package, the wallet plugin, and WharfKit: +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. ```bash -npm install @windstack/vexanium @windstack/wallet-plugin-wisp \ - @wharfkit/session @wharfkit/antelope @wharfkit/signing-request -``` - -```ts -import { SessionKit } from "@wharfkit/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 }], - walletPlugins: [new WispWalletPlugin()], -}); - -const { session } = await sessionKit.login(); - -await session.transact({ - action: { - account: "vex.token", - name: "transfer", - authorization: [session.permissionLevel], - data: { - from: session.actor, - to: "receiver", - quantity: "1.0000 VEX", - memo: "", - }, - }, -}); +npm install @windstack/antelope @windstack/session ``` -SessionKit resolves the signer, ABI data, TAPOS, and transaction. `WispWalletPlugin` sends the resulting serialized bytes to `vex_signTransaction` without rebuilding the transaction. - -### Direct provider access - -Use the lower-level client when SessionKit is not responsible for the flow: - -```ts -import { createVexaniumClient, vexNative } from "@windstack/vexanium"; - -const client = await createVexaniumClient({ - dapp: { - name: "My Vexanium App", - url: "https://app.example", - icon: "https://app.example/icon.png", - }, -}); - -let accounts = await client.getAccounts(); -if (accounts.length === 0) { - accounts = await client.connect({ chainId: vexNative.chainId }); -} -``` - -`getAccounts()` is a silent read. `connect()` may open the wallet approval screen. - -### Portable requests - -Vexanium Signing Requests use the `vsr:` URI scheme. The payload is handled by WharfKit's Signing Request library. Existing `esr:` input is validated by WharfKit and forwarded without rewriting the URI or encoded transaction. - ```ts -import { createSigningRequest, vexNative } from "@windstack/vexanium"; - -const request = await createSigningRequest({ - chainId: vexNative.chainId, - broadcast: true, - action: { - account: "vex.token", - name: "transfer", - authorization: [{ actor: "alice", permission: "active" }], - data: { - from: "alice", - to: "bob", - quantity: "1.0000 VEX", - memo: "", - }, - }, -}); +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"]); ``` -Use VSR for QR codes, links, and other portable requests. Do not wrap a transaction that SessionKit has already resolved; the wallet plugin signs those exact bytes directly. +## Existing packages -## VEX EVM +`@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. -VEX EVM uses chain ID `6736` (`0x1a50`) and 18 decimals for its native VEX balance. Its public JSON-RPC endpoint is `https://api.windcrypto.com/rpc`, and its explorer starts at `https://explorer.windcrypto.com/evm`. Indexed EVM data is available at `https://api.windcrypto.com/v3/evm`, including the live stats endpoint at `https://api.windcrypto.com/v3/evm/stats`. - -Use `vexEvm.rpcUrl` for JSON-RPC and `wallet_addEthereumChain`. The REST stats endpoint is for indexed network statistics, not provider requests. - -For an injected EVM wallet: - -```ts -import { createEVMClient } from "@windstack/evm"; - -const client = await createEVMClient(); -const accounts = await client.connect(); -const chainId = await client.getChainId(); +## Development -if (chainId !== "0x1a50") { - await client.switchChain("0x1a50"); -} +```bash +npm install +npm run validate:native +npm run validate ``` -## Provider Contract +## Publish native packages from VPS -Vexanium wallets expose `window.vexanium` and identify themselves through `providerInfo`. The SDK checks the provider shape, negotiates protocol version `1.x`, and verifies declared capabilities before connect or signing calls. - -`WISP_PROVIDER_CONTRACT` from `@windstack/core` is the runtime single source for shared Wisp/Vexanium and EVM provider identity, methods, events, capabilities, and chain identifiers. The EVM and Vexanium packages derive their exported constants from that object. - -The matching machine-readable specification is stored at [`specs/wisp-provider-contract.json`](./specs/wisp-provider-contract.json). Validation fails if the JSON specification, core export, or package constants drift apart. Wisp Wallet independently synchronizes its local runtime contract from this specification. - -The full Vexanium wallet-facing contract is documented in [VEXANIUM-PROVIDER-V1.md](./VEXANIUM-PROVIDER-V1.md). - -## Development +Authenticate to npm first (`npm whoami`). Then: ```bash -npm ci -npm run validate +npm install +npm run release:dry-run +npm run release:npm ``` -`validate` runs a clean TypeScript build, provider/signing regression tests, canonical provider specification checks, and an npm package dry run for every workspace. - -WharfKit is kept at its latest published package versions. npm currently reports a low-severity advisory in WharfKit's transitive `elliptic` dependency; there is no patched WharfKit release to upgrade to. The suggested npm remediation is an incompatible downgrade and is intentionally not applied. +`release:npm` publishes only the seven native packages, in dependency order. ## License -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT. Created by Gilang Ramadan; copyright PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/package.json b/package.json index 25ee4a5..ac914d9 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,30 @@ { "name": "windstack-sdk", - "version": "0.6.2", + "version": "1.0.0", "private": true, "type": "module", - "workspaces": [ - "packages/*" - ], + "workspaces": ["packages/*"], "scripts": { "clean": "rm -rf packages/*/dist packages/*/*.tsbuildinfo", - "build": "tsc -b packages/core packages/evm packages/solana packages/vexanium packages/wallet-plugin-wisp packages/session", - "typecheck": "tsc -b packages/core packages/evm packages/solana packages/vexanium packages/wallet-plugin-wisp packages/session --pretty", + "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: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", - "validate": "npm run clean && npm run typecheck && npm run build && npm test && npm run pack:dry-run", - "prepublishOnly": "npm run validate", - "test:signing": "node scripts/test-signing.mjs", - "test:provider": "node scripts/test-provider-contract.mjs && node scripts/test-provider-spec.mjs", - "test:provider-spec": "node scripts/test-provider-spec.mjs" - }, - "devDependencies": { - "typescript": "^7.0.2" - }, - "engines": { - "node": ">=18" + "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", + "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" }, + "devDependencies": { "typescript": "^7.0.2" }, + "engines": { "node": ">=20.19.0" }, "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", - "description": "TypeScript SDK packages for Wind Stack, Wisp Wallet, Wind Explorer, and Vexanium dApps.", + "author": "Gilang Ramadan", + "contributors": ["PT WIND KRIPTOGRAFI TEKNOLOGI"], + "description": "Modern TypeScript SDK packages for WindStack, Wisp Wallet, Vexanium and Antelope applications.", "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" } } diff --git a/packages/abi/LICENSE b/packages/abi/LICENSE new file mode 100644 index 0000000..00c2398 --- /dev/null +++ b/packages/abi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/abi/README.md b/packages/abi/README.md new file mode 100644 index 0000000..df27806 --- /dev/null +++ b/packages/abi/README.md @@ -0,0 +1,7 @@ +# @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. + +Created by **Gilang Ramadan**. + +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/abi/package.json b/packages/abi/package.json new file mode 100644 index 0000000..c608efe --- /dev/null +++ b/packages/abi/package.json @@ -0,0 +1,18 @@ +{ + "name": "@windstack/abi", + "version": "1.0.0", + "description": "Pure TypeScript Antelope ABI serializer and binary codec for WindStack.", + "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" }, + "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" } +} diff --git a/packages/abi/src/index.ts b/packages/abi/src/index.ts new file mode 100644 index 0000000..19b543f --- /dev/null +++ b/packages/abi/src/index.ts @@ -0,0 +1,183 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { PublicKey, Signature } from "@windstack/crypto"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); +const NAME_CHARS = ".12345abcdefghijklmnopqrstuvwxyz"; + +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 Abi = { + version: string; + types?: AbiTypeDef[]; + structs?: AbiStruct[]; + actions?: AbiAction[]; + tables?: Array<{ name: string; index_type: string; key_names?: string[]; key_types?: string[]; type: string }>; + variants?: AbiVariant[]; +}; + +export function bytesToHex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.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"); + 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"); + 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"); + value |= BigInt(char); + } else { + value |= BigInt(char) << BigInt(64 - 5 * (i + 1)); + } + } + return value; +} + +export function bigIntToName(value: bigint): string { + let tmp = 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; + } + 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); } +} + +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()); } +} + +function parseAsset(value: string): { amount: bigint; precision: number; symbol: string } { + const match = /^(-?)(\d+)(?:\.(\d+))? ([A-Z]{1,7})$/.exec(value); + if (!match) throw new TypeError(`Invalid asset: ${value}`); + const fraction = match[3] ?? ""; + const amount = BigInt(`${match[1]}${match[2]}${fraction}`); + 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 }; } + +export class AbiSerializer { + readonly abi: Abi; + readonly #aliases = new Map(); + readonly #structs = new Map(); + readonly #variants = new Map(); + constructor(abi: Abi) { + 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); } + #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; } + 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; } + 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; } + 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}`); + } + } + #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; + 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; } + 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) }; } + 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}`); + } + } +} diff --git a/packages/abi/tsconfig.json b/packages/abi/tsconfig.json new file mode 100644 index 0000000..8d0d56a --- /dev/null +++ b/packages/abi/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../crypto" }] +} diff --git a/packages/account/LICENSE b/packages/account/LICENSE new file mode 100644 index 0000000..00c2398 --- /dev/null +++ b/packages/account/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/account/README.md b/packages/account/README.md new file mode 100644 index 0000000..b9b3e19 --- /dev/null +++ b/packages/account/README.md @@ -0,0 +1,7 @@ +# @windstack/account + +Account reads, token balances, transfer builders, and `eosio` system-action helpers. + +Created by **Gilang Ramadan**. + +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/account/package.json b/packages/account/package.json new file mode 100644 index 0000000..d791e00 --- /dev/null +++ b/packages/account/package.json @@ -0,0 +1,18 @@ +{ + "name": "@windstack/account", + "version": "1.0.0", + "description": "Antelope account, balance, permission and system-action helpers for WindStack.", + "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" }, + "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" } +} diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts new file mode 100644 index 0000000..312c0b9 --- /dev/null +++ b/packages/account/src/index.ts @@ -0,0 +1,25 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { AbiCache, Contract, type ContractAction } from "@windstack/contract"; +import { RpcClient } from "@windstack/rpc"; + +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); } +} diff --git a/packages/account/tsconfig.json b/packages/account/tsconfig.json new file mode 100644 index 0000000..e45ac57 --- /dev/null +++ b/packages/account/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../contract" }, { "path": "../rpc" }] +} diff --git a/packages/antelope/LICENSE b/packages/antelope/LICENSE new file mode 100644 index 0000000..00c2398 --- /dev/null +++ b/packages/antelope/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/antelope/README.md b/packages/antelope/README.md new file mode 100644 index 0000000..3d21d03 --- /dev/null +++ b/packages/antelope/README.md @@ -0,0 +1,19 @@ +# @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. + +It is built from Antelope protocol formats, not cloned from WharfKit. The new dependency graph has no `@wharfkit/*`, `elliptic`, or `bn.js`. + +Created by **Gilang Ramadan**. + +```ts +import { AntelopeClient, PrivateKey, PrivateKeySigner } from "@windstack/antelope"; + +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 }); +``` + +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/antelope/package.json b/packages/antelope/package.json new file mode 100644 index 0000000..7cb4118 --- /dev/null +++ b/packages/antelope/package.json @@ -0,0 +1,18 @@ +{ + "name": "@windstack/antelope", + "version": "1.0.0", + "description": "Modern zero-WharfKit Antelope SDK: transactions, TAPOS, signing, contracts, accounts and RPC.", + "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/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", + "repository": { "type": "git", "url": "git+https://github.com/windvex/windstack-sdk.git", "directory": "packages/antelope" }, + "publishConfig": { "access": "public" }, + "engines": { "node": ">=20.19.0" } +} diff --git a/packages/antelope/src/index.ts b/packages/antelope/src/index.ts new file mode 100644 index 0000000..76c6a30 --- /dev/null +++ b/packages/antelope/src/index.ts @@ -0,0 +1,95 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { AccountClient } from "@windstack/account"; +import { BinaryWriter, hexToBytes, bytesToHex } from "@windstack/abi"; +import { AbiCache, Contract, type ContractAction } from "@windstack/contract"; +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"; +export * from "@windstack/abi"; +export * from "@windstack/contract"; +export * from "@windstack/crypto"; +export * from "@windstack/rpc"; + +export type Action = ContractAction; +export type Transaction = { + expiration: string; + ref_block_num: number; + ref_block_prefix: number; + max_net_usage_words: number; + max_cpu_usage_ms: number; + delay_sec: number; + context_free_actions: Action[]; + actions: Action[]; + transaction_extensions: Array<[number, 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; + const bytes = cryptoHexToBytes(block.id); + 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 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.writeVarBytes(hexToBytes(action.data)); +} +export function serializeTransaction(transaction: Transaction): Uint8Array { + 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)); } + 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"); + return sha256Digest(concatBytes(id, serializedTransaction, contextFreeDataHash)); +} + +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)); + } +} + +export type AntelopeClientOptions = RpcClientOptions & { abiCache?: AbiCache }; +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); } + async transact>(args: TransactArgs): Promise> { + if (!args.actions.length) throw new TypeError("Transaction must include at least one action"); + const info = await this.rpc.getInfo(args.signal); + 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 serializedTransaction = serializeTransaction(transaction); + 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); + return { transaction, serializedTransaction, signatures, response }; + } +} diff --git a/packages/antelope/tsconfig.json b/packages/antelope/tsconfig.json new file mode 100644 index 0000000..0123358 --- /dev/null +++ b/packages/antelope/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../crypto" }, { "path": "../abi" }, { "path": "../rpc" }, { "path": "../contract" }, { "path": "../account" }] +} diff --git a/packages/contract/LICENSE b/packages/contract/LICENSE new file mode 100644 index 0000000..00c2398 --- /dev/null +++ b/packages/contract/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/contract/README.md b/packages/contract/README.md new file mode 100644 index 0000000..d2a425f --- /dev/null +++ b/packages/contract/README.md @@ -0,0 +1,7 @@ +# @windstack/contract + +ABI-aware contract actions and table queries with a TTL ABI cache. + +Created by **Gilang Ramadan**. + +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/contract/package.json b/packages/contract/package.json new file mode 100644 index 0000000..7acc8b1 --- /dev/null +++ b/packages/contract/package.json @@ -0,0 +1,18 @@ +{ + "name": "@windstack/contract", + "version": "1.0.0", + "description": "ABI-aware Antelope smart-contract client for WindStack.", + "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" }, + "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" } +} diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts new file mode 100644 index 0000000..3a3cf46 --- /dev/null +++ b/packages/contract/src/index.ts @@ -0,0 +1,43 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { AbiSerializer, bytesToHex, 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 AuthorizationInput = PermissionLevel | `${string}@${string}`; + +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(); } +} + +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); +} + +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; } + 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; + } + async action(name: string, data: unknown, authorization: AuthorizationInput[] = [], signal?: AbortSignal): Promise { + const serializer = new AbiSerializer(await this.getAbi(false, signal)); + 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> { + return this.rpc.getTableRows({ code: this.account, scope, table, ...options }, signal); + } +} diff --git a/packages/contract/tsconfig.json b/packages/contract/tsconfig.json new file mode 100644 index 0000000..c4b03dc --- /dev/null +++ b/packages/contract/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../abi" }, { "path": "../rpc" }] +} diff --git a/packages/crypto/LICENSE b/packages/crypto/LICENSE new file mode 100644 index 0000000..00c2398 --- /dev/null +++ b/packages/crypto/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/crypto/README.md b/packages/crypto/README.md new file mode 100644 index 0000000..46068c4 --- /dev/null +++ b/packages/crypto/README.md @@ -0,0 +1,20 @@ +# @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. + +Created by **Gilang Ramadan**. + +```bash +npm install @windstack/crypto +``` + +```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())); +``` + +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/crypto/package.json b/packages/crypto/package.json new file mode 100644 index 0000000..eb65411 --- /dev/null +++ b/packages/crypto/package.json @@ -0,0 +1,18 @@ +{ + "name": "@windstack/crypto", + "version": "1.0.0", + "description": "Modern Antelope K1/R1 keys and signatures for WindStack, 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" }, + "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" } +} diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts new file mode 100644 index 0000000..2902e04 --- /dev/null +++ b/packages/crypto/src/index.ts @@ -0,0 +1,174 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * 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 { ripemd160 } from "@noble/hashes/legacy.js"; + +export type KeyType = "K1" | "R1"; +const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; +const encoder = new TextEncoder(); + +function assertBytes(value: Uint8Array, length: number, label: string): void { + if (!(value instanceof Uint8Array) || value.length !== length) throw new TypeError(`${label} must be ${length} bytes`); +} + +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; } + return out; +} + +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 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"); + 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; +} + +function base58Encode(bytes: Uint8Array): string { + 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"; +} + +function base58Decode(value: string): Uint8Array { + if (!value) throw new TypeError("Base58 value cannot be empty"); + let num = 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); + } + 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); } + return Uint8Array.from(bytes); +} + +function ripemdChecksum(data: Uint8Array, suffix?: string): Uint8Array { + const payload = suffix ? concatBytes(data, encoder.encode(suffix)) : data; + return ripemd160(payload).slice(0, 4); +} + +function encodeModern(data: Uint8Array, type: KeyType): string { + return base58Encode(concatBytes(data, ripemdChecksum(data, type))); +} + +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"); + 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); } + 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 (value.startsWith("EOS")) { + const decoded = base58Decode(value.slice(3)); + const data = decoded.slice(0, -4); + 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)}`; } + 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); } +} + +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(); + } + 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)); + } + toBytes(): Uint8Array { return this.#data.slice(); } + toString(): string { return `SIG_${this.type}_${encodeModern(this.#data, this.type)}`; } + 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" }); + } + 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 })); + } +} + +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); } + 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)); + 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"); + 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)}`; } + 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)); } + 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); + } +} diff --git a/packages/crypto/tsconfig.json b/packages/crypto/tsconfig.json new file mode 100644 index 0000000..ac7d40a --- /dev/null +++ b/packages/crypto/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "include": ["src/**/*.ts"] +} diff --git a/packages/rpc/LICENSE b/packages/rpc/LICENSE new file mode 100644 index 0000000..00c2398 --- /dev/null +++ b/packages/rpc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/rpc/README.md b/packages/rpc/README.md new file mode 100644 index 0000000..8811a35 --- /dev/null +++ b/packages/rpc/README.md @@ -0,0 +1,7 @@ +# @windstack/rpc + +Typed Antelope `/v1/chain/*` client with endpoint failover, timeout, AbortSignal, injected `fetch`, and structured RPC errors. Uses Web APIs only. + +Created by **Gilang Ramadan**. + +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/rpc/package.json b/packages/rpc/package.json new file mode 100644 index 0000000..952f16b --- /dev/null +++ b/packages/rpc/package.json @@ -0,0 +1,17 @@ +{ + "name": "@windstack/rpc", + "version": "1.0.0", + "description": "Typed, fault-tolerant Antelope node RPC client using Web-standard fetch and AbortSignal.", + "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" }, + "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" } +} diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts new file mode 100644 index 0000000..a1a7ed4 --- /dev/null +++ b/packages/rpc/src/index.ts @@ -0,0 +1,69 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * 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 TableRowsResponse = { rows: T[]; more: boolean | string; next_key?: string }; + +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; } +} + +export class RpcClient { + readonly endpoints: readonly string[]; + 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"); + 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)); + } + async request(path: string, body: unknown = {}, signal?: AbortSignal): Promise { + 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 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 }); } + 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 text = await response.text(); + let payload: unknown = null; + 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); + } + this.#cursor = (this.#cursor + attempt) % this.endpoints.length; + return payload as T; + } catch (error) { + lastError = error; + if (signal?.aborted) throw error; + } 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); } +} diff --git a/packages/rpc/tsconfig.json b/packages/rpc/tsconfig.json new file mode 100644 index 0000000..ac7d40a --- /dev/null +++ b/packages/rpc/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, + "include": ["src/**/*.ts"] +} diff --git a/packages/session/README.md b/packages/session/README.md index bfa5d1a..cfdf23f 100644 --- a/packages/session/README.md +++ b/packages/session/README.md @@ -1,33 +1,15 @@ # @windstack/session -Optional application-level session facade for EVM, Solana, and Vexanium providers. +WindStack native Antelope session orchestration. It provides a small SessionKit-style abstraction over `@windstack/antelope` without WharfKit. -Use this package only when one application needs a common `connect`, `invoke`, and `disconnect` interface across chain families. A VEX Native-only app should use WharfKit SessionKit with `@windstack/wallet-plugin-wisp` instead. - -```bash -npm install @windstack/session -``` +Created by **Gilang Ramadan**. ```ts -import { createWispSessionClient } from "@windstack/session"; - -const client = await createWispSessionClient({ - dapp: { name: "My App", url: "https://app.example" }, -}); +import { SessionKit } from "@windstack/session"; -const session = await client.connect([ - "eip155:6736", - "antelope:f9f432b1851b5c179d2091a96f593aa", -]); - -await client.invoke({ - scope: "eip155:6736", - request: { method: "eth_chainId" }, -}); +const kit = new SessionKit({ chains: [{ id: "...", url: "https://api.example" }], walletPlugins: [myWalletPlugin] }); +const session = await kit.login(); +await session.transact({ actions: [action] }); ``` -A session accepts at most one scope per chain family. The EVM scope must match the provider's active chain; the facade will not report an account as authorized on a different chain. - -## License - -MIT, PT WIND KRIPTOGRAFI TEKNOLOGI. +MIT © 2026 PT WIND KRIPTOGRAFI TEKNOLOGI. diff --git a/packages/session/package.json b/packages/session/package.json index 130c4a2..bed5a5e 100644 --- a/packages/session/package.json +++ b/packages/session/package.json @@ -1,62 +1,18 @@ { "name": "@windstack/session", - "version": "0.6.2", - "description": "Optional unified multichain session facade for Wind Stack EVM, Solana, and Vexanium clients.", + "version": "1.0.0", + "description": "Native Antelope session and wallet-plugin orchestration for WindStack.", "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", - "typecheck": "tsc -p tsconfig.json --noEmit false", - "prepack": "npm run build" - }, - "dependencies": { - "@windstack/core": "0.6.2", - "@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" } }, + "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, - "keywords": [ - "windstack", - "windcrypto", - "wind-crypto", - "wisp-wallet", - "wisp", - "session", - "vexanium", - "evm", - "solana", - "antelope" - ], "license": "MIT", - "author": "PT WIND KRIPTOGRAFI TEKNOLOGI", - "homepage": "https://github.com/windvex/windstack-sdk/tree/main/packages/session#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/windvex/windstack-sdk.git", - "directory": "packages/session" - }, - "bugs": { - "url": "https://github.com/windvex/windstack-sdk/issues" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "engines": { - "node": ">=18" - } + "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" } } diff --git a/packages/session/src/index.ts b/packages/session/src/index.ts index 3c62ec4..d933ee9 100644 --- a/packages/session/src/index.ts +++ b/packages/session/src/index.ts @@ -1,3 +1,7 @@ -export { createWispSessionClient } from "./client.js"; -export { createSessionId, isEVMScope, isSolanaScope, isVexaniumScope } from "./scopes.js"; -export type { WispInvokeArgs, WispSessionClient, WispSessionClientOptions } from "./types.js"; +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +export * from "./native.js"; diff --git a/packages/session/src/native.ts b/packages/session/src/native.ts new file mode 100644 index 0000000..9623ab8 --- /dev/null +++ b/packages/session/src/native.ts @@ -0,0 +1,58 @@ +/** + * WindStack Antelope SDK + * Created by Gilang Ramadan + * Copyright (c) 2026 PT WIND KRIPTOGRAFI TEKNOLOGI + * SPDX-License-Identifier: MIT + */ +import { AntelopeClient, type Action, type Signer, type TransactResult } from "@windstack/antelope"; + +export type SessionChain = { id: string; url: string }; +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 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); } +} + +export type SessionKitOptions = { chains: SessionChain[]; walletPlugins: WalletPlugin[]; appName?: string; storage?: SessionStorage; storageKey?: string }; +export class SessionKit { + 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"; + } + 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 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; + } + 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 }; + } +} diff --git a/packages/session/tsconfig.json b/packages/session/tsconfig.json index 5fffc97..31f2eee 100644 --- a/packages/session/tsconfig.json +++ b/packages/session/tsconfig.json @@ -1,15 +1,6 @@ { "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "dist", - "tsBuildInfoFile": "./tsconfig.tsbuildinfo" - }, - "include": ["src/**/*.ts"], - "references": [ - { "path": "../core" }, - { "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-native-antelope.mjs b/scripts/test-native-antelope.mjs new file mode 100644 index 0000000..b8a57ad --- /dev/null +++ b/scripts/test-native-antelope.mjs @@ -0,0 +1,41 @@ +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"; + +const privateKey = PrivateKey.fromBytes("K1", Uint8Array.from({ length: 32 }, (_, index) => index === 31 ? 1 : 0)); +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(PrivateKey.fromString(privateKey.toString()).toString(), privateKey.toString()); +assert.equal(bigIntToName(nameToBigInt("vex.token")), "vex.token"); + +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 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 chainId = "00".repeat(32); +const blockId = "00".repeat(32); +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" } }); +}; +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]) }); +assert.equal(result.signatures.length, 1); +assert.equal(result.response.transaction_id.length, 64); +console.log("native Antelope SDK tests passed"); diff --git a/tsconfig.base.json b/tsconfig.base.json index 7102b31..41f2a5e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,10 +1,7 @@ { "compilerOptions": { - "target": "ES2020", - "lib": [ - "ES2020", - "DOM" - ], + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, @@ -19,24 +16,18 @@ "noUnusedLocals": true, "noUnusedParameters": true, "paths": { - "@windstack/core": [ - "./packages/core/dist/index.d.ts" - ], - "@windstack/evm": [ - "./packages/evm/dist/index.d.ts" - ], - "@windstack/solana": [ - "./packages/solana/dist/index.d.ts" - ], - "@windstack/vexanium": [ - "./packages/vexanium/dist/index.d.ts" - ], - "@windstack/session": [ - "./packages/session/dist/index.d.ts" - ], - "@windstack/wallet-plugin-wisp": [ - "./packages/wallet-plugin-wisp/dist/index.d.ts" - ] + "@windstack/core": ["./packages/core/dist/index.d.ts"], + "@windstack/evm": ["./packages/evm/dist/index.d.ts"], + "@windstack/solana": ["./packages/solana/dist/index.d.ts"], + "@windstack/vexanium": ["./packages/vexanium/dist/index.d.ts"], + "@windstack/wallet-plugin-wisp": ["./packages/wallet-plugin-wisp/dist/index.d.ts"], + "@windstack/crypto": ["./packages/crypto/dist/index.d.ts"], + "@windstack/abi": ["./packages/abi/dist/index.d.ts"], + "@windstack/rpc": ["./packages/rpc/dist/index.d.ts"], + "@windstack/contract": ["./packages/contract/dist/index.d.ts"], + "@windstack/account": ["./packages/account/dist/index.d.ts"], + "@windstack/antelope": ["./packages/antelope/dist/index.d.ts"], + "@windstack/session": ["./packages/session/dist/index.d.ts"] } } } diff --git a/tsconfig.json b/tsconfig.json index 5df7623..51711e7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,12 @@ { "path": "./packages/core" }, { "path": "./packages/evm" }, { "path": "./packages/solana" }, + { "path": "./packages/crypto" }, + { "path": "./packages/abi" }, + { "path": "./packages/rpc" }, + { "path": "./packages/contract" }, + { "path": "./packages/account" }, + { "path": "./packages/antelope" }, { "path": "./packages/vexanium" }, { "path": "./packages/wallet-plugin-wisp" }, { "path": "./packages/session" } From 555c374082760ca87248a5ff940253c97f8ace16 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 06:49:23 +0700 Subject: [PATCH 2/6] fix(antelope): avoid codec export collisions --- packages/antelope/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/antelope/src/index.ts b/packages/antelope/src/index.ts index 76c6a30..bcfbad6 100644 --- a/packages/antelope/src/index.ts +++ b/packages/antelope/src/index.ts @@ -13,7 +13,8 @@ import { RpcClient, type GetBlockResponse, type RpcClientOptions } from "@windst export * from "@windstack/account"; export * from "@windstack/abi"; export * from "@windstack/contract"; -export * from "@windstack/crypto"; +export { PrivateKey, PublicKey, Signature, concatBytes, sha256Digest } from "@windstack/crypto"; +export type { KeyType } from "@windstack/crypto"; export * from "@windstack/rpc"; export type Action = ContractAction; From ba49c0a379a3c1de9425312b7c52397444b687b0 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 06:51:02 +0700 Subject: [PATCH 3/6] feat(session): add lazy legacy compatibility facade --- packages/session/src/compat.ts | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 packages/session/src/compat.ts diff --git a/packages/session/src/compat.ts b/packages/session/src/compat.ts new file mode 100644 index 0000000..ed06854 --- /dev/null +++ b/packages/session/src/compat.ts @@ -0,0 +1,128 @@ +/** + * 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; +} + +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}`; +} + +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; }, + }; +} From aee9de5a1458f9fa0948f9c59bcc5e39cd85fdd3 Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 06:51:13 +0700 Subject: [PATCH 4/6] fix(session): export compatibility API --- packages/session/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/session/src/index.ts b/packages/session/src/index.ts index d933ee9..0e5a8bd 100644 --- a/packages/session/src/index.ts +++ b/packages/session/src/index.ts @@ -5,3 +5,4 @@ * SPDX-License-Identifier: MIT */ export * from "./native.js"; +export * from "./compat.js"; From 490649894fc029e379b5ec6b47387ac5c91baf7d Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 06:51:22 +0700 Subject: [PATCH 5/6] build(session): compile compatibility facade --- packages/session/tsconfig.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/session/tsconfig.json b/packages/session/tsconfig.json index 31f2eee..f7dc5a6 100644 --- a/packages/session/tsconfig.json +++ b/packages/session/tsconfig.json @@ -1,6 +1,11 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "./tsconfig.tsbuildinfo" }, - "include": ["src/index.ts", "src/native.ts"], - "references": [{ "path": "../antelope" }] + "include": ["src/index.ts", "src/native.ts", "src/compat.ts"], + "references": [ + { "path": "../antelope" }, + { "path": "../evm" }, + { "path": "../solana" }, + { "path": "../vexanium" } + ] } From 05a6df1d49a370d8d8708eba945082b0acb72ebc Mon Sep 17 00:00:00 2001 From: Wind Crypto | Vexanium <0xgvexa@gmail.com> Date: Mon, 7 Sep 2026 06:51:35 +0700 Subject: [PATCH 6/6] chore(session): declare optional legacy peers --- packages/session/package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/session/package.json b/packages/session/package.json index bed5a5e..029b6bd 100644 --- a/packages/session/package.json +++ b/packages/session/package.json @@ -9,6 +9,16 @@ "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" + }, + "peerDependenciesMeta": { + "@windstack/evm": { "optional": true }, + "@windstack/solana": { "optional": true }, + "@windstack/vexanium": { "optional": true } + }, "sideEffects": false, "license": "MIT", "author": "Gilang Ramadan",