did-beta-bch 3.0.0 → 3.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/PROTOCOL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Cashlet DID Protocol Specification
2
2
 
3
3
  Version: `3.0.0`
4
- Status: Draft
4
+ Status: Final
5
5
 
6
6
  Cashlet DID v3 is a breaking protocol update. Conforming verifiers MUST reject
7
7
  v1 and v2 binding records. Existing identities must be rebound and republished.
package/README.md CHANGED
@@ -200,11 +200,18 @@ commitments or attach them to a binding event automatically.
200
200
  import {
201
201
  buildAnchorTx, verifyBchAnchorProof,
202
202
  BCH_MAINNET_CHECKPOINT, BCH_MAINNET_ASERT_PARAMETERS,
203
+ BCH_CHIPNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS,
204
+ broadcastAnchorTx, fetchAnchorProof, wsTransport,
205
+ ELECTRUM_MAINNET_WS,
203
206
  } from 'did-beta-bch'
204
207
 
205
- // Embed bindingId in an OP_RETURN transaction for on-chain commitment
208
+ // Build and broadcast
206
209
  const { txHex } = await buildAnchorTx(bindingId, { txid, vout, valueSats, lockingBytecode, privateKey })
207
- // Independently verify transaction, Merkle proof, headers, and depth
210
+ const transport = wsTransport(ELECTRUM_MAINNET_WS)
211
+ const txid = await broadcastAnchorTx(txHex, transport)
212
+
213
+ // Fetch proof and verify (after confirmation)
214
+ const proof = await fetchAnchorProof(txid, BCH_MAINNET_CHECKPOINT.height, wsTransport(ELECTRUM_MAINNET_WS))
208
215
  const result = verifyBchAnchorProof(proof, bindingId, {
209
216
  checkpoint: BCH_MAINNET_CHECKPOINT,
210
217
  asert: BCH_MAINNET_ASERT_PARAMETERS,
@@ -212,11 +219,30 @@ const result = verifyBchAnchorProof(proof, bindingId, {
212
219
  })
213
220
  ```
214
221
 
215
- The library does not broadcast the transaction or fetch proofs. Mainnet
216
- verification uses a fixed checkpoint and is checkpointed SPV, not full-node
217
- validation; a supplied chain does not prove it is the globally highest-work
218
- branch. There is no built-in chipnet checkpoint, so chipnet anchors must not
219
- be presented as verified.
222
+ `wsTransport` works in both browser and Node.js (Node 22+ has native WebSocket). For Node < 22 or TCP, implement the three-method `ElectrumTransport` interface.
223
+
224
+ Chipnet uses `BCH_CHIPNET_CHECKPOINT` and `BCH_CHIPNET_ASERT_PARAMETERS`. Note chipnet is a test network that may reset; the checkpoint is valid for the current chain instance only.
225
+
226
+ ### BIP47 payment code address derivation
227
+
228
+ ```ts
229
+ import { parsePaymentCode, derivePaymentAddress } from 'did-beta-bch'
230
+ import { deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode } from '@bitauth/libauth'
231
+
232
+ // Decode a recipient's payment code
233
+ const { publicKey, chainCode } = parsePaymentCode('PM8T...')
234
+
235
+ // Derive the recipient's HD public node from their payment code
236
+ // (or supply it directly if you have it from your own derivation)
237
+ const recipientNode = deriveHdPublicNode(deriveHdPath(recipientRoot, "m/47'/0'/0'"))
238
+
239
+ // Sender: derive the nth payment address to send to
240
+ const senderScanPrivkey = deriveHdPath(senderRoot, "m/47'/0'/0'/0").privateKey
241
+ const address = await derivePaymentAddress(senderScanPrivkey, recipientNode, 0)
242
+ wallet.sendTo(address, amount)
243
+ ```
244
+
245
+ `derivePaymentAddress` implements BIP47 v1 ECDH address derivation. Notification transactions and spending-side key derivation are wallet-specific and out of scope for this library.
220
246
 
221
247
  ### NIP-05 handle resolution
222
248
 
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Electrum client helpers for broadcasting anchor transactions and fetching
3
+ * SPV proofs. Works in Node.js (TCP) and browsers (WebSocket) via a pluggable
4
+ * transport. Default transports are provided for each environment.
5
+ */
6
+ import type { BchAnchorProof } from "./anchor-verification.js";
7
+ export declare const ELECTRUM_MAINNET_TCP = "electroncash.dk:50001";
8
+ export declare const ELECTRUM_MAINNET_WS = "wss://electroncash.dk:50004";
9
+ export declare const ELECTRUM_CHIPNET_TCP = "chipnet.imaginary.cash:50001";
10
+ export declare const ELECTRUM_CHIPNET_WS = "wss://chipnet.imaginary.cash:50004";
11
+ /** Minimal transport interface — send lines, receive lines. */
12
+ export interface ElectrumTransport {
13
+ send(line: string): void;
14
+ onLine(handler: (line: string) => void): void;
15
+ onError(handler: (err: Error) => void): void;
16
+ close(): void;
17
+ }
18
+ /** WebSocket transport — works in both browser and Node.js (Node 22+ has native WebSocket). */
19
+ export declare function wsTransport(url: string): ElectrumTransport;
20
+ /**
21
+ * Broadcast a signed anchor transaction hex to an Electrum server.
22
+ * Returns the txid on success.
23
+ */
24
+ export declare function broadcastAnchorTx(txHex: string, transport: ElectrumTransport): Promise<string>;
25
+ /**
26
+ * Fetch a BchAnchorProof for a confirmed transaction.
27
+ * checkpointHeight must match the checkpoint you intend to verify against —
28
+ * headers are fetched from checkpointHeight+1 through the confirmation window.
29
+ */
30
+ export declare function fetchAnchorProof(txid: string, checkpointHeight: number, transport: ElectrumTransport, minConfirmations?: number): Promise<BchAnchorProof>;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Electrum client helpers for broadcasting anchor transactions and fetching
3
+ * SPV proofs. Works in Node.js (TCP) and browsers (WebSocket) via a pluggable
4
+ * transport. Default transports are provided for each environment.
5
+ */
6
+ export const ELECTRUM_MAINNET_TCP = "electroncash.dk:50001";
7
+ export const ELECTRUM_MAINNET_WS = "wss://electroncash.dk:50004";
8
+ export const ELECTRUM_CHIPNET_TCP = "chipnet.imaginary.cash:50001";
9
+ export const ELECTRUM_CHIPNET_WS = "wss://chipnet.imaginary.cash:50004";
10
+ async function electrumCall(transport, requests, timeoutMs = 15000) {
11
+ const results = new Map();
12
+ return new Promise((resolve, reject) => {
13
+ const timer = setTimeout(() => { transport.close(); reject(new Error("Electrum request timed out")); }, timeoutMs);
14
+ transport.onError(e => { clearTimeout(timer); reject(e); });
15
+ transport.onLine(line => {
16
+ if (!line.trim())
17
+ return;
18
+ const msg = JSON.parse(line);
19
+ if (msg.error) {
20
+ clearTimeout(timer);
21
+ transport.close();
22
+ reject(new Error(msg.error.message));
23
+ return;
24
+ }
25
+ results.set(msg.id, msg.result);
26
+ if (results.size === requests.length) {
27
+ clearTimeout(timer);
28
+ transport.close();
29
+ resolve(results);
30
+ }
31
+ });
32
+ for (const req of requests)
33
+ transport.send(JSON.stringify(req) + "\n");
34
+ });
35
+ }
36
+ /** WebSocket transport — works in both browser and Node.js (Node 22+ has native WebSocket). */
37
+ export function wsTransport(url) {
38
+ const ws = new WebSocket(url, "binary");
39
+ let lineHandler = null;
40
+ let errHandler = null;
41
+ let buf = "";
42
+ ws.onmessage = (ev) => {
43
+ buf += typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);
44
+ const lines = buf.split("\n");
45
+ buf = lines.pop() ?? "";
46
+ for (const l of lines)
47
+ lineHandler?.(l);
48
+ };
49
+ ws.onerror = () => errHandler?.(new Error("WebSocket error"));
50
+ return {
51
+ send: (line) => ws.readyState === WebSocket.OPEN ? ws.send(line) : ws.addEventListener("open", () => ws.send(line)),
52
+ onLine: (h) => { lineHandler = h; },
53
+ onError: (h) => { errHandler = h; },
54
+ close: () => ws.close(),
55
+ };
56
+ }
57
+ /**
58
+ * Broadcast a signed anchor transaction hex to an Electrum server.
59
+ * Returns the txid on success.
60
+ */
61
+ export async function broadcastAnchorTx(txHex, transport) {
62
+ const results = await electrumCall(transport, [
63
+ { id: 1, method: "blockchain.transaction.broadcast", params: [txHex] },
64
+ ]);
65
+ const txid = results.get(1);
66
+ if (typeof txid !== "string" || !/^[0-9a-f]{64}$/.test(txid))
67
+ throw new Error(`Unexpected broadcast response: ${JSON.stringify(txid)}`);
68
+ return txid;
69
+ }
70
+ /**
71
+ * Fetch a BchAnchorProof for a confirmed transaction.
72
+ * checkpointHeight must match the checkpoint you intend to verify against —
73
+ * headers are fetched from checkpointHeight+1 through the confirmation window.
74
+ */
75
+ export async function fetchAnchorProof(txid, checkpointHeight, transport, minConfirmations = 6) {
76
+ if (!/^[0-9a-f]{64}$/.test(txid))
77
+ throw new Error("Invalid txid");
78
+ // Step 1: get Merkle proof to learn blockHeight and tx position
79
+ const merkleResults = await electrumCall(transport, [
80
+ { id: 1, method: "blockchain.transaction.get_merkle", params: [txid, 0] },
81
+ ]);
82
+ const merkle = merkleResults.get(1);
83
+ if (!merkle?.block_height)
84
+ throw new Error("Transaction not yet confirmed or not found");
85
+ const blockHeight = merkle.block_height;
86
+ const tipHeight = blockHeight + minConfirmations - 1;
87
+ const startHeight = checkpointHeight + 1;
88
+ const count = tipHeight - startHeight + 1;
89
+ if (count <= 0)
90
+ throw new Error("Block is at or below the checkpoint height");
91
+ // Step 2: fetch raw tx hex + header range
92
+ const batchResults = await electrumCall(transport, [
93
+ { id: 2, method: "blockchain.transaction.get", params: [txid, false] },
94
+ { id: 3, method: "blockchain.block.headers", params: [startHeight, count] },
95
+ ]);
96
+ const transactionHex = batchResults.get(2);
97
+ const headersResult = batchResults.get(3);
98
+ if (typeof transactionHex !== "string")
99
+ throw new Error("Failed to fetch transaction hex");
100
+ if (!headersResult?.hex)
101
+ throw new Error("Failed to fetch header chain");
102
+ // Split concatenated 80-byte headers (160 hex chars each)
103
+ const headers = [];
104
+ for (let i = 0; i < headersResult.count; i++)
105
+ headers.push(headersResult.hex.slice(i * 160, i * 160 + 160));
106
+ return { transactionHex, blockHeight, transactionIndex: merkle.pos, merkleBranch: merkle.merkle, headers };
107
+ }
@@ -42,3 +42,5 @@ export interface BchAnchorVerificationResult {
42
42
  export declare function verifyBchAnchorProof(proof: BchAnchorProof, bindingId: string, options: BchAnchorVerificationOptions): BchAnchorVerificationResult;
43
43
  export declare const BCH_MAINNET_ASERT_PARAMETERS: BchAsertParameters;
44
44
  export declare const BCH_MAINNET_CHECKPOINT: BchSpvCheckpoint;
45
+ export declare const BCH_CHIPNET_ASERT_PARAMETERS: BchAsertParameters;
46
+ export declare const BCH_CHIPNET_CHECKPOINT: BchSpvCheckpoint;
@@ -227,9 +227,9 @@ export const BCH_MAINNET_ASERT_PARAMETERS = {
227
227
  anchorPreviousBlockTime: 1605447844,
228
228
  powLimitBits: 0x1d00ffff,
229
229
  };
230
- // Fixed trust root, cross-checked against two independent BCH mainnet Electrum
231
- // servers. Refresh in a reviewed release as it ages; never fetch trust roots
232
- // from the same untrusted endpoint that supplies a proof.
230
+ // Fixed trust root, cross-checked against electroncash.dk and a second independent
231
+ // Electrum server. Refresh in a reviewed release as it ages; never fetch trust
232
+ // roots from the same untrusted endpoint that supplies a proof.
233
233
  export const BCH_MAINNET_CHECKPOINT = {
234
234
  height: 975752,
235
235
  hash: "0000000000000000010a200fe779aa3c313c7fc1b355b13560ef105709d153fd",
@@ -248,3 +248,35 @@ export const BCH_MAINNET_CHECKPOINT = {
248
248
  1793697770,
249
249
  ],
250
250
  };
251
+ // Chipnet ASERT anchor: block 16844 (November 2020 upgrade).
252
+ // anchorPreviousBlockTime is the timestamp of block 16843.
253
+ // Cross-checked against chipnet.imaginary.cash and chipnet.bch.ninja.
254
+ export const BCH_CHIPNET_ASERT_PARAMETERS = {
255
+ anchorHeight: 16844,
256
+ anchorBits: 0x1d00ffff,
257
+ anchorPreviousBlockTime: 1605451779,
258
+ powLimitBits: 0x1d00ffff,
259
+ };
260
+ // Fixed chipnet trust root at height 330718.
261
+ // Cross-checked against chipnet.imaginary.cash and chipnet.bch.ninja.
262
+ // WARNING: chipnet is a test network that may be reset. This checkpoint is
263
+ // valid for the current chipnet instance only. After a reset, callers must
264
+ // supply a new checkpoint derived from the post-reset chain.
265
+ export const BCH_CHIPNET_CHECKPOINT = {
266
+ height: 330718,
267
+ hash: "00000000ad6fb02510bdccdf5eb9c2d4d0f16cfaa721a0450a6c95fef549ee16",
268
+ headerHex: "040000004c9554d6ad2ce73070ececb406327f3f4b843146056fc64d0f010000000000003cefeb99df9976c9abc99da3a773405e8e2330603729ad0ce149071ad7868ee164cde96affff001d9f3db255",
269
+ recentTimestamps: [
270
+ 1793701553,
271
+ 1793701753,
272
+ 1793702955,
273
+ 1793703300,
274
+ 1793703430,
275
+ 1793704246,
276
+ 1793704910,
277
+ 1793704951,
278
+ 1793705277,
279
+ 1793706160,
280
+ 1793707364,
281
+ ],
282
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * BIP47 v1 reusable payment code helpers.
3
+ * Notification transactions and scanning are wallet-specific and out of scope.
4
+ */
5
+ import { deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode, type HdPublicNodeValid, type HdPrivateNodeValid } from "@bitauth/libauth";
6
+ export interface PaymentCode {
7
+ /** Compressed public key (33 bytes) */
8
+ publicKey: Uint8Array;
9
+ /** Chain code (32 bytes) */
10
+ chainCode: Uint8Array;
11
+ }
12
+ export interface EncodedPaymentCode extends PaymentCode {
13
+ /** Base58Check encoded payment code string ("PM8T...") */
14
+ encoded: string;
15
+ }
16
+ /**
17
+ * Encode a BIP47 v1 payment code from a public key and chain code.
18
+ */
19
+ export declare function encodePaymentCode(publicKey: Uint8Array, chainCode: Uint8Array): Promise<string>;
20
+ /**
21
+ * Decode a BIP47 v1 Base58Check payment code string.
22
+ */
23
+ export declare function parsePaymentCode(paymentCode: string): PaymentCode;
24
+ /**
25
+ * Derive a BIP47 payment code from a mnemonic (m/47'/0'/<account>').
26
+ */
27
+ export declare function derivePaymentCode(mnemonic: string, account?: number): Promise<EncodedPaymentCode>;
28
+ /**
29
+ * Derive the nth P2PKH CashAddr payment address for sending to a recipient.
30
+ *
31
+ * @param senderScanPrivkey - sender's M/47'/0'/0'/0 private key (32 bytes)
32
+ * @param recipientNode - recipient's BIP47 HD public node (M/47'/0'/0')
33
+ * @param index - address index (0-based)
34
+ * @param prefix - CashAddr prefix, defaults to "bitcoincash"
35
+ */
36
+ export declare function derivePaymentAddress(senderScanPrivkey: Uint8Array, recipientNode: HdPublicNodeValid, index: number, prefix?: "bitcoincash" | "bchtest" | "bchreg"): Promise<string>;
37
+ /**
38
+ * Derive the nth payment private key for a receiver to spend funds sent via BIP47.
39
+ *
40
+ * Per BIP47 spec: s = SHA256(b[i] * A) where A = sender's scan pubkey (M/47'/0'/0'/0)
41
+ * b' = b[i] + s
42
+ *
43
+ * @param receiverAccountNode - receiver's M/47'/0'/0' private node
44
+ * @param senderScanPubkey - sender's M/47'/0'/0'/0 compressed public key (33 bytes)
45
+ * @param index - address index (0-based)
46
+ */
47
+ export declare function derivePaymentPrivkey(receiverAccountNode: HdPrivateNodeValid, senderScanPubkey: Uint8Array, index: number): Promise<Uint8Array>;
48
+ export { deriveHdPublicNode, deriveHdPath, deriveHdPrivateNodeFromBip39Mnemonic };
49
+ /** Construct a minimal HdPublicNodeValid from a BIP47 PaymentCode for use with derivePaymentAddress/derivePaymentPrivkey. */
50
+ export declare function hdPublicNodeFromPaymentCode(pc: PaymentCode): HdPublicNodeValid;
package/dist/bip47.js ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * BIP47 v1 reusable payment code helpers.
3
+ * Notification transactions and scanning are wallet-specific and out of scope.
4
+ */
5
+ import { secp256k1, hash160, encodeCashAddress, deriveHdPublicNodeChild, deriveHdPrivateNodeChild, deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode, } from "@bitauth/libauth";
6
+ const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
7
+ // BIP47 payment code: [0x47][0x01][0x00][pubkey 33][chaincode 32][reserved 13][checksum 4] = 85 bytes
8
+ const PC_VERSION = 0x47;
9
+ const PC_TOTAL_BYTES = 85;
10
+ const PC_PAYLOAD_BYTES = 80;
11
+ function base58Encode(bytes) {
12
+ let n = 0n;
13
+ for (const b of bytes)
14
+ n = n * 256n + BigInt(b);
15
+ let result = "";
16
+ while (n > 0n) {
17
+ result = BASE58_ALPHABET[Number(n % 58n)] + result;
18
+ n /= 58n;
19
+ }
20
+ for (const b of bytes) {
21
+ if (b !== 0)
22
+ break;
23
+ result = "1" + result;
24
+ }
25
+ return result;
26
+ }
27
+ function base58Decode(s) {
28
+ let n = 0n;
29
+ for (const c of s) {
30
+ const d = BASE58_ALPHABET.indexOf(c);
31
+ if (d < 0)
32
+ throw new Error(`Invalid Base58 character: ${c}`);
33
+ n = n * 58n + BigInt(d);
34
+ }
35
+ const hex = n.toString(16).padStart(PC_TOTAL_BYTES * 2, "0");
36
+ const bytes = new Uint8Array(hex.length / 2);
37
+ for (let i = 0; i < bytes.length; i++)
38
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
39
+ let leading = 0;
40
+ for (const c of s) {
41
+ if (c === "1")
42
+ leading++;
43
+ else
44
+ break;
45
+ }
46
+ const result = new Uint8Array(leading + bytes.length);
47
+ result.set(bytes, leading);
48
+ return result;
49
+ }
50
+ async function sha256Once(data) {
51
+ if (typeof crypto !== "undefined" && crypto.subtle)
52
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ const nc = await Function('return import("crypto")')();
55
+ return nc.createHash("sha256").update(data).digest();
56
+ }
57
+ async function paymentCodeChecksum(versionAndPayload) {
58
+ return (await sha256Once(await sha256Once(versionAndPayload))).slice(0, 4);
59
+ }
60
+ /**
61
+ * Encode a BIP47 v1 payment code from a public key and chain code.
62
+ */
63
+ export async function encodePaymentCode(publicKey, chainCode) {
64
+ if (publicKey.length !== 33)
65
+ throw new Error("publicKey must be 33 bytes");
66
+ if (chainCode.length !== 32)
67
+ throw new Error("chainCode must be 32 bytes");
68
+ const full = new Uint8Array(PC_TOTAL_BYTES);
69
+ full[0] = PC_VERSION;
70
+ full[1] = 0x01; // payload version
71
+ full[2] = 0x00; // feature bits
72
+ full.set(publicKey, 3);
73
+ full.set(chainCode, 36);
74
+ // bytes 69-80 are reserved zeros
75
+ full.set(await paymentCodeChecksum(full.slice(0, PC_PAYLOAD_BYTES + 1)), PC_PAYLOAD_BYTES + 1);
76
+ return base58Encode(full);
77
+ }
78
+ /**
79
+ * Decode a BIP47 v1 Base58Check payment code string.
80
+ */
81
+ export function parsePaymentCode(paymentCode) {
82
+ const decoded = base58Decode(paymentCode);
83
+ if (decoded.length !== PC_TOTAL_BYTES)
84
+ throw new Error(`Invalid payment code length: ${decoded.length}`);
85
+ if (decoded[0] !== PC_VERSION)
86
+ throw new Error(`Unsupported payment code version: 0x${decoded[0].toString(16)}`);
87
+ const payload = decoded.slice(1, 1 + PC_PAYLOAD_BYTES);
88
+ if (payload[0] !== 0x01)
89
+ throw new Error(`Unsupported payment code payload version: ${payload[0]}`);
90
+ return {
91
+ publicKey: payload.slice(2, 35),
92
+ chainCode: payload.slice(35, 67),
93
+ };
94
+ }
95
+ /**
96
+ * Derive a BIP47 payment code from a mnemonic (m/47'/0'/<account>').
97
+ */
98
+ export async function derivePaymentCode(mnemonic, account = 0) {
99
+ const root = await deriveHdPrivateNodeFromBip39Mnemonic(mnemonic);
100
+ const node = deriveHdPath(root, `m/47'/0'/${account}'`);
101
+ if (typeof node === "string")
102
+ throw new Error(`BIP47 derivation failed: ${node}`);
103
+ const publicKey = secp256k1.derivePublicKeyCompressed(node.privateKey);
104
+ if (typeof publicKey === "string")
105
+ throw new Error(`Public key derivation failed: ${publicKey}`);
106
+ return { publicKey, chainCode: node.chainCode, encoded: await encodePaymentCode(publicKey, node.chainCode) };
107
+ }
108
+ /**
109
+ * Derive the nth P2PKH CashAddr payment address for sending to a recipient.
110
+ *
111
+ * @param senderScanPrivkey - sender's M/47'/0'/0'/0 private key (32 bytes)
112
+ * @param recipientNode - recipient's BIP47 HD public node (M/47'/0'/0')
113
+ * @param index - address index (0-based)
114
+ * @param prefix - CashAddr prefix, defaults to "bitcoincash"
115
+ */
116
+ export async function derivePaymentAddress(senderScanPrivkey, recipientNode, index, prefix = "bitcoincash") {
117
+ if (index < 0 || !Number.isSafeInteger(index))
118
+ throw new Error("Index must be a non-negative integer");
119
+ const childNode = deriveHdPublicNodeChild(recipientNode, index);
120
+ if (typeof childNode === "string")
121
+ throw new Error(`HD derivation failed: ${childNode}`);
122
+ const sharedPoint = secp256k1.mulTweakPublicKeyCompressed(childNode.publicKey, senderScanPrivkey);
123
+ if (typeof sharedPoint === "string")
124
+ throw new Error(`ECDH failed: ${sharedPoint}`);
125
+ const s = await sha256Once(sharedPoint.slice(1));
126
+ const paymentPubkey = secp256k1.addTweakPublicKeyCompressed(childNode.publicKey, s);
127
+ if (typeof paymentPubkey === "string")
128
+ throw new Error(`Pubkey tweak failed: ${paymentPubkey}`);
129
+ const result = encodeCashAddress({ prefix, type: "p2pkh", payload: hash160(paymentPubkey) });
130
+ return typeof result === "string" ? result : result.address;
131
+ }
132
+ /**
133
+ * Derive the nth payment private key for a receiver to spend funds sent via BIP47.
134
+ *
135
+ * Per BIP47 spec: s = SHA256(b[i] * A) where A = sender's scan pubkey (M/47'/0'/0'/0)
136
+ * b' = b[i] + s
137
+ *
138
+ * @param receiverAccountNode - receiver's M/47'/0'/0' private node
139
+ * @param senderScanPubkey - sender's M/47'/0'/0'/0 compressed public key (33 bytes)
140
+ * @param index - address index (0-based)
141
+ */
142
+ export async function derivePaymentPrivkey(receiverAccountNode, senderScanPubkey, index) {
143
+ if (index < 0 || !Number.isSafeInteger(index))
144
+ throw new Error("Index must be a non-negative integer");
145
+ // Receiver's child private key b[i]
146
+ const receiverChild = deriveHdPrivateNodeChild(receiverAccountNode, index);
147
+ if (typeof receiverChild === "string")
148
+ throw new Error(`HD child derivation failed: ${receiverChild}`);
149
+ // s = sha256(x-coord of b[i] * A)
150
+ const sharedPoint = secp256k1.mulTweakPublicKeyCompressed(senderScanPubkey, receiverChild.privateKey);
151
+ if (typeof sharedPoint === "string")
152
+ throw new Error(`ECDH failed: ${sharedPoint}`);
153
+ const s = await sha256Once(sharedPoint.slice(1));
154
+ // b' = b[i] + s (mod n)
155
+ const tweaked = secp256k1.addTweakPrivateKey(receiverChild.privateKey, s);
156
+ if (typeof tweaked === "string")
157
+ throw new Error(`Private key tweak failed: ${tweaked}`);
158
+ return tweaked;
159
+ }
160
+ export { deriveHdPublicNode, deriveHdPath, deriveHdPrivateNodeFromBip39Mnemonic };
161
+ /** Construct a minimal HdPublicNodeValid from a BIP47 PaymentCode for use with derivePaymentAddress/derivePaymentPrivkey. */
162
+ export function hdPublicNodeFromPaymentCode(pc) {
163
+ return {
164
+ publicKey: pc.publicKey,
165
+ chainCode: pc.chainCode,
166
+ depth: 0,
167
+ childIndex: 0,
168
+ parentFingerprint: new Uint8Array(4),
169
+ parentIdentifier: new Uint8Array(20),
170
+ };
171
+ }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export { buildDisclosure, verifyDisclosure, type Disclosure, type DisclosureReveal, } from "./disclosure.js";
2
+ export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, type PaymentCode, type EncodedPaymentCode, } from "./bip47.js";
2
3
  export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
3
- export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, type BchSpvCheckpoint, type BchAsertParameters, type BchAnchorProof, type BchAnchorVerificationOptions, type BchAnchorVerificationResult, } from "./anchor-verification.js";
4
+ export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS, BCH_CHIPNET_CHECKPOINT, type BchSpvCheckpoint, type BchAsertParameters, type BchAnchorProof, type BchAnchorVerificationOptions, type BchAnchorVerificationResult, } from "./anchor-verification.js";
5
+ export { broadcastAnchorTx, fetchAnchorProof, wsTransport, ELECTRUM_MAINNET_TCP, ELECTRUM_MAINNET_WS, ELECTRUM_CHIPNET_TCP, ELECTRUM_CHIPNET_WS, type ElectrumTransport, } from "./anchor-client.js";
4
6
  export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
5
7
  export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, ROTATION_AUTH_KIND, type Network, type CanonicalBinding, type IdentityBinding, type NostrEvent, buildBinding, bchSigningPayload, nostrEventTemplate, rotationAuthorizationTemplate, verifyRotationAuthorization, canonicalize, sha256hex, } from "./binding.js";
6
8
  export { BCH_PROOF_FORMAT, normalizeBchMessageSignature, signBchMessage, type BchMessageProof, type BchMessageSigner, type BchProofTag, } from "./signing.js";
