bch-rpa 0.0.0-stage → 1.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/CHANGELOG.md ADDED
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0 (2026-10-27)
4
+
5
+ ### Migration
6
+
7
+ - Only `cashcode:` and `cashcodetest:` are accepted. CashCodes use compressed one-time public keys and are incompatible with legacy uncompressed-key PayCodes; changing a prefix does not convert between variants.
8
+ - New wallets default to `/3/0` scan and `/3/1` spend. Existing wallets must retain their original derivation paths to recover earlier payments. For wallets created with the former `/2` default, use `deriveRpaKeys(accountNode, { chainIndex: 2 })`.
9
+
10
+ ### Fixed
11
+
12
+ - Match RPA outputs by decoded locking scripts rather than address strings, including equivalent token-aware CashAddresses
13
+ - Use `cashcode:` and `cashcodetest:` wire prefixes for mainnet and testnet paycodes
14
+ - Encode CashCode payloads without an extra CashAddr format-version byte
15
+ - Preserve display-order outpoint transaction hashes and reconstruct wire-order serialized inputs when parsing raw transactions
16
+ - Use Selene's agreed RPA derivation chain 3 for scan and spend keys
17
+ - Enforce the sender's one-week minimum paycode expiry window
18
+ - Scan every consecutive RPA payment output rather than stopping after ten child indices
19
+ - Reject invalid paycodes, prefix sizes, outpoint indices, expiry values, and designated-input counts
20
+
21
+ ### Documentation
22
+
23
+ - State that CashCodes use compressed one-time public keys and are intentionally incompatible with the legacy uncompressed-key `paycode:` / `paycodetest:` variant; remove misleading Electron Cash interoperability claims
24
+ - Define CashCodes as token-aware destinations for BCH, fungible CashTokens, and NFTs, with token accounting and transaction construction owned by the integrating wallet
25
+ - Clarify that scan network flags format returned addresses and do not affect locking-script ownership matching
26
+
27
+ ## 1.0.0 (2026-02-14)
28
+
29
+ Initial stable release.
30
+
31
+ ### Features
32
+
33
+ - **Paycode generation, encoding, and decoding** — P2PKH paycodes (versions 1/5) with configurable prefix size and optional expiry
34
+ - **Sender flow** — `prepareSend()` derives one-time payment addresses from a static paycode via ECDH + CKDpub
35
+ - **Receiver scanning** — Fulcrum-backed primary path (`tryTransaction`) and self-scan fallback (`scanTransaction`) with prefix matching
36
+ - **Spending key derivation** — `deriveSpendingKey()` recovers the private key for matched one-time addresses
37
+ - **HD wallet integration** — `deriveRpaKeys()` derives scan/spend keypairs from a BIP44 account node (chain 2, indices 0/1)
38
+ - **Prefix grinding** — optional `@noble/secp256k1` peer dependency for signature grinding to match paycode prefixes
39
+ - **Transaction parsing** — handles both Fulcrum verbose JSON and raw hex formats
40
+ - **Mainnet and testnet** — network-specific encoding and automatic address prefix selection
41
+ - **Shared-secret vectors** — tested shared-secret computation, including the 257-bit overflow edge case; these checks do not establish interoperability with legacy PayCode payments
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 bch-rpa contributors
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,97 @@
1
- # Temporary Holding Version
1
+ # bch-rpa
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 library for Bitcoin Cash Reusable Payment Addresses (RPA). Share one CashCode; senders derive fresh one-time payment destinations, and the recipient detects payments and derives their spending keys.
4
+
5
+ - `cashcode:` — mainnet.
6
+ - `cashcodetest:` — testnet / chipnet.
7
+ - **Both imply support for receiving and spending BCH, fungible CashTokens, and NFTs.**
8
+
9
+ **CashCodes use compressed public keys, including the one-time public key hashed into each payment address. They are intentionally incompatible with the legacy uncompressed-key `paycode:` / `paycodetest:` variant.** This library rejects both legacy prefixes. Changing a prefix does not convert between variants.
10
+
11
+ Built on [`@bitauth/libauth`](https://github.com/bitauth/libauth). The library handles encoding, key derivation, scanning, and signature grinding. Your wallet handles networking, transaction construction, validation, UTXOs, and token accounting.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add bch-rpa
17
+ ```
18
+
19
+ ESM only; Node.js 18+ or a modern browser. Sending to a CashCode with a nonzero scan prefix also requires:
20
+
21
+ ```bash
22
+ pnpm add @noble/secp256k1@2
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ The examples assume your wallet already has keys, transaction inputs, and a source of raw transactions. Public API names retain the term `Paycode`.
28
+
29
+ ### Create a CashCode
30
+
31
+ ```typescript
32
+ import { deriveRpaKeys, generatePaycode, encodePaycode } from 'bch-rpa';
33
+
34
+ const keys = deriveRpaKeys(accountNode);
35
+ const paycode = generatePaycode(keys.scanPubkey, keys.spendPubkey, {
36
+ prefixSize: 8,
37
+ });
38
+ const cashCode = encodePaycode(paycode);
39
+ ```
40
+
41
+ Share `cashCode`; keep the private keys private. The default derivation paths are the account's `/3/0` scan key and `/3/1` spend key. Use `{ testnet: true }` when generating a testnet / chipnet CashCode.
42
+
43
+ ### Derive a payment destination
44
+
45
+ ```typescript
46
+ import { decodePaycode, prepareSend } from 'bch-rpa';
47
+
48
+ const [payment] = prepareSend(decodePaycode(recipientCashCode), {
49
+ privkey: inputPrivateKey,
50
+ outpointTxid: inputTxid,
51
+ outpointIndex: inputOutputIndex,
52
+ });
53
+
54
+ // Build a transaction output paying payment.address.
55
+ ```
56
+
57
+ The designated P2PKH input must remain among the transaction's first 30 inputs. For nonzero scan prefixes, use [`grindPrefix()`](https://gitlab.com/selene.cash/bch-rpa/-/blob/v1.1.0/docs/INTEGRATION.md#grinding) when signing that input. The wallet then broadcasts the transaction.
58
+
59
+ ### Detect received payments
60
+
61
+ Fetch candidates using Fulcrum's `blockchain.rpa.get_history` and `blockchain.rpa.get_mempool`, then retrieve each raw transaction with `blockchain.transaction.get`:
62
+
63
+ ```typescript
64
+ import { parseRawTransaction, tryTransaction, deriveSpendingKey } from 'bch-rpa';
65
+
66
+ const tx = parseRawTransaction(rawTxHex);
67
+ const matches = tryTransaction(tx, keys.scanPrivkey, keys.spendPubkey);
68
+
69
+ for (const match of matches) {
70
+ match.outputIndices.forEach((outputIndex, childIndex) => {
71
+ const spendingKey = deriveSpendingKey(
72
+ keys.spendPrivkey, match.sharedSecret, childIndex,
73
+ );
74
+ // spendingKey controls tx.txid:outputIndex.
75
+ });
76
+ }
77
+ ```
78
+
79
+ Check unspent status before counting or spending matched outputs. Without an RPA indexer, use `scanTransaction()` to perform prefix filtering locally.
80
+
81
+ ## Integration notes
82
+
83
+ - **Tokens:** BCH-only and token-bearing outputs use the same derived locking scripts and spending keys. Keep token categories, amounts, NFT details, and token change in the wallet. The parsers return only a scanning projection; output `value` is BCH in satoshis.
84
+ - **Addresses:** Scanning compares decoded locking scripts, so ordinary and token-aware CashAddr encodings match identically. If an address-based token-sending API requires a token-aware CashAddr, re-encode the derived destination's script accordingly.
85
+ - **Networks:** `prepareSend()` selects address formatting from the CashCode version. Pass `true` as the final argument to parsing and scanning functions for testnet / chipnet address formatting; the wallet must connect to the correct blockchain separately.
86
+ - **Scope:** Supports compressed-key, on-chain P2PKH paycodes (versions `1` and `5`). Legacy `paycode:` / `paycodetest:`, offline-only paycodes, P2SH multisig paycodes, and private-key interchange are not supported.
87
+
88
+ ## Reference
89
+
90
+ - [Wallet integration guide](https://gitlab.com/selene.cash/bch-rpa/-/blob/v1.1.0/docs/INTEGRATION.md) — complete send/receive flows, server queries, and grinding.
91
+ - [Public API](https://gitlab.com/selene.cash/bch-rpa/-/blob/v1.1.0/src/index.ts) and [TypeScript types](https://gitlab.com/selene.cash/bch-rpa/-/blob/v1.1.0/src/types.ts).
92
+ - [Examples](https://gitlab.com/selene.cash/bch-rpa/-/tree/v1.1.0/examples) — sender, receiver, and HD wallet integration.
93
+ - [RPA specification](https://github.com/imaginaryusername/Reusable_specs/blob/master/reusable_addresses.md).
94
+
95
+ ## License
96
+
97
+ [MIT](LICENSE)
@@ -0,0 +1,12 @@
1
+ import type { RpaPaymentAddress } from './types.js';
2
+ /**
3
+ * Derive a one-time payment address from the receiver's spend pubkey and a shared secret.
4
+ * Uses BIP32 CKDpub with a synthetic HD node.
5
+ */
6
+ export declare function derivePaymentAddress(spendPubkey: Uint8Array, sharedSecret: Uint8Array, childIndex?: number, testnet?: boolean): RpaPaymentAddress;
7
+ /**
8
+ * Derive the spending private key for a one-time address.
9
+ * Uses BIP32 CKDpriv with a synthetic HD node.
10
+ */
11
+ export declare function deriveSpendingKey(spendPrivkey: Uint8Array, sharedSecret: Uint8Array, childIndex?: number): Uint8Array;
12
+ //# sourceMappingURL=derive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../src/derive.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAMpD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,UAAU,EACvB,YAAY,EAAE,UAAU,EACxB,UAAU,GAAE,MAAU,EACtB,OAAO,GAAE,OAAe,GACvB,iBAAiB,CAqCnB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,UAAU,EACxB,YAAY,EAAE,UAAU,EACxB,UAAU,GAAE,MAAU,GACrB,UAAU,CAyBZ"}
package/dist/derive.js ADDED
@@ -0,0 +1,70 @@
1
+ import { deriveHdPublicNodeChild, deriveHdPrivateNodeChild, secp256k1, sha256, ripemd160, encodeCashAddress, CashAddressType, validateSecp256k1PrivateKey, } from '@bitauth/libauth';
2
+ function hash160(data) {
3
+ return ripemd160.hash(sha256.hash(data));
4
+ }
5
+ /**
6
+ * Derive a one-time payment address from the receiver's spend pubkey and a shared secret.
7
+ * Uses BIP32 CKDpub with a synthetic HD node.
8
+ */
9
+ export function derivePaymentAddress(spendPubkey, sharedSecret, childIndex = 0, testnet = false) {
10
+ if (!secp256k1.validatePublicKey(spendPubkey)) {
11
+ throw new Error('Invalid spendPubkey: must be a 33-byte compressed secp256k1 point');
12
+ }
13
+ if (sharedSecret.length !== 32) {
14
+ throw new Error('Invalid sharedSecret: must be 32 bytes');
15
+ }
16
+ if (!Number.isInteger(childIndex) || childIndex < 0 || childIndex >= 0x80000000) {
17
+ throw new Error('Invalid childIndex: must be an integer in [0, 2^31)');
18
+ }
19
+ const parentNode = {
20
+ publicKey: spendPubkey,
21
+ chainCode: sharedSecret,
22
+ childIndex: 0,
23
+ depth: 0,
24
+ parentFingerprint: new Uint8Array(4),
25
+ };
26
+ const childNode = deriveHdPublicNodeChild(parentNode, childIndex);
27
+ if (typeof childNode === 'string') {
28
+ throw new Error(`HD public derivation failed: ${childNode}`);
29
+ }
30
+ const pubkeyHash = hash160(childNode.publicKey);
31
+ const { address } = encodeCashAddress({
32
+ payload: pubkeyHash,
33
+ type: CashAddressType.p2pkh,
34
+ prefix: testnet ? 'bchtest' : 'bitcoincash',
35
+ });
36
+ return {
37
+ address,
38
+ pubkey: childNode.publicKey,
39
+ childIndex,
40
+ sharedSecret,
41
+ };
42
+ }
43
+ /**
44
+ * Derive the spending private key for a one-time address.
45
+ * Uses BIP32 CKDpriv with a synthetic HD node.
46
+ */
47
+ export function deriveSpendingKey(spendPrivkey, sharedSecret, childIndex = 0) {
48
+ if (!validateSecp256k1PrivateKey(spendPrivkey)) {
49
+ throw new Error('Invalid spendPrivkey: must be a 32-byte valid secp256k1 scalar');
50
+ }
51
+ if (sharedSecret.length !== 32) {
52
+ throw new Error('Invalid sharedSecret: must be 32 bytes');
53
+ }
54
+ if (!Number.isInteger(childIndex) || childIndex < 0 || childIndex >= 0x80000000) {
55
+ throw new Error('Invalid childIndex: must be an integer in [0, 2^31)');
56
+ }
57
+ const parentNode = {
58
+ privateKey: spendPrivkey,
59
+ chainCode: sharedSecret,
60
+ childIndex: 0,
61
+ depth: 0,
62
+ parentFingerprint: new Uint8Array(4),
63
+ };
64
+ const childNode = deriveHdPrivateNodeChild(parentNode, childIndex);
65
+ if (typeof childNode === 'string') {
66
+ throw new Error(`HD private derivation failed: ${childNode}`);
67
+ }
68
+ return childNode.privateKey;
69
+ }
70
+ //# sourceMappingURL=derive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"derive.js","sourceRoot":"","sources":["../src/derive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import type { Paycode } from './types.js';
2
+ /**
3
+ * Check if prefix grinding is available.
4
+ * Requires @noble/secp256k1 as a peer dependency (libauth signing is deterministic).
5
+ */
6
+ export declare function isGrindingAvailable(): Promise<boolean>;
7
+ /**
8
+ * Grind signature until serialized input's double-SHA256 matches paycode prefix.
9
+ * Requires @noble/secp256k1 as peer dependency for non-deterministic signing.
10
+ *
11
+ * **Signature format:** The callback receives and the result contains a 64-byte
12
+ * compact signature (r || s), NOT DER-encoded. Your `serializeInput` callback is
13
+ * responsible for DER-encoding the signature and appending the sighash byte
14
+ * (typically 0x41 for SIGHASH_ALL|FORKID) before building the scriptSig.
15
+ *
16
+ * @param paycode - Decoded paycode (prefix extracted via getScanPrefix)
17
+ * @param inputPrivkey - 32-byte private key of the designated input
18
+ * @param sigHash - 32-byte sighash to sign
19
+ * @param serializeInput - Callback that builds serialized input bytes from a 64-byte compact signature (r || s).
20
+ * Must DER-encode the signature and append the sighash byte before constructing the full scriptSig.
21
+ * @param maxIterations - Maximum grinding attempts (default 1,000,000)
22
+ * @returns Matching signature (64-byte compact r||s) and iteration count, or null if prefixSize is 0 or max reached
23
+ */
24
+ export declare function grindPrefix(paycode: Paycode, inputPrivkey: Uint8Array, sigHash: Uint8Array, serializeInput: (signature: Uint8Array) => Uint8Array, maxIterations?: number): Promise<{
25
+ signature: Uint8Array;
26
+ iterations: number;
27
+ } | null>;
28
+ //# sourceMappingURL=grind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grind.d.ts","sourceRoot":"","sources":["../src/grind.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAgB1C;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE5D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,UAAU,EACxB,OAAO,EAAE,UAAU,EACnB,cAAc,EAAE,CAAC,SAAS,EAAE,UAAU,KAAK,UAAU,EACrD,aAAa,GAAE,MAAkB,GAChC,OAAO,CAAC;IAAE,SAAS,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAmC/D"}
package/dist/grind.js ADDED
@@ -0,0 +1,69 @@
1
+ import { sha256, binToHex, validateSecp256k1PrivateKey } from '@bitauth/libauth';
2
+ import { getScanPrefix } from './paycode.js';
3
+ let cachedNoble;
4
+ async function loadNoble() {
5
+ if (cachedNoble !== undefined)
6
+ return cachedNoble;
7
+ try {
8
+ cachedNoble = await import('@noble/secp256k1');
9
+ return cachedNoble;
10
+ }
11
+ catch {
12
+ cachedNoble = null;
13
+ return null;
14
+ }
15
+ }
16
+ /**
17
+ * Check if prefix grinding is available.
18
+ * Requires @noble/secp256k1 as a peer dependency (libauth signing is deterministic).
19
+ */
20
+ export async function isGrindingAvailable() {
21
+ return (await loadNoble()) !== null;
22
+ }
23
+ /**
24
+ * Grind signature until serialized input's double-SHA256 matches paycode prefix.
25
+ * Requires @noble/secp256k1 as peer dependency for non-deterministic signing.
26
+ *
27
+ * **Signature format:** The callback receives and the result contains a 64-byte
28
+ * compact signature (r || s), NOT DER-encoded. Your `serializeInput` callback is
29
+ * responsible for DER-encoding the signature and appending the sighash byte
30
+ * (typically 0x41 for SIGHASH_ALL|FORKID) before building the scriptSig.
31
+ *
32
+ * @param paycode - Decoded paycode (prefix extracted via getScanPrefix)
33
+ * @param inputPrivkey - 32-byte private key of the designated input
34
+ * @param sigHash - 32-byte sighash to sign
35
+ * @param serializeInput - Callback that builds serialized input bytes from a 64-byte compact signature (r || s).
36
+ * Must DER-encode the signature and append the sighash byte before constructing the full scriptSig.
37
+ * @param maxIterations - Maximum grinding attempts (default 1,000,000)
38
+ * @returns Matching signature (64-byte compact r||s) and iteration count, or null if prefixSize is 0 or max reached
39
+ */
40
+ export async function grindPrefix(paycode, inputPrivkey, sigHash, serializeInput, maxIterations = 1_000_000) {
41
+ if (!validateSecp256k1PrivateKey(inputPrivkey)) {
42
+ throw new Error('Invalid inputPrivkey: must be a 32-byte valid secp256k1 scalar');
43
+ }
44
+ if (!(sigHash instanceof Uint8Array) || sigHash.length !== 32) {
45
+ throw new Error('Invalid sigHash: must be 32 bytes');
46
+ }
47
+ const targetPrefix = getScanPrefix(paycode);
48
+ if (targetPrefix === '')
49
+ return null;
50
+ const noble = await loadNoble();
51
+ if (!noble) {
52
+ throw new Error('Prefix grinding requires @noble/secp256k1 as a peer dependency. ' +
53
+ 'Install it with: npm install @noble/secp256k1');
54
+ }
55
+ const n = targetPrefix.length;
56
+ for (let i = 0; i < maxIterations; i++) {
57
+ // lowS:true is required for BCH (BIP62), extraEntropy gives nonce variation for grinding
58
+ const sig = await noble.signAsync(sigHash, inputPrivkey, { lowS: true, extraEntropy: true });
59
+ const sigBytes = sig.toCompactRawBytes();
60
+ const serialized = serializeInput(sigBytes);
61
+ const hash = sha256.hash(sha256.hash(serialized));
62
+ const hashPrefix = binToHex(hash).slice(0, n);
63
+ if (hashPrefix === targetPrefix) {
64
+ return { signature: sigBytes, iterations: i + 1 };
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ //# sourceMappingURL=grind.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grind.js","sourceRoot":"","sources":["../src/grind.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAI7C,IAAI,WAA6D,CAAC;AAElE,KAAK,UAAU,SAAS;IACtB,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,WAAW,CAAC;IAClD,IAAI,CAAC;QACH,WAAW,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAA6C,CAAC;QAC3F,OAAO,WAAW,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,WAAW,GAAG,IAAI,CAAC;QACnB,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,OAAO,CAAC,MAAM,SAAS,EAAE,CAAC,KAAK,IAAI,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAgB,EAChB,YAAwB,EACxB,OAAmB,EACnB,cAAqD,EACrD,gBAAwB,SAAS;IAEjC,IAAI,CAAC,2BAA2B,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,YAAY,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IAC5C,IAAI,YAAY,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAErC,MAAM,KAAK,GAAG,MAAM,SAAS,EAAE,CAAC;IAChC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,kEAAkE;YAClE,+CAA+C,CAChD,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC;IAE9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,yFAAyF;QACzF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7F,MAAM,QAAQ,GAAG,GAAG,CAAC,iBAAiB,EAAE,CAAC;QACzC,MAAM,UAAU,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;QAC5C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAClD,MAAM,UAAU,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAE9C,IAAI,UAAU,KAAK,YAAY,EAAE,CAAC;YAChC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;QACpD,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
package/dist/hd.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { HdPrivateNodeValid } from '@bitauth/libauth';
2
+ /**
3
+ * Derive RPA scan and spend keypairs from an HD account node.
4
+ *
5
+ * Selene convention: chain 3 = RPA keys.
6
+ * scan = accountNode/3/0
7
+ * spend = accountNode/3/1
8
+ *
9
+ * Given Selene's default path m/44'/145'/0', this produces:
10
+ * scan = m/44'/145'/0'/3/0
11
+ * spend = m/44'/145'/0'/3/1
12
+ *
13
+ * This is a wallet convention, not a protocol requirement. Other wallets may
14
+ * use different derivation paths. Use the `options` parameter to customize indices.
15
+ */
16
+ export declare function deriveRpaKeys(accountNode: HdPrivateNodeValid, options?: {
17
+ chainIndex?: number;
18
+ scanIndex?: number;
19
+ spendIndex?: number;
20
+ }): {
21
+ scanPrivkey: Uint8Array;
22
+ scanPubkey: Uint8Array;
23
+ spendPrivkey: Uint8Array;
24
+ spendPubkey: Uint8Array;
25
+ };
26
+ //# sourceMappingURL=hd.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hd.d.ts","sourceRoot":"","sources":["../src/hd.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,kBAAkB,EAC/B,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE;IACD,WAAW,EAAE,UAAU,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,YAAY,EAAE,UAAU,CAAC;IACzB,WAAW,EAAE,UAAU,CAAC;CACzB,CAiCA"}
package/dist/hd.js ADDED
@@ -0,0 +1,47 @@
1
+ import { deriveHdPrivateNodeChild, secp256k1 } from '@bitauth/libauth';
2
+ /**
3
+ * Derive RPA scan and spend keypairs from an HD account node.
4
+ *
5
+ * Selene convention: chain 3 = RPA keys.
6
+ * scan = accountNode/3/0
7
+ * spend = accountNode/3/1
8
+ *
9
+ * Given Selene's default path m/44'/145'/0', this produces:
10
+ * scan = m/44'/145'/0'/3/0
11
+ * spend = m/44'/145'/0'/3/1
12
+ *
13
+ * This is a wallet convention, not a protocol requirement. Other wallets may
14
+ * use different derivation paths. Use the `options` parameter to customize indices.
15
+ */
16
+ export function deriveRpaKeys(accountNode, options) {
17
+ const chainIndex = options?.chainIndex ?? 3;
18
+ const scanIndex = options?.scanIndex ?? 0;
19
+ const spendIndex = options?.spendIndex ?? 1;
20
+ const rpaChain = deriveHdPrivateNodeChild(accountNode, chainIndex);
21
+ if (typeof rpaChain === 'string') {
22
+ throw new Error(`HD derivation failed for chain ${chainIndex}: ${rpaChain}`);
23
+ }
24
+ const scanNode = deriveHdPrivateNodeChild(rpaChain, scanIndex);
25
+ if (typeof scanNode === 'string') {
26
+ throw new Error(`HD derivation failed for scan key (${chainIndex}/${scanIndex}): ${scanNode}`);
27
+ }
28
+ const spendNode = deriveHdPrivateNodeChild(rpaChain, spendIndex);
29
+ if (typeof spendNode === 'string') {
30
+ throw new Error(`HD derivation failed for spend key (${chainIndex}/${spendIndex}): ${spendNode}`);
31
+ }
32
+ const scanPubkey = secp256k1.derivePublicKeyCompressed(scanNode.privateKey);
33
+ if (typeof scanPubkey === 'string') {
34
+ throw new Error(`Failed to derive scan pubkey: ${scanPubkey}`);
35
+ }
36
+ const spendPubkey = secp256k1.derivePublicKeyCompressed(spendNode.privateKey);
37
+ if (typeof spendPubkey === 'string') {
38
+ throw new Error(`Failed to derive spend pubkey: ${spendPubkey}`);
39
+ }
40
+ return {
41
+ scanPrivkey: scanNode.privateKey,
42
+ scanPubkey,
43
+ spendPrivkey: spendNode.privateKey,
44
+ spendPubkey,
45
+ };
46
+ }
47
+ //# sourceMappingURL=hd.js.map
package/dist/hd.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hd.js","sourceRoot":"","sources":["../src/hd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGvE;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,WAA+B,EAC/B,OAA0E;IAO1E,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,OAAO,EAAE,UAAU,IAAI,CAAC,CAAC;IAE5C,MAAM,QAAQ,GAAG,wBAAwB,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IACnE,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,kCAAkC,UAAU,KAAK,QAAQ,EAAE,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,QAAQ,GAAG,wBAAwB,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC/D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,sCAAsC,UAAU,IAAI,SAAS,MAAM,QAAQ,EAAE,CAAC,CAAC;IACjG,CAAC;IACD,MAAM,SAAS,GAAG,wBAAwB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACjE,IAAI,OAAO,SAAS,KAAK,QAAQ,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,uCAAuC,UAAU,IAAI,UAAU,MAAM,SAAS,EAAE,CAAC,CAAC;IACpG,CAAC;IAED,MAAM,UAAU,GAAG,SAAS,CAAC,yBAAyB,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC5E,IAAI,OAAO,UAAU,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,iCAAiC,UAAU,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,WAAW,GAAG,SAAS,CAAC,yBAAyB,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IAC9E,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,kCAAkC,WAAW,EAAE,CAAC,CAAC;IACnE,CAAC;IAED,OAAO;QACL,WAAW,EAAE,QAAQ,CAAC,UAAU;QAChC,UAAU;QACV,YAAY,EAAE,SAAS,CAAC,UAAU;QAClC,WAAW;KACZ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ export type { Paycode, RpaSharedSecret, RpaPaymentAddress, RpaMatch, RpaScanInput, RpaScanOutput, RpaScanTransaction, FulcrumRpaHistoryItem, } from './types.js';
2
+ export { generatePaycode, encodePaycode, decodePaycode, validatePaycode, getScanPrefix, isExpired, CASHCODE_PREFIX, CASHCODE_TEST_PREFIX, } from './paycode.js';
3
+ export { computeSharedSecret } from './secret.js';
4
+ export { derivePaymentAddress, deriveSpendingKey } from './derive.js';
5
+ export { prepareSend, selectDesignatedInput } from './send.js';
6
+ export { extractPubkeyFromScriptSig, parseRawTransaction, parseVerboseTransaction, } from './parse.js';
7
+ export { tryTransaction, tryTransactions, scanTransaction, matchesPrefix, } from './scan.js';
8
+ export { grindPrefix, isGrindingAvailable } from './grind.js';
9
+ export { deriveRpaKeys } from './hd.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,OAAO,EACP,eAAe,EACf,iBAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,eAAe,EACf,aAAa,EACb,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,eAAe,EACf,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAE/D,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { generatePaycode, encodePaycode, decodePaycode, validatePaycode, getScanPrefix, isExpired, CASHCODE_PREFIX, CASHCODE_TEST_PREFIX, } from './paycode.js';
2
+ export { computeSharedSecret } from './secret.js';
3
+ export { derivePaymentAddress, deriveSpendingKey } from './derive.js';
4
+ export { prepareSend, selectDesignatedInput } from './send.js';
5
+ export { extractPubkeyFromScriptSig, parseRawTransaction, parseVerboseTransaction, } from './parse.js';
6
+ export { tryTransaction, tryTransactions, scanTransaction, matchesPrefix, } from './scan.js';
7
+ export { grindPrefix, isGrindingAvailable } from './grind.js';
8
+ export { deriveRpaKeys } from './hd.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EACL,eAAe,EACf,aAAa,EACb,aAAa,EACb,eAAe,EACf,aAAa,EACb,SAAS,EACT,eAAe,EACf,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAElD,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAE/D,OAAO,EACL,0BAA0B,EAC1B,mBAAmB,EACnB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,cAAc,EACd,eAAe,EACf,eAAe,EACf,aAAa,GACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE9D,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { RpaScanTransaction } from './types.js';
2
+ /**
3
+ * Extract a compressed public key from a P2PKH scriptSig (unlocking bytecode).
4
+ * P2PKH scriptSig format: <sig_len> <sig> 0x21 <33-byte compressed pubkey>
5
+ * Returns null if not a standard P2PKH input.
6
+ */
7
+ export declare function extractPubkeyFromScriptSig(scriptSig: Uint8Array): Uint8Array | null;
8
+ /**
9
+ * Parse a raw transaction hex string into an RpaScanTransaction.
10
+ * Extracts pubkeys from P2PKH scriptSigs, computes txid, and builds output addresses.
11
+ */
12
+ export declare function parseRawTransaction(txHex: string, testnet?: boolean): RpaScanTransaction;
13
+ /**
14
+ * Parse a Fulcrum verbose transaction JSON into an RpaScanTransaction.
15
+ */
16
+ export declare function parseVerboseTransaction(verboseTx: {
17
+ txid: string;
18
+ vin: Array<{
19
+ txid: string;
20
+ vout: number;
21
+ scriptSig: {
22
+ hex: string;
23
+ };
24
+ }>;
25
+ vout: Array<{
26
+ value: number;
27
+ scriptPubKey: {
28
+ hex: string;
29
+ addresses?: string[];
30
+ };
31
+ }>;
32
+ }, testnet?: boolean): RpaScanTransaction;
33
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAA+B,MAAM,YAAY,CAAC;AAElF;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAanF;AA+BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,OAAe,GACvB,kBAAkB,CA2BpB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE;IACT,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,KAAK,CAAC;QACT,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAC;KAC5B,CAAC,CAAC;IACH,IAAI,EAAE,KAAK,CAAC;QACV,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC;KACrD,CAAC,CAAC;CACJ,EACD,OAAO,GAAE,OAAe,GACvB,kBAAkB,CAuBpB"}
package/dist/parse.js ADDED
@@ -0,0 +1,106 @@
1
+ import { decodeTransaction, hexToBin, binToHex, sha256, lockingBytecodeToCashAddress, encodeTransactionInput, } from '@bitauth/libauth';
2
+ /**
3
+ * Extract a compressed public key from a P2PKH scriptSig (unlocking bytecode).
4
+ * P2PKH scriptSig format: <sig_len> <sig> 0x21 <33-byte compressed pubkey>
5
+ * Returns null if not a standard P2PKH input.
6
+ */
7
+ export function extractPubkeyFromScriptSig(scriptSig) {
8
+ // Minimum: 1-byte push + some sig + 1-byte push (0x21) + 33 bytes pubkey
9
+ if (scriptSig.length < 36)
10
+ return null;
11
+ // Check if the last 34 bytes are: 0x21 (push 33 bytes) followed by 33 bytes
12
+ const pushBytePos = scriptSig.length - 34;
13
+ if (scriptSig[pushBytePos] !== 0x21)
14
+ return null;
15
+ const pubkey = scriptSig.slice(pushBytePos + 1);
16
+ // Compressed pubkey starts with 0x02 or 0x03
17
+ if (pubkey[0] !== 0x02 && pubkey[0] !== 0x03)
18
+ return null;
19
+ return pubkey;
20
+ }
21
+ /**
22
+ * Compute a transaction ID (double SHA256 of raw bytes, reversed to display order).
23
+ */
24
+ function computeTxid(rawBytes) {
25
+ const hash = sha256.hash(sha256.hash(rawBytes));
26
+ // Reverse for display order
27
+ const reversed = new Uint8Array(hash.length);
28
+ for (let i = 0; i < hash.length; i++) {
29
+ reversed[i] = hash[hash.length - 1 - i];
30
+ }
31
+ return binToHex(reversed);
32
+ }
33
+ /**
34
+ * Convert a locking bytecode to a CashAddr string.
35
+ * Returns empty string for non-standard scripts.
36
+ */
37
+ function lockingBytecodeToAddress(bytecode, testnet) {
38
+ const result = lockingBytecodeToCashAddress({
39
+ bytecode,
40
+ prefix: testnet ? 'bchtest' : 'bitcoincash',
41
+ });
42
+ if (typeof result === 'string')
43
+ return '';
44
+ return result.address;
45
+ }
46
+ /**
47
+ * Parse a raw transaction hex string into an RpaScanTransaction.
48
+ * Extracts pubkeys from P2PKH scriptSigs, computes txid, and builds output addresses.
49
+ */
50
+ export function parseRawTransaction(txHex, testnet = false) {
51
+ const rawBytes = hexToBin(txHex);
52
+ const decoded = decodeTransaction(rawBytes);
53
+ if (typeof decoded === 'string') {
54
+ throw new Error(`Failed to decode transaction: ${decoded}`);
55
+ }
56
+ const txid = computeTxid(rawBytes);
57
+ const inputs = decoded.inputs.map((input) => {
58
+ const pubkey = extractPubkeyFromScriptSig(input.unlockingBytecode);
59
+ const outpointTxid = binToHex(input.outpointTransactionHash);
60
+ return {
61
+ pubkey,
62
+ serializedInput: encodeTransactionInput(input),
63
+ outpointTxid,
64
+ outpointIndex: input.outpointIndex,
65
+ };
66
+ });
67
+ const outputs = decoded.outputs.map((output) => ({
68
+ address: lockingBytecodeToAddress(output.lockingBytecode, testnet),
69
+ value: output.valueSatoshis,
70
+ }));
71
+ return { txid, inputs, outputs };
72
+ }
73
+ /**
74
+ * Parse a Fulcrum verbose transaction JSON into an RpaScanTransaction.
75
+ */
76
+ export function parseVerboseTransaction(verboseTx, testnet = false) {
77
+ const inputs = verboseTx.vin.map((vin) => {
78
+ const scriptSigBytes = hexToBin(vin.scriptSig.hex);
79
+ const pubkey = extractPubkeyFromScriptSig(scriptSigBytes);
80
+ return {
81
+ pubkey,
82
+ serializedInput: new Uint8Array(0), // Not needed for Fulcrum-backed scanning
83
+ outpointTxid: vin.txid,
84
+ outpointIndex: vin.vout,
85
+ };
86
+ });
87
+ const outputs = verboseTx.vout.map((vout) => {
88
+ // Convert BCH amount to satoshis using string math to avoid IEEE 754 precision loss
89
+ const valueSatoshis = bchToSatoshis(vout.value);
90
+ // Derive address from scriptPubKey hex
91
+ const lockingBytecode = hexToBin(vout.scriptPubKey.hex);
92
+ const address = vout.scriptPubKey.addresses?.[0]
93
+ ?? lockingBytecodeToAddress(lockingBytecode, testnet);
94
+ return { address, value: valueSatoshis };
95
+ });
96
+ return { txid: verboseTx.txid, inputs, outputs };
97
+ }
98
+ /** Convert a BCH float value to satoshis using string math to avoid IEEE 754 issues. */
99
+ function bchToSatoshis(value) {
100
+ // Convert to string, split on decimal point, pad/truncate to 8 decimal places
101
+ const str = value.toFixed(8);
102
+ const [whole, frac] = str.split('.');
103
+ const satStr = whole + (frac ?? '').padEnd(8, '0').slice(0, 8);
104
+ return BigInt(satStr);
105
+ }
106
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}