did-beta-bch 0.0.0-stage → 1.0.1

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,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to `cashlet-did` will be documented here.
4
+
5
+ ## [Unreleased]
6
+
7
+ ### Added
8
+ - `buildAnchorTx(bindingId, utxo)` — build an OP_RETURN BCH transaction anchoring a binding to the chain
9
+ - `issueCredential(subjectNpub, type, issuerPrivkey)` — issue a third-party Nostr attestation (kind 30079)
10
+ - `resolveCredentials(npub)` — fetch all credentials issued to an npub from relays
11
+ - `buildDisclosure(attrs)` — commit to a set of attributes with individual SHA-256 hashes
12
+ - `verifyDisclosure(revealed, commitments)` — verify selectively revealed attributes against commitments
13
+ - `resolveAddress(npub)` — one-call pay-by-handle helper; returns BCH address string or null
14
+ - `resolveIdentityChain(npub)` — returns full binding history for an npub, newest first
15
+ - Key rotation support: `previousBindingId` field in `CanonicalBinding`; `["supersedes", id]` tag in published Nostr events
16
+ - `anchor` field in `CanonicalBinding`; `["bch-anchor", txid]` tag in Nostr events
17
+
18
+ ## [0.1.0] — initial extraction
19
+
20
+ ### Added
21
+ - `buildBinding()` — construct a canonical BCH–Nostr identity binding
22
+ - `bchSigningPayload()` — deterministic payload for BCH key to sign
23
+ - `nostrEventTemplate()` — unsigned Nostr event template (kind 30078)
24
+ - `resolveIdentity(npub)` — resolve BCH address from npub via Nostr relays
25
+ - `verifyIdentity(resolved)` — verify BCH + Nostr signatures on a resolved identity
26
+ - `verifyChallengeProof(proof, nonce)` — verify challenge-response authentication proof
27
+ - `publishToRelays()` / `queryRelays()` — minimal NIP-01 relay client
28
+ - `DEFAULT_RELAYS` — nos.lol, relay.damus.io, relay.nostr.band
29
+ - `canonicalize()` / `sha256hex()` — deterministic JSON serialisation utilities
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # cashlet-did
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
+ A standalone, framework-agnostic library implementing the **Cashlet DID protocol** — a decentralised identity layer that binds Bitcoin Cash (BCH) addresses to Nostr public keys.
4
+
5
+ Any BCH wallet or Nostr client can use this library to publish, resolve, and verify BCH–Nostr identity bindings without depending on the Cashlet extension.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install cashlet-did
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Pay by handle — one call
16
+
17
+ ```ts
18
+ import { resolveAddress } from 'cashlet-did'
19
+
20
+ const address = await resolveAddress('npub1...')
21
+ // Returns a token-aware bitcoincash:z... address
22
+ // Works for BCH, CashTokens (FT), and NFTs — no conversion needed
23
+ if (address) wallet.sendTo(address, amount)
24
+ ```
25
+
26
+ ### Resolve + verify
27
+
28
+ const identity = await resolveIdentity('npub1...')
29
+ if (identity) {
30
+ console.log(identity.bchAddress) // bitcoincash:z...
31
+ const result = await verifyIdentity(identity)
32
+ console.log(result.status) // "verified"
33
+ }
34
+ ```
35
+
36
+ ### Build and publish a binding (wallet integration)
37
+
38
+ ```ts
39
+ import { buildBinding, bchSigningPayload, nostrEventTemplate, publishToRelays, DEFAULT_RELAYS } from 'cashlet-did'
40
+ import { finalizeEvent } from 'nostr-tools'
41
+
42
+ const binding = await buildBinding(bchAddress, 'mainnet', nostrPubkeyHex)
43
+ const message = bchSigningPayload(binding)
44
+
45
+ // Sign message with BCH private key (recoverable compact sig)
46
+ const bchSig = signWithBchKey(message) // your wallet's signing function
47
+
48
+ const template = nostrEventTemplate(binding, { message, signature: bchSig })
49
+ const event = finalizeEvent(template, nostrPrivkey)
50
+ await publishToRelays(DEFAULT_RELAYS, event)
51
+ ```
52
+
53
+ ### Verify a challenge-response proof
54
+
55
+ ```ts
56
+ import { verifyChallengeProof } from 'cashlet-did'
57
+
58
+ const valid = verifyChallengeProof(
59
+ { signature, payload, address },
60
+ nonce
61
+ )
62
+ ```
63
+
64
+ ## Protocol
65
+
66
+ Bindings are published as Nostr events of kind `30078` with the following tags:
67
+
68
+ | Tag | Value |
69
+ |---|---|
70
+ | `d` | binding ID (SHA-256 of canonical binding JSON) |
71
+ | `bch` | BCH address, network |
72
+ | `bch-proof` | signing payload, recoverable compact signature |
73
+ | `protocol` | `cashlet-did`, version |
74
+ | `t` | `cashlet-did` |
75
+
76
+ The BCH signature uses Bitcoin signed message format (`\x18Bitcoin Signed Message:\n`) with a 65-byte recoverable compact signature (recovery ID byte + 64-byte compact sig).
77
+
78
+ ## Relays
79
+
80
+ Default relays: `wss://nos.lol`, `wss://relay.damus.io`, `wss://relay.nostr.band`
81
+
82
+ Pass a custom relay list to any function that accepts `relays?: string[]`.
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,18 @@
1
+ /**
2
+ * BCH on-chain identity commitment via OP_RETURN.
3
+ * Anchors a bindingId to a BCH block, providing a tamper-evident timestamp.
4
+ */
5
+ export declare const ANCHOR_PREFIX = "cashlet-did:";
6
+ export interface AnchorUtxo {
7
+ txid: string;
8
+ vout: number;
9
+ valueSats: number;
10
+ lockingBytecode: Uint8Array;
11
+ privateKey: Uint8Array;
12
+ }
13
+ export interface AnchorResult {
14
+ txHex: string;
15
+ opReturnData: string;
16
+ }
17
+ /** Build an OP_RETURN tx anchoring a bindingId to the BCH chain */
18
+ export declare function buildAnchorTx(bindingId: string, utxo: AnchorUtxo): Promise<AnchorResult>;
package/dist/anchor.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * BCH on-chain identity commitment via OP_RETURN.
3
+ * Anchors a bindingId to a BCH block, providing a tamper-evident timestamp.
4
+ */
5
+ export const ANCHOR_PREFIX = "cashlet-did:";
6
+ const OP_RETURN = 0x6a;
7
+ const DUST_SATS = 1000; // anchor output value (burned)
8
+ const FEE_SATS = 500;
9
+ /** Build an OP_RETURN tx anchoring a bindingId to the BCH chain */
10
+ export async function buildAnchorTx(bindingId, utxo) {
11
+ const { encodeTransaction, generateTransaction, hexToBin, binToHex, walletTemplateP2pkhNonHd, importWalletTemplate, walletTemplateToCompilerBch } = await import("@bitauth/libauth");
12
+ const data = ANCHOR_PREFIX + bindingId;
13
+ const dataBytes = new TextEncoder().encode(data);
14
+ // OP_RETURN <push> <data>
15
+ const opReturnScript = new Uint8Array([OP_RETURN, dataBytes.length, ...dataBytes]);
16
+ const template = importWalletTemplate(walletTemplateP2pkhNonHd);
17
+ if (typeof template === "string")
18
+ throw new Error(template);
19
+ const compiler = walletTemplateToCompilerBch(template);
20
+ const result = generateTransaction({
21
+ inputs: [{
22
+ outpointTransactionHash: hexToBin(utxo.txid).reverse(),
23
+ outpointIndex: utxo.vout,
24
+ sequenceNumber: 0xffffffff,
25
+ unlockingBytecode: {
26
+ compiler,
27
+ data: { keys: { privateKeys: { key: utxo.privateKey } } },
28
+ valueSatoshis: BigInt(utxo.valueSats),
29
+ script: "unlock",
30
+ token: undefined,
31
+ },
32
+ }],
33
+ locktime: 0,
34
+ outputs: [
35
+ { lockingBytecode: opReturnScript, valueSatoshis: 0n },
36
+ // change back to sender minus fee
37
+ {
38
+ lockingBytecode: utxo.lockingBytecode,
39
+ valueSatoshis: BigInt(utxo.valueSats - FEE_SATS),
40
+ },
41
+ ],
42
+ version: 2,
43
+ });
44
+ if (!result.success)
45
+ throw new Error("Failed to build anchor tx: " + JSON.stringify(result.errors));
46
+ return { txHex: binToHex(encodeTransaction(result.transaction)), opReturnData: data };
47
+ }
@@ -0,0 +1,59 @@
1
+ export declare const PROTOCOL_ID = "cashlet-did";
2
+ export declare const PROTOCOL_VERSION = "1.0.0";
3
+ export declare const BINDING_KIND = 30078;
4
+ export type Network = "mainnet" | "testnet" | "chipnet";
5
+ export interface CanonicalBinding {
6
+ protocol: typeof PROTOCOL_ID;
7
+ version: typeof PROTOCOL_VERSION;
8
+ bindingId: string;
9
+ bch: {
10
+ address: string;
11
+ network: Network;
12
+ paymentCode?: string;
13
+ };
14
+ nostr: {
15
+ pubkey: string;
16
+ };
17
+ createdAt: number;
18
+ nonce: string;
19
+ purpose: "identity-binding-v1";
20
+ previousBindingId?: string;
21
+ anchor?: {
22
+ txid: string;
23
+ blockHeight?: number;
24
+ };
25
+ }
26
+ export interface IdentityBinding {
27
+ binding: CanonicalBinding;
28
+ bchProof: {
29
+ message: string;
30
+ signature: string;
31
+ pubkeyHex?: string;
32
+ };
33
+ nostrProof: {
34
+ event: NostrEvent;
35
+ };
36
+ publishedAt?: number;
37
+ }
38
+ export interface NostrEvent {
39
+ id: string;
40
+ pubkey: string;
41
+ created_at: number;
42
+ kind: number;
43
+ tags: string[][];
44
+ content: string;
45
+ sig: string;
46
+ }
47
+ export declare function canonicalize(v: unknown): string;
48
+ export declare function sha256hex(s: string): Promise<string>;
49
+ export declare function buildBinding(bchAddress: string, network: Network, nostrPubkey: string, paymentCode?: string, previousBindingId?: string): Promise<CanonicalBinding>;
50
+ export declare function bchSigningPayload(binding: CanonicalBinding): string;
51
+ export declare function nostrEventTemplate(binding: CanonicalBinding, bchProof?: {
52
+ message: string;
53
+ signature: string;
54
+ }): {
55
+ kind: number;
56
+ created_at: number;
57
+ tags: string[][];
58
+ content: string;
59
+ };
@@ -0,0 +1,71 @@
1
+ export const PROTOCOL_ID = "cashlet-did";
2
+ export const PROTOCOL_VERSION = "1.0.0";
3
+ export const BINDING_KIND = 30078;
4
+ export function canonicalize(v) {
5
+ if (v === null || typeof v !== "object")
6
+ return JSON.stringify(v);
7
+ if (Array.isArray(v))
8
+ return "[" + v.map(canonicalize).join(",") + "]";
9
+ const obj = v;
10
+ const keys = Object.keys(obj).sort();
11
+ return "{" + keys.filter(k => obj[k] !== undefined)
12
+ .map(k => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
13
+ }
14
+ export async function sha256hex(s) {
15
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s));
16
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
17
+ }
18
+ function randomHex(bytes) {
19
+ const arr = new Uint8Array(bytes);
20
+ crypto.getRandomValues(arr);
21
+ return Array.from(arr).map(b => b.toString(16).padStart(2, "0")).join("");
22
+ }
23
+ export async function buildBinding(bchAddress, network, nostrPubkey, paymentCode, previousBindingId) {
24
+ const partial = {
25
+ protocol: PROTOCOL_ID,
26
+ version: PROTOCOL_VERSION,
27
+ bch: { address: bchAddress, network, ...(paymentCode ? { paymentCode } : {}) },
28
+ nostr: { pubkey: nostrPubkey },
29
+ createdAt: Math.floor(Date.now() / 1000),
30
+ nonce: randomHex(32),
31
+ purpose: "identity-binding-v1",
32
+ ...(previousBindingId ? { previousBindingId } : {}),
33
+ };
34
+ const bindingId = await sha256hex(canonicalize(partial));
35
+ return { ...partial, bindingId };
36
+ }
37
+ export function bchSigningPayload(binding) {
38
+ return `${PROTOCOL_ID}/bch-proof/v${PROTOCOL_VERSION}\n` + canonicalize({
39
+ bindingId: binding.bindingId,
40
+ bchAddress: binding.bch.address,
41
+ bchNetwork: binding.bch.network,
42
+ nostrPubkey: binding.nostr.pubkey,
43
+ nonce: binding.nonce,
44
+ purpose: binding.purpose,
45
+ createdAt: binding.createdAt,
46
+ });
47
+ }
48
+ export function nostrEventTemplate(binding, bchProof) {
49
+ return {
50
+ kind: BINDING_KIND,
51
+ created_at: Math.floor(Date.now() / 1000),
52
+ tags: [
53
+ ["d", binding.bindingId],
54
+ ["bch", binding.bch.address, binding.bch.network],
55
+ ["protocol", PROTOCOL_ID, PROTOCOL_VERSION],
56
+ ["t", PROTOCOL_ID],
57
+ ...(binding.previousBindingId ? [["supersedes", binding.previousBindingId]] : []),
58
+ ...(binding.anchor ? [["bch-anchor", binding.anchor.txid, ...(binding.anchor.blockHeight ? [String(binding.anchor.blockHeight)] : [])]] : []),
59
+ ...(bchProof ? [["bch-proof", bchProof.message, bchProof.signature]] : []),
60
+ ],
61
+ content: canonicalize({
62
+ protocol: PROTOCOL_ID,
63
+ version: PROTOCOL_VERSION,
64
+ bindingId: binding.bindingId,
65
+ bch: binding.bch,
66
+ nonce: binding.nonce,
67
+ createdAt: binding.createdAt,
68
+ ...(binding.previousBindingId ? { previousBindingId: binding.previousBindingId } : {}),
69
+ }),
70
+ };
71
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Verifiable Credentials — third-party attestations anchored to a Nostr identity.
3
+ * Issuer signs a Nostr event (kind 30079) attesting a claim about a subject npub.
4
+ * Subject's binding event links to received credentials via ["credential", eventId, issuerPubkey, type] tag.
5
+ */
6
+ import type { RelayResult } from "./relay.js";
7
+ export declare const CREDENTIAL_KIND = 30079;
8
+ export interface CredentialPayload {
9
+ subject: string;
10
+ type: string;
11
+ issuedAt: number;
12
+ expiry?: number;
13
+ claims?: Record<string, string>;
14
+ }
15
+ export interface IssuedCredential {
16
+ id: string;
17
+ issuer: string;
18
+ payload: CredentialPayload;
19
+ rawEvent: object;
20
+ }
21
+ /** Issue a credential — issuer signs a Nostr event attesting a claim about subjectNpub */
22
+ export declare function issueCredential(subjectNpub: string, type: string, issuerPrivkey: Uint8Array, claims?: Record<string, string>, relays?: string[]): Promise<{
23
+ credential: IssuedCredential;
24
+ results: RelayResult[];
25
+ }>;
26
+ /** Resolve all credentials issued to an npub */
27
+ export declare function resolveCredentials(npub: string, relays?: string[]): Promise<IssuedCredential[]>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Verifiable Credentials — third-party attestations anchored to a Nostr identity.
3
+ * Issuer signs a Nostr event (kind 30079) attesting a claim about a subject npub.
4
+ * Subject's binding event links to received credentials via ["credential", eventId, issuerPubkey, type] tag.
5
+ */
6
+ import { finalizeEvent, nip19 } from "nostr-tools";
7
+ import { queryRelays, publishToRelays, DEFAULT_RELAYS } from "./relay.js";
8
+ export const CREDENTIAL_KIND = 30079;
9
+ /** Issue a credential — issuer signs a Nostr event attesting a claim about subjectNpub */
10
+ export async function issueCredential(subjectNpub, type, issuerPrivkey, claims, relays = DEFAULT_RELAYS) {
11
+ const decoded = nip19.decode(subjectNpub);
12
+ if (decoded.type !== "npub")
13
+ throw new Error("Invalid npub");
14
+ const subjectPubkey = decoded.data;
15
+ const payload = {
16
+ subject: subjectPubkey,
17
+ type,
18
+ issuedAt: Math.floor(Date.now() / 1000),
19
+ ...(claims ? { claims } : {}),
20
+ };
21
+ const template = {
22
+ kind: CREDENTIAL_KIND,
23
+ created_at: payload.issuedAt,
24
+ tags: [
25
+ ["p", subjectPubkey],
26
+ ["credential-type", type],
27
+ ["t", "cashlet-did-credential"],
28
+ ],
29
+ content: JSON.stringify(payload),
30
+ };
31
+ const signed = finalizeEvent(template, issuerPrivkey);
32
+ const results = await publishToRelays(relays, signed);
33
+ const { getPublicKey } = await import("nostr-tools");
34
+ const issuerPubkey = getPublicKey(issuerPrivkey);
35
+ return {
36
+ credential: { id: signed.id, issuer: issuerPubkey, payload, rawEvent: signed },
37
+ results,
38
+ };
39
+ }
40
+ /** Resolve all credentials issued to an npub */
41
+ export async function resolveCredentials(npub, relays = DEFAULT_RELAYS) {
42
+ const decoded = nip19.decode(npub);
43
+ if (decoded.type !== "npub")
44
+ return [];
45
+ const pubkey = decoded.data;
46
+ const events = await queryRelays(relays, {
47
+ kinds: [CREDENTIAL_KIND],
48
+ "#p": [pubkey],
49
+ "#t": ["cashlet-did-credential"],
50
+ limit: 50,
51
+ });
52
+ return events.map(e => {
53
+ try {
54
+ const payload = JSON.parse(e.content);
55
+ return { id: e.id, issuer: e.pubkey, payload, rawEvent: e };
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }).filter(Boolean);
61
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Selective disclosure of identity attributes.
3
+ * Each attribute is individually hashed — the holder commits to all attributes
4
+ * in the binding event, then reveals only chosen ones to a verifier.
5
+ *
6
+ * Usage:
7
+ * const d = await buildDisclosure({ merchant: "true", country: "AU" })
8
+ * // store d.commitments in binding event tags
9
+ * const revealed = d.reveal(["merchant"])
10
+ * // verifier calls verifyDisclosure(revealed, commitments)
11
+ */
12
+ export interface Disclosure {
13
+ /** SHA-256 hashes of each attribute — publish these in the binding event */
14
+ commitments: Record<string, string>;
15
+ /** Reveal selected attribute values — share with verifier */
16
+ reveal: (keys: string[]) => Record<string, string>;
17
+ }
18
+ /** Build a disclosure object from a set of attributes */
19
+ export declare function buildDisclosure(attrs: Record<string, string>): Promise<Disclosure>;
20
+ /**
21
+ * Verify that revealed attributes match their published commitments.
22
+ * Returns the set of verified attribute keys.
23
+ */
24
+ export declare function verifyDisclosure(revealed: Record<string, string>, commitments: Record<string, string>): Promise<{
25
+ verified: string[];
26
+ failed: string[];
27
+ }>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Selective disclosure of identity attributes.
3
+ * Each attribute is individually hashed — the holder commits to all attributes
4
+ * in the binding event, then reveals only chosen ones to a verifier.
5
+ *
6
+ * Usage:
7
+ * const d = await buildDisclosure({ merchant: "true", country: "AU" })
8
+ * // store d.commitments in binding event tags
9
+ * const revealed = d.reveal(["merchant"])
10
+ * // verifier calls verifyDisclosure(revealed, commitments)
11
+ */
12
+ async function hashAttr(key, value) {
13
+ const data = new TextEncoder().encode(`cashlet-did/attr/${key}:${value}`);
14
+ const buf = await crypto.subtle.digest("SHA-256", data);
15
+ return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
16
+ }
17
+ /** Build a disclosure object from a set of attributes */
18
+ export async function buildDisclosure(attrs) {
19
+ const commitments = {};
20
+ for (const [k, v] of Object.entries(attrs)) {
21
+ commitments[k] = await hashAttr(k, v);
22
+ }
23
+ return {
24
+ commitments,
25
+ reveal: (keys) => Object.fromEntries(keys.filter(k => k in attrs).map(k => [k, attrs[k]])),
26
+ };
27
+ }
28
+ /**
29
+ * Verify that revealed attributes match their published commitments.
30
+ * Returns the set of verified attribute keys.
31
+ */
32
+ export async function verifyDisclosure(revealed, commitments) {
33
+ const verified = [];
34
+ const failed = [];
35
+ for (const [k, v] of Object.entries(revealed)) {
36
+ const expected = commitments[k];
37
+ if (!expected) {
38
+ failed.push(k);
39
+ continue;
40
+ }
41
+ const actual = await hashAttr(k, v);
42
+ (actual === expected ? verified : failed).push(k);
43
+ }
44
+ return { verified, failed };
45
+ }
@@ -0,0 +1,6 @@
1
+ export { buildDisclosure, verifyDisclosure, type Disclosure, } from "./disclosure.js";
2
+ export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
3
+ export { issueCredential, resolveCredentials, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
4
+ export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, type Network, type CanonicalBinding, type IdentityBinding, type NostrEvent, buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
5
+ export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
6
+ export { resolveIdentity, resolveAddress, verifyIdentity, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, } from "./verify.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ export { buildDisclosure, verifyDisclosure, } from "./disclosure.js";
2
+ export { buildAnchorTx, ANCHOR_PREFIX, } from "./anchor.js";
3
+ export { issueCredential, resolveCredentials, CREDENTIAL_KIND, } from "./credentials.js";
4
+ export {
5
+ // Protocol constants
6
+ PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND,
7
+ // Binding construction
8
+ buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
9
+ export {
10
+ // Relay communication
11
+ publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, } from "./relay.js";
12
+ export {
13
+ // Verification
14
+ resolveIdentity, resolveAddress, verifyIdentity, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
@@ -0,0 +1,11 @@
1
+ import type { NostrEvent } from "./binding.js";
2
+ export interface RelayResult {
3
+ relay: string;
4
+ ok: boolean;
5
+ error?: string;
6
+ }
7
+ export declare function publishToRelay(url: string, event: NostrEvent): Promise<RelayResult>;
8
+ export declare function publishToRelays(relays: string[], event: NostrEvent): Promise<RelayResult[]>;
9
+ export declare function queryRelay(url: string, filter: Record<string, unknown>): Promise<NostrEvent[]>;
10
+ export declare function queryRelays(relays: string[], filter: Record<string, unknown>): Promise<NostrEvent[]>;
11
+ export declare const DEFAULT_RELAYS: string[];
package/dist/relay.js ADDED
@@ -0,0 +1,76 @@
1
+ const TIMEOUT_MS = 8000;
2
+ function openRelay(url) {
3
+ return new Promise((resolve, reject) => {
4
+ const ws = new WebSocket(url);
5
+ const t = setTimeout(() => { ws.close(); reject(new Error("connect timeout")); }, TIMEOUT_MS);
6
+ ws.onopen = () => { clearTimeout(t); resolve(ws); };
7
+ ws.onerror = () => { clearTimeout(t); reject(new Error("connect error")); };
8
+ });
9
+ }
10
+ export async function publishToRelay(url, event) {
11
+ try {
12
+ const ws = await openRelay(url);
13
+ return await new Promise((resolve) => {
14
+ const t = setTimeout(() => { ws.close(); resolve({ relay: url, ok: false, error: "publish timeout" }); }, TIMEOUT_MS);
15
+ ws.onmessage = (e) => {
16
+ clearTimeout(t);
17
+ ws.close();
18
+ try {
19
+ const msg = JSON.parse(e.data);
20
+ if (msg[0] === "OK" && msg[1] === event.id)
21
+ resolve({ relay: url, ok: msg[2] === true, error: msg[3] || undefined });
22
+ else
23
+ resolve({ relay: url, ok: false, error: "unexpected response" });
24
+ }
25
+ catch {
26
+ resolve({ relay: url, ok: false, error: "parse error" });
27
+ }
28
+ };
29
+ ws.send(JSON.stringify(["EVENT", event]));
30
+ });
31
+ }
32
+ catch (e) {
33
+ return { relay: url, ok: false, error: String(e) };
34
+ }
35
+ }
36
+ export async function publishToRelays(relays, event) {
37
+ return Promise.all(relays.map(r => publishToRelay(r, event)));
38
+ }
39
+ export async function queryRelay(url, filter) {
40
+ try {
41
+ const ws = await openRelay(url);
42
+ return await new Promise((resolve) => {
43
+ const events = [];
44
+ const subId = Math.random().toString(36).slice(2);
45
+ const t = setTimeout(() => { ws.close(); resolve(events); }, TIMEOUT_MS);
46
+ ws.onmessage = (e) => {
47
+ try {
48
+ const msg = JSON.parse(e.data);
49
+ if (msg[0] === "EVENT" && msg[1] === subId)
50
+ events.push(msg[2]);
51
+ if (msg[0] === "EOSE" && msg[1] === subId) {
52
+ clearTimeout(t);
53
+ ws.close();
54
+ resolve(events);
55
+ }
56
+ }
57
+ catch { /* ignore */ }
58
+ };
59
+ ws.send(JSON.stringify(["REQ", subId, filter]));
60
+ });
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ export async function queryRelays(relays, filter) {
67
+ const results = await Promise.all(relays.map(r => queryRelay(r, filter)));
68
+ const seen = new Set();
69
+ return results.flat().filter(e => { if (seen.has(e.id))
70
+ return false; seen.add(e.id); return true; });
71
+ }
72
+ export const DEFAULT_RELAYS = [
73
+ "wss://nos.lol",
74
+ "wss://relay.damus.io",
75
+ "wss://relay.nostr.band",
76
+ ];
@@ -0,0 +1,40 @@
1
+ import { canonicalize, sha256hex, bchSigningPayload, type CanonicalBinding, type IdentityBinding, type NostrEvent } from "./binding.js";
2
+ export type VerifyStatus = "verified" | "invalid_bch_sig" | "invalid_nostr_sig" | "id_mismatch" | "malformed";
3
+ export interface VerifyResult {
4
+ status: VerifyStatus;
5
+ bchValid: boolean;
6
+ nostrValid: boolean;
7
+ details: string;
8
+ }
9
+ export interface ResolvedIdentity {
10
+ npub: string;
11
+ pubkey: string;
12
+ bchAddress: string;
13
+ network: string;
14
+ bindingId: string;
15
+ createdAt: number;
16
+ bchProofTag: string[];
17
+ rawEvent: NostrEvent;
18
+ }
19
+ /** Resolve a BCH address from an npub via Nostr relays */
20
+ export declare function resolveIdentity(npub: string, relays?: string[]): Promise<ResolvedIdentity | null>;
21
+ /** Verify the BCH + Nostr signatures on a resolved identity */
22
+ export declare function verifyIdentity(resolved: ResolvedIdentity): Promise<VerifyResult>;
23
+ /** Verify a challenge-response proof */
24
+ export declare function verifyChallengeProof(proof: {
25
+ signature: string;
26
+ payload: string;
27
+ address: string;
28
+ }, nonce: string): boolean;
29
+ /**
30
+ * Resolve a BCH address from an npub — the primary pay-by-handle entry point.
31
+ * Returns the verified BCH address string, or null if not found.
32
+ *
33
+ * @example
34
+ * const address = await resolveAddress('npub1...')
35
+ * if (address) wallet.sendTo(address, amount)
36
+ */
37
+ export declare function resolveAddress(npub: string, relays?: string[]): Promise<string | null>;
38
+ /** Returns full binding history for an npub, newest first */
39
+ export declare function resolveIdentityChain(npub: string, relays?: string[]): Promise<ResolvedIdentity[]>;
40
+ export { bchSigningPayload, canonicalize, sha256hex, type CanonicalBinding, type IdentityBinding };
package/dist/verify.js ADDED
@@ -0,0 +1,164 @@
1
+ import { verifyEvent } from "nostr-tools";
2
+ import { secp256k1, hash256, encodeCashAddress, hash160 } from "@bitauth/libauth";
3
+ import { canonicalize, sha256hex, bchSigningPayload, BINDING_KIND, PROTOCOL_ID, } from "./binding.js";
4
+ import { queryRelays, DEFAULT_RELAYS } from "./relay.js";
5
+ import { nip19 } from "nostr-tools";
6
+ function bitcoinMsgHash(message) {
7
+ const enc = new TextEncoder();
8
+ const msgBytes = enc.encode(message);
9
+ const prefix = enc.encode("\x18Bitcoin Signed Message:\n");
10
+ const lenByte = new Uint8Array([msgBytes.length]);
11
+ return hash256(new Uint8Array([...prefix, ...lenByte, ...msgBytes]));
12
+ }
13
+ function recoverBchAddress(sigHex, msgHash, expectedAddress) {
14
+ const sig = Uint8Array.from(sigHex.match(/.{2}/g).map(b => parseInt(b, 16)));
15
+ const tryRecover = (compact, id) => {
16
+ try {
17
+ const pub = secp256k1.recoverPublicKeyCompressed(compact, id, msgHash);
18
+ if (!(pub instanceof Uint8Array))
19
+ return null;
20
+ const addrPrefix = (expectedAddress?.split(":")[0] ?? "bitcoincash");
21
+ for (const type of ["p2pkhWithTokens", "p2pkh"]) {
22
+ const r = encodeCashAddress({ prefix: addrPrefix, type, payload: hash160(pub) });
23
+ if (r?.address)
24
+ return r.address;
25
+ if (typeof r === "string")
26
+ return r;
27
+ }
28
+ }
29
+ catch { /* try next */ }
30
+ return null;
31
+ };
32
+ if (sig.length === 65) {
33
+ const recoveryId = (sig[0] & 0x03);
34
+ const addr = tryRecover(sig.slice(1), recoveryId);
35
+ if (addr)
36
+ return addr;
37
+ }
38
+ for (const id of [0, 1, 2, 3]) {
39
+ const addr = tryRecover(sig, id);
40
+ if (addr)
41
+ return addr;
42
+ }
43
+ return null;
44
+ }
45
+ /** Resolve a BCH address from an npub via Nostr relays */
46
+ export async function resolveIdentity(npub, relays = DEFAULT_RELAYS) {
47
+ const decoded = nip19.decode(npub);
48
+ if (decoded.type !== "npub")
49
+ return null;
50
+ const pubkey = decoded.data;
51
+ const events = await queryRelays(relays, {
52
+ kinds: [BINDING_KIND],
53
+ authors: [pubkey],
54
+ "#t": [PROTOCOL_ID],
55
+ limit: 10,
56
+ });
57
+ if (!events.length)
58
+ return null;
59
+ // Pick latest
60
+ const event = events.sort((a, b) => b.created_at - a.created_at)[0];
61
+ const bchTag = event.tags.find(t => t[0] === "bch");
62
+ const dTag = event.tags.find(t => t[0] === "d");
63
+ const bchProofTag = event.tags.find(t => t[0] === "bch-proof");
64
+ if (!bchTag || !dTag || !bchProofTag)
65
+ return null;
66
+ let createdAt = event.created_at;
67
+ try {
68
+ createdAt = JSON.parse(event.content).createdAt ?? createdAt;
69
+ }
70
+ catch { /* use event ts */ }
71
+ return {
72
+ npub,
73
+ pubkey,
74
+ bchAddress: bchTag[1],
75
+ network: bchTag[2] ?? "mainnet",
76
+ bindingId: dTag[1],
77
+ createdAt,
78
+ bchProofTag,
79
+ rawEvent: event,
80
+ };
81
+ }
82
+ /** Verify the BCH + Nostr signatures on a resolved identity */
83
+ export async function verifyIdentity(resolved) {
84
+ const { bchProofTag, rawEvent, bchAddress, bindingId, pubkey } = resolved;
85
+ const [, message, sigHex] = bchProofTag;
86
+ // 1. Nostr event signature
87
+ let nostrValid = false;
88
+ try {
89
+ nostrValid = verifyEvent(rawEvent);
90
+ const dTag = rawEvent.tags.find(t => t[0] === "d");
91
+ const bchTag = rawEvent.tags.find(t => t[0] === "bch");
92
+ if (dTag?.[1] !== bindingId || bchTag?.[1] !== bchAddress)
93
+ nostrValid = false;
94
+ if (rawEvent.pubkey !== pubkey)
95
+ nostrValid = false;
96
+ }
97
+ catch {
98
+ nostrValid = false;
99
+ }
100
+ // 2. BCH signature recovery
101
+ const msgHash = bitcoinMsgHash(message);
102
+ const recovered = recoverBchAddress(sigHex, msgHash, bchAddress);
103
+ const bchValid = recovered === bchAddress;
104
+ // 3. Payload integrity
105
+ if (!message.startsWith(`${PROTOCOL_ID}/bch-proof/`)) {
106
+ return { status: "malformed", bchValid: false, nostrValid, details: "Unexpected proof payload format" };
107
+ }
108
+ if (bchValid && nostrValid)
109
+ return { status: "verified", bchValid, nostrValid, details: "Both proofs valid" };
110
+ if (!bchValid)
111
+ return { status: "invalid_bch_sig", bchValid, nostrValid, details: "BCH signature invalid" };
112
+ return { status: "invalid_nostr_sig", bchValid, nostrValid, details: "Nostr signature invalid" };
113
+ }
114
+ /** Verify a challenge-response proof */
115
+ export function verifyChallengeProof(proof, nonce) {
116
+ if (!proof.payload.includes(nonce))
117
+ return false;
118
+ if (!proof.payload.startsWith(`${PROTOCOL_ID}/challenge`))
119
+ return false;
120
+ const msgHash = bitcoinMsgHash(proof.payload);
121
+ const recovered = recoverBchAddress(proof.signature, msgHash, proof.address);
122
+ return recovered === proof.address;
123
+ }
124
+ /**
125
+ * Resolve a BCH address from an npub — the primary pay-by-handle entry point.
126
+ * Returns the verified BCH address string, or null if not found.
127
+ *
128
+ * @example
129
+ * const address = await resolveAddress('npub1...')
130
+ * if (address) wallet.sendTo(address, amount)
131
+ */
132
+ export async function resolveAddress(npub, relays = DEFAULT_RELAYS) {
133
+ const identity = await resolveIdentity(npub, relays);
134
+ return identity?.bchAddress ?? null;
135
+ }
136
+ /** Returns full binding history for an npub, newest first */
137
+ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
138
+ const decoded = nip19.decode(npub);
139
+ if (decoded.type !== "npub")
140
+ return [];
141
+ const pubkey = decoded.data;
142
+ const events = await queryRelays(relays, {
143
+ kinds: [BINDING_KIND],
144
+ authors: [pubkey],
145
+ "#t": [PROTOCOL_ID],
146
+ limit: 50,
147
+ });
148
+ const results = [];
149
+ for (const event of events.sort((a, b) => b.created_at - a.created_at)) {
150
+ const bchTag = event.tags.find(t => t[0] === "bch");
151
+ const dTag = event.tags.find(t => t[0] === "d");
152
+ const bchProofTag = event.tags.find(t => t[0] === "bch-proof");
153
+ if (!bchTag || !dTag || !bchProofTag)
154
+ continue;
155
+ let createdAt = event.created_at;
156
+ try {
157
+ createdAt = JSON.parse(event.content).createdAt ?? createdAt;
158
+ }
159
+ catch { /* use event ts */ }
160
+ results.push({ npub, pubkey, bchAddress: bchTag[1], network: bchTag[2] ?? "mainnet", bindingId: dTag[1], createdAt, bchProofTag, rawEvent: event });
161
+ }
162
+ return results;
163
+ }
164
+ export { bchSigningPayload, canonicalize, sha256hex };
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "did-beta-bch",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": ["dist", "README.md", "CHANGELOG.md"],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": ["bch", "bitcoin-cash", "nostr", "did", "decentralised-identity", "cashaddr", "nip-06", "nip-07"],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "engines": { "node": ">=18" },
24
+ "dependencies": {
25
+ "@bitauth/libauth": "^3.1.0-next.8",
26
+ "nostr-tools": "^2.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.4.0"
30
+ }
31
+ }