7
9
  export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
8
- export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof, NOSTR_CHALLENGE_KIND, type DualChallengeProof, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, type BCHAnchorClaim, } from "./verify.js";
10
+ export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof, NOSTR_CHALLENGE_KIND, type DualChallengeProof, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, type BCHAnchorClaim, bitcoinSignedMessageHash, encodeBitcoinCompactSize, encodeCompactSignatureBase64, } from "./verify.js";
9
11
  export { CashletDidError, type CashletDidErrorCode } from "./errors.js";
10
12
  export { deriveNostrKeypair, type NostrKeypair, } from "./keygen.js";
11
13
  export { buildHttpAuthEventAsync, verifyHttpAuth, type HttpAuthEventTemplate, type HttpAuthResult, } from "./nip98.js";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { buildDisclosure, verifyDisclosure, } from "./disclosure.js";
2
+ export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, } from "./bip47.js";
2
3
  export { buildAnchorTx, ANCHOR_PREFIX, } from "./anchor.js";
3
- export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, } from "./anchor-verification.js";
4
+ export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS, BCH_CHIPNET_CHECKPOINT, } from "./anchor-verification.js";
5
+ export { broadcastAnchorTx, fetchAnchorProof, wsTransport, ELECTRUM_MAINNET_TCP, ELECTRUM_MAINNET_WS, ELECTRUM_CHIPNET_TCP, ELECTRUM_CHIPNET_WS, } from "./anchor-client.js";
4
6
  export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, } from "./credentials.js";
