@blockula/sdk 0.0.0-stage β†’ 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jack Dishman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,30 @@
1
- # Temporary Holding Version
1
+ # @blockula/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ πŸ§› TypeScript SDK for **Blockula**, a post-quantum blockchain. It covers ML-DSA-65 (FIPS 204) wallets, `.boo` names, transfers, multi-key accounts, agent session keys, and passkey-unlocked wallets.
4
+
5
+ ```bash
6
+ npm install @blockula/sdk
7
+ ```
8
+
9
+ ```ts
10
+ import { Client, Wallet } from "@blockula/sdk";
11
+
12
+ const wallet = Wallet.generate(new Client("http://localhost:7070"));
13
+ await wallet.faucet(); // test chains: 100 FANG
14
+ await wallet.registerName("vlad"); // vlad.boo
15
+ await wallet.send("alice.boo", "1.5", { memo: "trick or treat" });
16
+
17
+ // Let an agent spend up to 10 FANG/day from this account (enforced on-chain)
18
+ await wallet.addSessionKey(agentKeyId, "claude", { limit: "10", periodBlocks: 86_400, expiresInBlocks: 30 * 86_400 });
19
+ ```
20
+
21
+ In the browser, a passkey unlocks the wallet. There's no seed phrase, and the passkey's (non-quantum-safe) signature is never used on-chain. Its PRF output derives the ML-DSA key:
22
+
23
+ ```ts
24
+ import { createPasskeyWallet, unlockPasskeyWallet } from "@blockula/sdk/passkey";
25
+ const wallet = await createPasskeyWallet({ username: "vlad" });
26
+ ```
27
+
28
+ Amounts are FANG strings (`"1.5"`) or bigint drops (1 FANG = 10⁹ drops). Every transaction result includes a plain-English `summary`.
29
+
30
+ ⚠️ Early and unaudited: test chains only.
@@ -0,0 +1,79 @@
1
+ import { type Action, type Tx, type UnsignedTx } from "./tx.js";
2
+ export * from "./tx.js";
3
+ export declare const DEFAULT_RPC = "http://localhost:7070";
4
+ export declare function addressFromPublicKey(publicKey: Uint8Array): string;
5
+ /** A key's id: BLAKE3(public key) as hex. Same 32 bytes as inside the `boo1…` address it derives. */
6
+ export declare function keyId(publicKey: Uint8Array): string;
7
+ export declare function isAddress(s: string): boolean;
8
+ /**
9
+ * Wallet = one ML-DSA-65 key (from a 32-byte seed, FIPS 204 ΞΎ) acting for one account.
10
+ * By default the account is the key's own address. Call `actAs(account)` to use a key that
11
+ * was added to another account (a second device, or an agent session key).
12
+ */
13
+ export declare class Wallet {
14
+ readonly seed: Uint8Array;
15
+ client: Client;
16
+ readonly publicKey: Uint8Array;
17
+ /** This key's own address. */
18
+ readonly address: string;
19
+ /** The account this wallet acts for (defaults to `address`). */
20
+ account: string;
21
+ /** WebAuthn credential id, when the wallet was unlocked by a passkey. Not secret. */
22
+ credentialId?: Uint8Array;
23
+ private readonly secretKey;
24
+ private constructor();
25
+ get keyId(): string;
26
+ /** Act for another account this key has been authorized on. Accepts a name or address. */
27
+ actAs(account: string): Promise<this>;
28
+ static generate(client?: Client): Wallet;
29
+ static fromSeed(seed: string | Uint8Array, client?: Client): Wallet;
30
+ /**
31
+ * Derive a wallet from a WebAuthn PRF output. The passkey never signs transactions
32
+ * (its P-256 signatures aren't quantum-safe); it only unlocks this ML-DSA key.
33
+ */
34
+ static fromPasskeyPrf(prfOutput: Uint8Array, client?: Client): Wallet;
35
+ get seedHex(): string;
36
+ sign(tx: UnsignedTx): Tx;
37
+ build(action: Action, memo?: string): Promise<Tx>;
38
+ /** Send FANG to a `.boo` name or `boo1…` address. `amount` is in FANG ("1.5") or bigint drops. */
39
+ send(to: string, amount: string | number | bigint, opts?: {
40
+ memo?: string;
41
+ wait?: boolean;
42
+ }): Promise<any>;
43
+ registerName(name: string, opts?: {
44
+ wait?: boolean;
45
+ }): Promise<any>;
46
+ /** Authorize another device's key (e.g. a second passkey) with full control. */
47
+ addOwnerKey(keyIdHex: string, label: string): Promise<any>;
48
+ /**
49
+ * Let an agent/app key spend from this account, transfers only, within a limit.
50
+ * Example: `addSessionKey(agentKeyId, "claude", { limit: "10", periodBlocks: 86_400, expiresInBlocks: 30 * 86_400 })`.
51
+ */
52
+ addSessionKey(keyIdHex: string, label: string, opts: {
53
+ limit: string | number | bigint;
54
+ periodBlocks: number;
55
+ expiresInBlocks: number;
56
+ }): Promise<any>;
57
+ removeKey(keyIdHex: string): Promise<any>;
58
+ balance(): Promise<string>;
59
+ /** Dev/test chains only: drip 100 FANG to this wallet. */
60
+ faucet(wait?: boolean): Promise<any>;
61
+ }
62
+ export declare class Client {
63
+ readonly rpc: string;
64
+ constructor(rpc?: string);
65
+ private call;
66
+ status(): Promise<any>;
67
+ blocks(limit?: number): Promise<any>;
68
+ block(heightOrHash: number | string): Promise<any>;
69
+ tx(id: string): Promise<any>;
70
+ account(addressOrName: string): Promise<any>;
71
+ name(name: string): Promise<any>;
72
+ /** Accounts a key is authorized on (its own address, plus any it was added to). */
73
+ keyAccounts(keyIdHex: string): Promise<any>;
74
+ search(q: string): Promise<any>;
75
+ resolve(nameOrAddress: string): Promise<string>;
76
+ submit(tx: Tx, wait?: boolean): Promise<any>;
77
+ faucet(to: string, wait?: boolean): Promise<any>;
78
+ waitForTx(id: string, timeoutMs?: number): Promise<any>;
79
+ }
package/dist/index.js ADDED
@@ -0,0 +1,167 @@
1
+ import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
2
+ import { blake3 } from "@noble/hashes/blake3.js";
3
+ import { bytesToHex, hexToBytes, randomBytes, utf8ToBytes } from "@noble/hashes/utils.js";
4
+ import { bech32m } from "@scure/base";
5
+ import { minFee, parseFang, signingBytes } from "./tx.js";
6
+ export * from "./tx.js";
7
+ export const DEFAULT_RPC = "http://localhost:7070";
8
+ export function addressFromPublicKey(publicKey) {
9
+ return bech32m.encode("boo", bech32m.toWords(blake3(publicKey)));
10
+ }
11
+ /** A key's id: BLAKE3(public key) as hex. Same 32 bytes as inside the `boo1…` address it derives. */
12
+ export function keyId(publicKey) {
13
+ return bytesToHex(blake3(publicKey));
14
+ }
15
+ export function isAddress(s) {
16
+ try {
17
+ const { prefix, words } = bech32m.decode(s);
18
+ return prefix === "boo" && bech32m.fromWords(words).length === 32;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ /**
25
+ * Wallet = one ML-DSA-65 key (from a 32-byte seed, FIPS 204 ΞΎ) acting for one account.
26
+ * By default the account is the key's own address. Call `actAs(account)` to use a key that
27
+ * was added to another account (a second device, or an agent session key).
28
+ */
29
+ export class Wallet {
30
+ seed;
31
+ client;
32
+ publicKey;
33
+ /** This key's own address. */
34
+ address;
35
+ /** The account this wallet acts for (defaults to `address`). */
36
+ account;
37
+ /** WebAuthn credential id, when the wallet was unlocked by a passkey. Not secret. */
38
+ credentialId;
39
+ secretKey;
40
+ constructor(seed, client = new Client()) {
41
+ this.seed = seed;
42
+ this.client = client;
43
+ const keys = ml_dsa65.keygen(seed);
44
+ this.publicKey = keys.publicKey;
45
+ this.secretKey = keys.secretKey;
46
+ this.address = addressFromPublicKey(keys.publicKey);
47
+ this.account = this.address;
48
+ }
49
+ get keyId() {
50
+ return keyId(this.publicKey);
51
+ }
52
+ /** Act for another account this key has been authorized on. Accepts a name or address. */
53
+ async actAs(account) {
54
+ this.account = await this.client.resolve(account);
55
+ return this;
56
+ }
57
+ static generate(client) {
58
+ return new Wallet(randomBytes(32), client);
59
+ }
60
+ static fromSeed(seed, client) {
61
+ const bytes = typeof seed === "string" ? hexToBytes(seed) : seed;
62
+ if (bytes.length !== 32)
63
+ throw new Error("seed must be 32 bytes");
64
+ return new Wallet(bytes, client);
65
+ }
66
+ /**
67
+ * Derive a wallet from a WebAuthn PRF output. The passkey never signs transactions
68
+ * (its P-256 signatures aren't quantum-safe); it only unlocks this ML-DSA key.
69
+ */
70
+ static fromPasskeyPrf(prfOutput, client) {
71
+ return new Wallet(blake3(prfOutput, { context: utf8ToBytes("blockula passkey wallet v1") }), client);
72
+ }
73
+ get seedHex() {
74
+ return bytesToHex(this.seed);
75
+ }
76
+ sign(tx) {
77
+ const signature = ml_dsa65.sign(signingBytes(tx), this.secretKey);
78
+ return { ...tx, public_key: bytesToHex(this.publicKey), signature: bytesToHex(signature) };
79
+ }
80
+ async build(action, memo = "") {
81
+ const [status, account] = await Promise.all([this.client.status(), this.client.account(this.account).catch(() => null)]);
82
+ const unsigned = { chain_id: status.chain_id, from: this.account, nonce: account?.next_nonce ?? 0, fee: "0", action, memo };
83
+ unsigned.fee = minFee(unsigned).toString();
84
+ return this.sign(unsigned);
85
+ }
86
+ /** Send FANG to a `.boo` name or `boo1…` address. `amount` is in FANG ("1.5") or bigint drops. */
87
+ async send(to, amount, opts = {}) {
88
+ const tx = await this.build({ type: "transfer", to, amount: parseFang(amount).toString() }, opts.memo);
89
+ return this.client.submit(tx, opts.wait ?? true);
90
+ }
91
+ async registerName(name, opts = {}) {
92
+ const tx = await this.build({ type: "register_name", name });
93
+ return this.client.submit(tx, opts.wait ?? true);
94
+ }
95
+ /** Authorize another device's key (e.g. a second passkey) with full control. */
96
+ async addOwnerKey(keyIdHex, label) {
97
+ return this.client.submit(await this.build({ type: "add_key", key_id: keyIdHex, label, role: { type: "owner" } }));
98
+ }
99
+ /**
100
+ * Let an agent/app key spend from this account, transfers only, within a limit.
101
+ * Example: `addSessionKey(agentKeyId, "claude", { limit: "10", periodBlocks: 86_400, expiresInBlocks: 30 * 86_400 })`.
102
+ */
103
+ async addSessionKey(keyIdHex, label, opts) {
104
+ const { height } = await this.client.status();
105
+ const role = { type: "session", limit: parseFang(opts.limit).toString(), period_blocks: opts.periodBlocks, expires_at: height + opts.expiresInBlocks };
106
+ return this.client.submit(await this.build({ type: "add_key", key_id: keyIdHex, label, role }));
107
+ }
108
+ async removeKey(keyIdHex) {
109
+ return this.client.submit(await this.build({ type: "remove_key", key_id: keyIdHex }));
110
+ }
111
+ balance() {
112
+ return this.client.account(this.account).then((a) => a.balance_display, () => "0 FANG");
113
+ }
114
+ /** Dev/test chains only: drip 100 FANG to this wallet. */
115
+ faucet(wait = true) {
116
+ return this.client.faucet(this.account, wait);
117
+ }
118
+ }
119
+ export class Client {
120
+ rpc;
121
+ constructor(rpc = DEFAULT_RPC) {
122
+ this.rpc = rpc;
123
+ }
124
+ async call(path, body) {
125
+ const res = await fetch(this.rpc.replace(/\/$/, "") + "/api" + path, body === undefined ? undefined : { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
126
+ const json = await res.json();
127
+ if (!res.ok)
128
+ throw new Error(json.error ?? res.statusText);
129
+ return json;
130
+ }
131
+ status() { return this.call("/status"); }
132
+ blocks(limit = 20) { return this.call(`/blocks?limit=${limit}`); }
133
+ block(heightOrHash) { return this.call(`/block/${heightOrHash}`); }
134
+ tx(id) { return this.call(`/tx/${id}`); }
135
+ account(addressOrName) { return this.call(`/account/${encodeURIComponent(addressOrName)}`); }
136
+ name(name) { return this.call(`/name/${encodeURIComponent(name)}`); }
137
+ /** Accounts a key is authorized on (its own address, plus any it was added to). */
138
+ keyAccounts(keyIdHex) { return this.call(`/key/${keyIdHex}`); }
139
+ search(q) { return this.call(`/search/${encodeURIComponent(q)}`); }
140
+ async resolve(nameOrAddress) {
141
+ if (isAddress(nameOrAddress))
142
+ return nameOrAddress;
143
+ const r = await this.name(nameOrAddress);
144
+ if (r.available)
145
+ throw new Error(`${r.name} isn't registered`);
146
+ return r.owner.address;
147
+ }
148
+ async submit(tx, wait = true) {
149
+ const { id } = await this.call("/tx", tx);
150
+ return wait ? this.waitForTx(id) : this.tx(id);
151
+ }
152
+ async faucet(to, wait = true) {
153
+ const { id } = await this.call("/faucet", { to });
154
+ return wait ? this.waitForTx(id) : this.tx(id);
155
+ }
156
+ async waitForTx(id, timeoutMs = 15_000) {
157
+ const start = Date.now();
158
+ for (;;) {
159
+ const tx = await this.tx(id);
160
+ if (tx.status === "finalized")
161
+ return tx;
162
+ if (Date.now() - start > timeoutMs)
163
+ throw new Error(`tx ${id} still pending after ${timeoutMs}ms`);
164
+ await new Promise((r) => setTimeout(r, 250));
165
+ }
166
+ }
167
+ }
@@ -0,0 +1,12 @@
1
+ import { Client, Wallet } from "./index.js";
2
+ /** Create a new passkey and the wallet it unlocks. */
3
+ export declare function createPasskeyWallet(opts: {
4
+ username: string;
5
+ client?: Client;
6
+ rpName?: string;
7
+ }): Promise<Wallet>;
8
+ /** Unlock an existing passkey wallet (shows the OS passkey picker). */
9
+ export declare function unlockPasskeyWallet(opts?: {
10
+ client?: Client;
11
+ credentialId?: Uint8Array;
12
+ }): Promise<Wallet>;
@@ -0,0 +1,59 @@
1
+ // Browser-only: passkey-backed Blockula wallets via the WebAuthn PRF extension.
2
+ //
3
+ // The passkey (Face ID / Touch ID / security key) produces a symmetric PRF secret,
4
+ // which deterministically derives the ML-DSA-65 wallet key. Same passkey β†’ same wallet
5
+ // on every synced device, with no seed phrase. The passkey's own P-256 signature is never
6
+ // used on-chain, so the account stays quantum-safe.
7
+ import { Wallet } from "./index.js";
8
+ const PRF_SALT = new TextEncoder().encode("blockula/wallet/v1");
9
+ function rpId() {
10
+ return location.hostname;
11
+ }
12
+ function walletFrom(cred, prf, client) {
13
+ const wallet = Wallet.fromPasskeyPrf(prf, client);
14
+ if (cred)
15
+ wallet.credentialId = new Uint8Array(cred.rawId);
16
+ return wallet;
17
+ }
18
+ async function prfFromCredential(cred) {
19
+ const ext = cred?.getClientExtensionResults();
20
+ const first = ext?.prf?.results?.first;
21
+ if (!first) {
22
+ throw new Error("This passkey provider doesn't support the PRF extension. Try iCloud Keychain, Google Password Manager or 1Password.");
23
+ }
24
+ return new Uint8Array(first);
25
+ }
26
+ /** Create a new passkey and the wallet it unlocks. */
27
+ export async function createPasskeyWallet(opts) {
28
+ const cred = (await navigator.credentials.create({
29
+ publicKey: {
30
+ rp: { id: rpId(), name: opts.rpName ?? "Blockula" },
31
+ user: { id: crypto.getRandomValues(new Uint8Array(16)), name: opts.username, displayName: opts.username },
32
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
33
+ pubKeyCredParams: [{ type: "public-key", alg: -7 }, { type: "public-key", alg: -8 }, { type: "public-key", alg: -257 }],
34
+ authenticatorSelection: { residentKey: "required", userVerification: "required" },
35
+ extensions: { prf: { eval: { first: PRF_SALT } } },
36
+ },
37
+ }));
38
+ // Some authenticators only return PRF output on get(), not create(). Only retry with get()
39
+ // when the authenticator said PRF is enabled, so PRF-less providers fail fast with one prompt.
40
+ const ext = cred?.getClientExtensionResults();
41
+ if (ext?.prf?.results?.first)
42
+ return walletFrom(cred, new Uint8Array(ext.prf.results.first), opts.client);
43
+ if (ext?.prf?.enabled)
44
+ return unlockPasskeyWallet({ client: opts.client, credentialId: cred ? new Uint8Array(cred.rawId) : undefined });
45
+ return walletFrom(cred, await prfFromCredential(cred), opts.client);
46
+ }
47
+ /** Unlock an existing passkey wallet (shows the OS passkey picker). */
48
+ export async function unlockPasskeyWallet(opts = {}) {
49
+ const cred = (await navigator.credentials.get({
50
+ publicKey: {
51
+ rpId: rpId(),
52
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
53
+ userVerification: "required",
54
+ allowCredentials: opts.credentialId ? [{ type: "public-key", id: opts.credentialId }] : [],
55
+ extensions: { prf: { eval: { first: PRF_SALT } } },
56
+ },
57
+ }));
58
+ return walletFrom(cred, await prfFromCredential(cred), opts.client);
59
+ }
package/dist/tx.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ export type KeyRole = {
2
+ type: "owner";
3
+ }
4
+ /** Transfers only, at most `limit` drops (amount + fee) per `period_blocks`, until block `expires_at`. */
5
+ | {
6
+ type: "session";
7
+ limit: string;
8
+ period_blocks: number;
9
+ expires_at: number;
10
+ };
11
+ export type Action = {
12
+ type: "transfer";
13
+ to: string;
14
+ amount: string;
15
+ } | {
16
+ type: "register_name";
17
+ name: string;
18
+ } | {
19
+ type: "add_key";
20
+ key_id: string;
21
+ label: string;
22
+ role: KeyRole;
23
+ } | {
24
+ type: "remove_key";
25
+ key_id: string;
26
+ };
27
+ export interface UnsignedTx {
28
+ chain_id: string;
29
+ /** The account acting; the signing key must be authorized on it. */
30
+ from: string;
31
+ nonce: number;
32
+ fee: string;
33
+ action: Action;
34
+ memo: string;
35
+ }
36
+ export interface Tx extends UnsignedTx {
37
+ public_key: string;
38
+ signature: string;
39
+ }
40
+ export declare const PUBLIC_KEY_LEN = 1952;
41
+ export declare const SIGNATURE_LEN = 3309;
42
+ export declare const DROPS_PER_FANG = 1000000000n;
43
+ export declare function signingBytes(tx: UnsignedTx): Uint8Array;
44
+ /** Mirrors `min_fee` in node/src/types.rs: base fee is priced per byte and burned. */
45
+ export declare function minFee(tx: UnsignedTx): bigint;
46
+ /** "1.5" β†’ 1500000000n drops. Accepts numbers, decimal strings or bigint drops. */
47
+ export declare function parseFang(amount: string | number | bigint): bigint;
48
+ export declare function formatFang(drops: bigint | string): string;
package/dist/tx.js ADDED
@@ -0,0 +1,76 @@
1
+ // Canonical transaction encoding. Must stay byte-identical to node/src/types.rs `Tx::signing_bytes`.
2
+ export const PUBLIC_KEY_LEN = 1952;
3
+ export const SIGNATURE_LEN = 3309;
4
+ export const DROPS_PER_FANG = 1000000000n;
5
+ const utf8 = new TextEncoder();
6
+ class Writer {
7
+ parts = [];
8
+ bytes(b) { for (const x of b)
9
+ this.parts.push(x); }
10
+ u8(n) { this.parts.push(n & 0xff); }
11
+ u16(n) { this.u8(n); this.u8(n >> 8); }
12
+ u64(n) { for (let i = 0n; i < 8n; i++)
13
+ this.u8(Number((n >> (8n * i)) & 0xffn)); }
14
+ str(s) { const b = utf8.encode(s); this.u16(b.length); this.bytes(b); }
15
+ done() { return Uint8Array.from(this.parts); }
16
+ }
17
+ export function signingBytes(tx) {
18
+ const w = new Writer();
19
+ w.bytes(utf8.encode("BLOCKULA_TX_V2"));
20
+ w.str(tx.chain_id);
21
+ w.str(tx.from);
22
+ w.u64(BigInt(tx.nonce));
23
+ w.u64(BigInt(tx.fee));
24
+ const a = tx.action;
25
+ switch (a.type) {
26
+ case "transfer":
27
+ w.u8(0);
28
+ w.str(a.to);
29
+ w.u64(BigInt(a.amount));
30
+ break;
31
+ case "register_name":
32
+ w.u8(1);
33
+ w.str(a.name);
34
+ break;
35
+ case "add_key":
36
+ w.u8(2);
37
+ w.str(a.key_id);
38
+ w.str(a.label);
39
+ if (a.role.type === "owner")
40
+ w.u8(0);
41
+ else {
42
+ w.u8(1);
43
+ w.u64(BigInt(a.role.limit));
44
+ w.u64(BigInt(a.role.period_blocks));
45
+ w.u64(BigInt(a.role.expires_at));
46
+ }
47
+ break;
48
+ case "remove_key":
49
+ w.u8(3);
50
+ w.str(a.key_id);
51
+ break;
52
+ }
53
+ w.str(tx.memo);
54
+ return w.done();
55
+ }
56
+ /** Mirrors `min_fee` in node/src/types.rs: base fee is priced per byte and burned. */
57
+ export function minFee(tx) {
58
+ const size = signingBytes(tx).length + PUBLIC_KEY_LEN + SIGNATURE_LEN;
59
+ return 10000n + BigInt(size) * 10n;
60
+ }
61
+ /** "1.5" β†’ 1500000000n drops. Accepts numbers, decimal strings or bigint drops. */
62
+ export function parseFang(amount) {
63
+ if (typeof amount === "bigint")
64
+ return amount;
65
+ const s = String(amount).trim();
66
+ if (!/^\d+(\.\d{1,9})?$/.test(s))
67
+ throw new Error(`invalid FANG amount: ${s}`);
68
+ const [whole, frac = ""] = s.split(".");
69
+ return BigInt(whole) * DROPS_PER_FANG + BigInt(frac.padEnd(9, "0"));
70
+ }
71
+ export function formatFang(drops) {
72
+ const d = BigInt(drops);
73
+ const whole = d / DROPS_PER_FANG;
74
+ const frac = (d % DROPS_PER_FANG).toString().padStart(9, "0").replace(/0+$/, "");
75
+ return `${whole}${frac ? "." + frac : ""} FANG`;
76
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@blockula/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "\ud83e\udddb TypeScript SDK for Blockula \u2014 post-quantum wallets (ML-DSA-65), .boo names, transfers",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./passkey": "./dist/passkey.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "tsx test/e2e.ts"
20
+ },
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@noble/hashes": "^2.4.0",
24
+ "@noble/post-quantum": "^0.7.1",
25
+ "@scure/base": "^2.4.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "*",
29
+ "tsx": "*",
30
+ "typescript": "*"
31
+ },
32
+ "author": "Jack Dishman",
33
+ "keywords": [
34
+ "blockula",
35
+ "post-quantum",
36
+ "ml-dsa",
37
+ "passkey",
38
+ "webauthn",
39
+ "blockchain",
40
+ "wallet"
41
+ ],
42
+ "engines": {
43
+ "node": ">=20"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }