pivx-wallet 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.
@@ -0,0 +1,31 @@
1
+ export type Network = 'mainnet' | 'testnet';
2
+ export type AddressKind = 'p2pkh' | 'p2sh' | 'staking' | 'exchange';
3
+ /** hash160 = RIPEMD160(SHA256(data)). */
4
+ export declare function hash160(data: Uint8Array): Uint8Array;
5
+ /** Encode a 20-byte hash as a PIVX base58check address of the given kind. */
6
+ export declare function encodeAddress(hash: Uint8Array, network: Network, kind: AddressKind): string;
7
+ /** P2PKH address for a compressed public key. */
8
+ export declare function p2pkhAddress(pubkey: Uint8Array, network: Network): string;
9
+ export interface DecodedAddress {
10
+ hash: Uint8Array;
11
+ kind: AddressKind;
12
+ network: Network;
13
+ }
14
+ /** Decode and validate a PIVX transparent address, identifying kind + network. */
15
+ export declare function decodeAddress(address: string): DecodedAddress;
16
+ /** True if `address` is a well-formed PIVX transparent address. */
17
+ export declare function isValidAddress(address: string): boolean;
18
+ /** A derived transparent key. */
19
+ export interface TransparentKey {
20
+ privateKey: Uint8Array;
21
+ publicKey: Uint8Array;
22
+ network: Network;
23
+ address: string;
24
+ /** WIF (compressed) encoding, for import into other tools. */
25
+ wif: string;
26
+ }
27
+ /**
28
+ * Derive the BIP44 transparent key at m/44'/coin'/account'/change/index.
29
+ * `change` is 0 for external (receive) addresses, 1 for internal (change).
30
+ */
31
+ export declare function deriveKey(seed: Uint8Array, network: Network, account: number, change: number, index: number): TransparentKey;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Transparent (non-shielded) HD wallet: BIP32/44 key derivation and PIVX
3
+ * address encoding/decoding for P2PKH, cold-staking, and exchange addresses.
4
+ *
5
+ * Keys derive under BIP44 m/44'/119'/account'/change/index (coin type 119
6
+ * mainnet, 1 testnet). Addresses are base58check with network-specific
7
+ * version prefixes (from chainparams).
8
+ */
9
+ import { HDKey } from '@scure/bip32';
10
+ import { base58check } from '@scure/base';
11
+ import { sha256 } from '@noble/hashes/sha2.js';
12
+ import { ripemd160 } from '@noble/hashes/legacy.js';
13
+ const b58c = base58check(sha256);
14
+ /** base58 version prefix bytes per network + kind (from chainparams.cpp). */
15
+ const PREFIX = {
16
+ mainnet: { p2pkh: [30], p2sh: [13], staking: [63], exchange: [0x01, 0xb9, 0xa2] },
17
+ testnet: { p2pkh: [139], p2sh: [19], staking: [73], exchange: [0x01, 0xb9, 0xb1] },
18
+ };
19
+ const COIN_TYPE = { mainnet: 119, testnet: 1 };
20
+ const WIF_PREFIX = { mainnet: 212, testnet: 239 };
21
+ /** hash160 = RIPEMD160(SHA256(data)). */
22
+ export function hash160(data) {
23
+ return ripemd160(sha256(data));
24
+ }
25
+ /** Encode a 20-byte hash as a PIVX base58check address of the given kind. */
26
+ export function encodeAddress(hash, network, kind) {
27
+ if (hash.length !== 20)
28
+ throw new Error('hash must be 20 bytes');
29
+ const prefix = PREFIX[network][kind];
30
+ return b58c.encode(Uint8Array.from([...prefix, ...hash]));
31
+ }
32
+ /** P2PKH address for a compressed public key. */
33
+ export function p2pkhAddress(pubkey, network) {
34
+ return encodeAddress(hash160(pubkey), network, 'p2pkh');
35
+ }
36
+ /** Decode and validate a PIVX transparent address, identifying kind + network. */
37
+ export function decodeAddress(address) {
38
+ let data;
39
+ try {
40
+ data = b58c.decode(address);
41
+ }
42
+ catch {
43
+ throw new Error(`invalid address: ${address}`);
44
+ }
45
+ for (const network of ['mainnet', 'testnet']) {
46
+ for (const kind of ['p2pkh', 'p2sh', 'staking', 'exchange']) {
47
+ const prefix = PREFIX[network][kind];
48
+ if (data.length === prefix.length + 20 && prefix.every((b, i) => data[i] === b)) {
49
+ return { hash: data.slice(prefix.length), kind, network };
50
+ }
51
+ }
52
+ }
53
+ throw new Error(`invalid address: ${address}`);
54
+ }
55
+ /** True if `address` is a well-formed PIVX transparent address. */
56
+ export function isValidAddress(address) {
57
+ try {
58
+ decodeAddress(address);
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ /**
66
+ * Derive the BIP44 transparent key at m/44'/coin'/account'/change/index.
67
+ * `change` is 0 for external (receive) addresses, 1 for internal (change).
68
+ */
69
+ export function deriveKey(seed, network, account, change, index) {
70
+ const master = HDKey.fromMasterSeed(seed);
71
+ const child = master.derive(`m/44'/${COIN_TYPE[network]}'/${account}'/${change}/${index}`);
72
+ if (!child.privateKey || !child.publicKey)
73
+ throw new Error('key derivation failed');
74
+ const wif = b58c.encode(Uint8Array.from([WIF_PREFIX[network], ...child.privateKey, 0x01]));
75
+ return {
76
+ privateKey: child.privateKey,
77
+ publicKey: child.publicKey, // compressed
78
+ network,
79
+ address: p2pkhAddress(child.publicKey, network),
80
+ wif,
81
+ };
82
+ }
@@ -0,0 +1,109 @@
1
+ /** Amounts in this package are integer satoshis (1 PIV = 1e8), the unit of
2
+ * the underlying shield library — unlike the `pivx-rpc` layer, which uses
3
+ * PIV floats as the node emits them. */
4
+ /** A decrypted shielded note the wallet can track (and spend, with a spending key). */
5
+ export interface SpendableNote {
6
+ /** Opaque note object from the shield library (JSON-serializable). */
7
+ note: {
8
+ value: number;
9
+ recipient: number[];
10
+ rseed: unknown;
11
+ };
12
+ /** Hex-serialized incremental merkle witness. */
13
+ witness: string;
14
+ /** Hex nullifier — how spends of this note are recognized on-chain. */
15
+ nullifier: string;
16
+ /** Decoded text memo, when the note carried one. */
17
+ memo?: string | null;
18
+ }
19
+ /** A block to scan: raw tx hexes plus the block height. */
20
+ export interface WalletBlock {
21
+ height: number;
22
+ txs: {
23
+ hex: string;
24
+ txid: string;
25
+ }[];
26
+ }
27
+ /** Transparent UTXO used as input when shielding funds. */
28
+ export interface TransparentInput {
29
+ txid: string;
30
+ vout: number;
31
+ amount: number;
32
+ /** 32-byte private key controlling the UTXO. */
33
+ private_key: Uint8Array;
34
+ /** scriptPubKey bytes of the UTXO. */
35
+ script: Uint8Array;
36
+ }
37
+ export interface CreateWalletOptions {
38
+ /** 32 bytes of seed entropy (derives the spending key; full capability). */
39
+ seed?: Uint8Array;
40
+ /** Bech32 extended spending key (`p-secret-spending-key-…`; full capability). */
41
+ spendingKey?: string;
42
+ /** Bech32 extended full viewing key (watch-only: scan/receive, no spending). */
43
+ viewingKey?: string;
44
+ network?: 'mainnet' | 'testnet';
45
+ /** Wallet birth height: scanning starts at the nearest checkpoint at or below it. */
46
+ birthHeight: number;
47
+ /** ZIP32 account index under the seed. Default 0. */
48
+ accountIndex?: number;
49
+ /**
50
+ * Proving backend. Defaults to single-core WASM. Set `multicore: true` to
51
+ * use the parallel WASM build (browser only, needs cross-origin isolation).
52
+ * For server-side proving, use the native Rust SDK instead.
53
+ */
54
+ proving?: import('./shield-bindings.js').ProvingOptions;
55
+ }
56
+ export interface CreateTransactionOptions {
57
+ /** Recipient: shield (ps1…) or transparent address. */
58
+ to: string;
59
+ /** Amount in satoshis. */
60
+ amount: number;
61
+ /** UTF-8 memo (shield recipients only, max 512 bytes). */
62
+ memo?: string;
63
+ /**
64
+ * Inputs: 'shield' (default) spends the wallet's notes; pass transparent
65
+ * UTXOs instead to shield transparent funds.
66
+ */
67
+ inputs?: 'shield' | TransparentInput[];
68
+ /** Required when spending transparent inputs (change must stay transparent). */
69
+ transparentChangeAddress?: string;
70
+ /**
71
+ * Opt in to sweep semantics: allow the amount to consume the entire
72
+ * spendable balance, paying the fee out of the recipient's amount.
73
+ * Without this, an amount that leaves no room for the fee is rejected.
74
+ */
75
+ sweep?: boolean;
76
+ }
77
+ export interface BuiltTransaction {
78
+ txid: string;
79
+ /** Fully-proved raw transaction hex, ready for `sendrawtransaction`. */
80
+ hex: string;
81
+ /** Nullifiers this tx spends (tracked as pending until finalize/discard). */
82
+ nullifiers: string[];
83
+ }
84
+ export interface SyncOptions {
85
+ /** Blocks per root-check batch. Default 100. */
86
+ batchSize?: number;
87
+ /**
88
+ * Max concurrent block fetches. Default 8. Keep well under the node's
89
+ * rpcworkqueue (default 16) — a full batch fired at once returns 500s.
90
+ */
91
+ rpcConcurrency?: number;
92
+ onProgress?: (height: number, tip: number) => void;
93
+ }
94
+ /** Serialized wallet state (spending key deliberately excluded). */
95
+ export interface WalletState {
96
+ version: 1;
97
+ network: 'mainnet' | 'testnet';
98
+ extfvk: string;
99
+ lastProcessedBlock: number;
100
+ commitmentTree: string;
101
+ diversifierIndex: number[];
102
+ notes: SpendableNote[];
103
+ nullifierMap: Record<string, {
104
+ recipient: string;
105
+ value: number;
106
+ }>;
107
+ /** txid → nullifiers for broadcast-but-unconfirmed spends. */
108
+ pendingSpends?: Record<string, string[]>;
109
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /** Amounts in this package are integer satoshis (1 PIV = 1e8), the unit of
2
+ * the underlying shield library — unlike the `pivx-rpc` layer, which uses
3
+ * PIV floats as the node emits them. */
4
+ export {};
@@ -0,0 +1,161 @@
1
+ import { type PivxClient } from 'pivx-rpc';
2
+ import { type ProvingOptions } from './shield-bindings.js';
3
+ import type { BuiltTransaction, CreateTransactionOptions, CreateWalletOptions, SpendableNote, SyncOptions, WalletBlock } from './types.js';
4
+ /** Thrown when a watch-only wallet is asked to spend. */
5
+ export declare class NoSpendAuthorityError extends Error {
6
+ constructor();
7
+ }
8
+ /** Thrown when the local commitment tree diverges from the node's sapling root. */
9
+ export declare class ScanDivergedError extends Error {
10
+ readonly height: number;
11
+ readonly localRoot: string;
12
+ readonly nodeRoot: string;
13
+ constructor(height: number, localRoot: string, nodeRoot: string);
14
+ }
15
+ /**
16
+ * Standalone PIVX wallet: owns keys, scans blocks, tracks shielded notes,
17
+ * and builds fully-proved transactions locally. A node (via `pivx-rpc`) is
18
+ * only used as a chain-data source and broadcast endpoint.
19
+ *
20
+ * Capabilities follow the key material: constructed from a seed or spending
21
+ * key the wallet can spend; from a viewing key it can scan, derive receive
22
+ * addresses, and track balance (watch-only) — and can be upgraded in place
23
+ * with {@link loadSpendingKey}.
24
+ */
25
+ export declare class PivxWallet {
26
+ private readonly shield;
27
+ readonly network: 'mainnet' | 'testnet';
28
+ private readonly extfvk;
29
+ private commitmentTree;
30
+ private lastProcessedBlock;
31
+ private extsk?;
32
+ private notes;
33
+ private nullifierMap;
34
+ /** txid → nullifiers awaiting broadcast confirmation. Persisted, so a
35
+ * crash between broadcast and finalize can't resurrect spent notes. */
36
+ private pendingSpends;
37
+ private diversifierIndex;
38
+ /** One writer at a time: block state-mutating operations from racing. */
39
+ private busy;
40
+ /** Whether the starting checkpoint has been confirmed against the node. */
41
+ private startValidated;
42
+ private constructor();
43
+ private get isTestnet();
44
+ /** True when the wallet holds spend authority. */
45
+ get canSpend(): boolean;
46
+ static create(opts: CreateWalletOptions): Promise<PivxWallet>;
47
+ /** Upgrade a watch-only wallet. The key must match the stored viewing key. */
48
+ loadSpendingKey(spendingKey: string): void;
49
+ /** Next diversified shield receive address. */
50
+ getNewAddress(): string;
51
+ /** Confirmed shielded balance in satoshis (scanned notes, minus pending spends). */
52
+ getBalance(): number;
53
+ /** Whether `address` is a shield (Sapling) address on this wallet's network. */
54
+ private isShieldAddress;
55
+ /** Currently tracked unspent notes. */
56
+ getNotes(): readonly SpendableNote[];
57
+ getLastSyncedBlock(): number;
58
+ /** Look up a note by its on-chain nullifier (payment attribution for spends). */
59
+ getNoteFromNullifier(nullifier: string): {
60
+ recipient: string;
61
+ value: number;
62
+ } | undefined;
63
+ /**
64
+ * Scan blocks (strictly ascending heights, all above the last synced
65
+ * block). Returns the raw hexes of transactions relevant to this wallet.
66
+ * Use this directly when you have your own block feed; otherwise see
67
+ * {@link sync}.
68
+ */
69
+ handleBlocks(blocks: WalletBlock[]): string[];
70
+ /** handleBlocks without the busy guard, for internal use by sync (which
71
+ * already holds the guard). */
72
+ private applyBlocks;
73
+ /**
74
+ * Decrypt a single transaction's outputs for this wallet without touching
75
+ * wallet state — a hint for 0-conf payment detection from the mempool.
76
+ *
77
+ * This only trial-decrypts; it does NOT validate the transaction (proof,
78
+ * double-spend, or whether it will ever confirm). Do not credit funds
79
+ * from a preview: dedupe on the caller's own txid and credit only from
80
+ * confirmed notes returned by {@link getNotes} after {@link sync}.
81
+ */
82
+ previewTransaction(hex: string): {
83
+ recipient: string;
84
+ value: number;
85
+ memo?: string | null;
86
+ }[];
87
+ /**
88
+ * Sync from the node up to its current tip.
89
+ *
90
+ * Each batch checks the locally-built tree against the node's own
91
+ * `finalsaplingroot`. That catches malformed or mis-ordered data from the
92
+ * node, but it is a self-consistency check, not chain authentication: the
93
+ * SDK does not validate proof-of-stake, so a dishonest node can still serve
94
+ * a self-consistent fabricated chain. Point this at a node you trust. See
95
+ * SECURITY.md.
96
+ */
97
+ sync(client: PivxClient, opts?: SyncOptions): Promise<void>;
98
+ /**
99
+ * Confirm the starting commitment tree against the node before scanning
100
+ * forward. A fresh wallet begins at a bundled checkpoint; if that
101
+ * checkpoint's tree does not match the node's sapling root at that height
102
+ * (some near-tip checkpoints in the shield library are captured on stale
103
+ * blocks), walk back to the newest checkpoint the node does confirm. A
104
+ * wallet that already holds scanned notes and no longer matches is treated
105
+ * as diverged rather than silently rewound.
106
+ */
107
+ private ensureValidCheckpoint;
108
+ /**
109
+ * Reset scan state to the checkpoint at or below `height` and drop all
110
+ * tracked notes. This is the recovery path after a divergence error: call
111
+ * it, then sync again. It needs no keys.
112
+ */
113
+ reloadFromCheckpoint(height: number): void;
114
+ /** Load sapling proving parameters (required once before building transactions). */
115
+ loadProver(source: {
116
+ path: string;
117
+ } | {
118
+ url: string;
119
+ } | {
120
+ spend: Uint8Array;
121
+ output: Uint8Array;
122
+ }): Promise<void>;
123
+ /**
124
+ * Build and prove a transaction locally. Nothing is broadcast; the spent
125
+ * notes are held as pending until {@link finalizeTransaction} or
126
+ * {@link discardTransaction}.
127
+ */
128
+ createTransaction(opts: CreateTransactionOptions): Promise<BuiltTransaction>;
129
+ /** Build, broadcast, and finalize in one step. */
130
+ send(client: PivxClient, opts: CreateTransactionOptions): Promise<string>;
131
+ /** Mark a broadcast transaction's notes as spent. */
132
+ finalizeTransaction(txid: string): void;
133
+ /** Release a failed transaction's notes back to the spendable set. */
134
+ discardTransaction(txid: string): void;
135
+ /**
136
+ * Transactions built and broadcast but not yet finalized or discarded
137
+ * (txid → the nullifiers they spend). After a broadcast error left a spend
138
+ * ambiguous, use this to find the txid, confirm it on-chain, then
139
+ * {@link finalizeTransaction} or {@link discardTransaction}.
140
+ */
141
+ pendingTransactions(): Record<string, string[]>;
142
+ /**
143
+ * Serialize wallet state to JSON. The spending key is deliberately
144
+ * excluded — persist it separately (encrypted) and restore with
145
+ * {@link loadSpendingKey}.
146
+ */
147
+ save(): string;
148
+ /**
149
+ * Restore a wallet from {@link save} output.
150
+ *
151
+ * For a watch-only deposit scanner, pass `opts.expectedViewingKey` (the key
152
+ * you know this wallet should have): a tampered state file that swapped in
153
+ * an attacker's viewing key would otherwise silently repoint deposit
154
+ * addresses to the attacker. Saved-state integrity is theft-critical here.
155
+ */
156
+ static load(json: string, opts?: {
157
+ proving?: ProvingOptions;
158
+ expectedViewingKey?: string;
159
+ }): Promise<PivxWallet>;
160
+ private encodeRecipient;
161
+ }