5
7
  export {
6
8
  // Protocol constants
@@ -13,7 +15,7 @@ export {
13
15
  publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, } from "./relay.js";
14
16
  export {
15
17
  // Verification
16
- resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof, NOSTR_CHALLENGE_KIND, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
18
+ resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof, NOSTR_CHALLENGE_KIND, verifyChallengeProof, resolveIdentityChain, bitcoinSignedMessageHash, encodeBitcoinCompactSize, encodeCompactSignatureBase64, } from "./verify.js";
17
19
  export { CashletDidError } from "./errors.js";
18
20
  export { deriveNostrKeypair, } from "./keygen.js";
19
21
  export { buildHttpAuthEventAsync, verifyHttpAuth, } from "./nip98.js";
package/dist/verify.d.ts CHANGED
@@ -31,6 +31,19 @@ export interface ResolvedIdentity {
31
31
  /** Unverified metadata claimed by the event; this library does not verify chain inclusion. */
32
32
  anchorClaim?: BCHAnchorClaim;
33
33
  }
34
+ /** Encode n as a Bitcoin CompactSize integer. */
35
+ export declare function encodeBitcoinCompactSize(n: number): Uint8Array;
36
+ /**
37
+ * Compute the double-SHA256 hash used by Bitcoin signed messages.
38
+ * Format: SHA256d("\x18Bitcoin Signed Message:\n" + varint(len) + message)
39
+ * Uses proper varint encoding — required for messages longer than 252 bytes.
40
+ */
41
+ export declare function bitcoinSignedMessageHash(message: string): Uint8Array;
42
+ /**
43
+ * Encode a 65-byte compact signature (recovery ID + 64-byte r||s) as Base64.
44
+ * Header byte = 31 + recoveryId (compressed key, recovery ID 0–3).
45
+ */
46
+ export declare function encodeCompactSignatureBase64(recoveryId: number, signature: Uint8Array): string;
34
47
  /** Resolve a BCH address from an npub via Nostr relays */
35
48
  export declare function resolveIdentity(npub: string, relays?: string[]): Promise<ResolvedIdentity | null>;
36
49
  /** Verify the BCH + Nostr signatures on a resolved identity */
package/dist/verify.js CHANGED
@@ -7,7 +7,7 @@ import { nip19 } from "nostr-tools";
7
7
  import { BCH_PROOF_FORMAT, normalizeBchMessageSignature, } from "./signing.js";
8
8
  export const NOSTR_CHALLENGE_KIND = 27235;
9
9
  /** Encode n as a Bitcoin CompactSize integer. */
10
- function varint(n) {
10
+ export function encodeBitcoinCompactSize(n) {
11
11
  if (n < 0xfd)
12
12
  return new Uint8Array([n]);
13
13
  const value = BigInt(n);
@@ -31,12 +31,28 @@ function varint(n) {
31
31
  * Format: SHA256d("\x18Bitcoin Signed Message:\n" + varint(len) + message)
32
32
  * Uses proper varint encoding — required for messages longer than 252 bytes.
33
33
  */
34
- function bitcoinMsgHash(message) {
34
+ export function bitcoinSignedMessageHash(message) {
35
35
  const enc = new TextEncoder();
36
36
  const msgBytes = enc.encode(message);
37
37
  const prefix = enc.encode("\x18Bitcoin Signed Message:\n");
38
- return hash256(new Uint8Array([...prefix, ...varint(msgBytes.length), ...msgBytes]));
38
+ return hash256(new Uint8Array([...prefix, ...encodeBitcoinCompactSize(msgBytes.length), ...msgBytes]));
39
39
  }
40
+ /**
41
+ * Encode a 65-byte compact signature (recovery ID + 64-byte r||s) as Base64.
42
+ * Header byte = 31 + recoveryId (compressed key, recovery ID 0–3).
43
+ */
44
+ export function encodeCompactSignatureBase64(recoveryId, signature) {
45
+ if (!Number.isInteger(recoveryId) || recoveryId < 0 || recoveryId > 3)
46
+ throw new RangeError("Recovery ID must be 0–3");
47
+ if (signature.length !== 64)
48
+ throw new TypeError("Signature must be 64 bytes");
49
+ const compact = new Uint8Array(65);
50
+ compact[0] = 31 + recoveryId;
51
+ compact.set(signature, 1);
52
+ return btoa(String.fromCharCode(...compact));
53
+ }
54
+ // Internal alias used within this file
55
+ function bitcoinMsgHash(message) { return bitcoinSignedMessageHash(message); }
40
56
  function recoverBchAddress(signatureBase64, msgHash, expectedAddress) {
41
57
  try {
42
58
  normalizeBchMessageSignature(signatureBase64);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "did-beta-bch",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,8 +20,8 @@
20
20
  "scripts": {
21
21
  "build": "tsc && npm run build:demo",
22
22
  "build:demo": "esbuild demo/client.js --bundle --format=esm --platform=browser --target=es2022 --outfile=demo/bundle.js",
23
- "typecheck": "tsc --noEmit",
24
23
  "test": "node --test test/cashlet-did.test.mjs",
24
+ "typecheck": "tsc --noEmit",
25
25
  "prepublishOnly": "npm run build"
26
26
  },
27
27
  "keywords": [