@orangecheck/sdk 1.11.0 → 1.12.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/README.md CHANGED
@@ -145,6 +145,35 @@ Now the gate doesn't have to trust the client — the address it sees has been c
145
145
 
146
146
  ---
147
147
 
148
+ ## Bonds: one coin backing one account
149
+
150
+ An attestation publishes an address. A bond keeps it private. The holder signs
151
+ once to pledge one unspent coin to one subject, and whoever relies on the bond
152
+ sees only floors (at least N sats, at least D days) and a nullifier that is the
153
+ same for that coin at that site and unrelated anywhere else. Spec:
154
+ [`BOND.md`](https://github.com/orangecheck/oc-attest-protocol/blob/main/BOND.md).
155
+
156
+ ```ts
157
+ import { bondFloors, bondNullifier, esploraSource, issueBondMessage, verifyBond } from '@orangecheck/sdk';
158
+
159
+ // Registry: hand the holder a message naming their coin and your subject.
160
+ const { message } = issueBondMessage({ address, outpoint: `${txid}:${vout}`, subject: didOc });
161
+
162
+ // The holder signs it with BIP-322 in their wallet, then you verify the
163
+ // signature and the coin: unspent, confirmed, 6+ deep, at that address.
164
+ const r = await verifyBond({ message, signature, subject: didOc, source: esploraSource() });
165
+ if (r.ok) {
166
+ const age = r.bond.tip.height - r.bond.confirmed_height + 1;
167
+ const claim = { ...bondFloors(r.bond.sats, age), nullifier: bondNullifier(audience, r.bond.outpoint) };
168
+ }
169
+ ```
170
+
171
+ `checkBondCoin` re-checks a stored bond's coin later; a spent coin ends the
172
+ bond. Ban a nullifier and you've banned the coin: the holder has to move it to
173
+ get past the ban, and moving it restarts its age.
174
+
175
+ ---
176
+
148
177
  ## Creating an attestation (issuer side)
149
178
 
150
179
  If you're building a wallet, a Nostr client, or a settings page where users should be able to _create_ a proof, use:
@@ -248,6 +277,13 @@ createAttestation(options: CreateAttestationOptions): Promise<AttestationEnvelop
248
277
  issueChallenge(options): Challenge
249
278
  verifyChallenge(options): Promise<VerifyChallengeResult>
250
279
 
280
+ // Bonds (BOND.md).
281
+ issueBondMessage(options): { message, nonce, expiresAt }
282
+ verifyBond(options): Promise<BondResult>
283
+ checkBondCoin(options): Promise<...>
284
+ bondNullifier(audience, outpoint): string
285
+ bondFloors(sats, ageBlocks): { sats_floor, days_floor }
286
+
251
287
  // Building blocks.
252
288
  buildCanonicalMessage(...)
253
289
  generateAttestationId(msg: string): Promise<string>
@@ -1,4 +1,4 @@
1
- import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-D9Tb1roj.mjs';
1
+ import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface CreateAttestationOptions {
4
4
  message: string;
@@ -1,4 +1,4 @@
1
- import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-D9Tb1roj.js';
1
+ import { S as Scheme, I as IdentityBinding, A as AttestationEnvelope } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface CreateAttestationOptions {
4
4
  message: string;
@@ -1,4 +1,4 @@
1
- import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-D9Tb1roj.mjs';
1
+ import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface CanonicalInput {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-D9Tb1roj.js';
1
+ import { I as IdentityBinding, S as Scheme, A as AttestationEnvelope } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface CanonicalInput {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { S as Scheme } from './types-D9Tb1roj.mjs';
1
+ import { S as Scheme } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface IssueChallengeOptions {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { S as Scheme } from './types-D9Tb1roj.js';
1
+ import { S as Scheme } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface IssueChallengeOptions {
4
4
  address: string;
@@ -1,4 +1,4 @@
1
- import { c as NostrEvent, I as IdentityBinding } from './types-D9Tb1roj.mjs';
1
+ import { c as NostrEvent, I as IdentityBinding } from './types-BBRMCgMo.mjs';
2
2
 
3
3
  interface DnsVerificationResult {
4
4
  verified: boolean;
@@ -1,4 +1,4 @@
1
- import { c as NostrEvent, I as IdentityBinding } from './types-D9Tb1roj.js';
1
+ import { c as NostrEvent, I as IdentityBinding } from './types-BBRMCgMo.js';
2
2
 
3
3
  interface DnsVerificationResult {
4
4
  verified: boolean;
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-D9Tb1roj.mjs';
2
- export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-D9Tb1roj.mjs';
1
+ import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BBRMCgMo.mjs';
2
+ export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BBRMCgMo.mjs';
3
3
  export { verify } from './verify.mjs';
4
4
  export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.mjs';
5
5
  export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.mjs';
@@ -36,6 +36,108 @@ declare function check(params: CheckParams): Promise<CheckResult>;
36
36
  declare function nostrPubkeyToHex(value: string): string | null;
37
37
  declare function nostrIdentifierForms(identifier: string): string[];
38
38
 
39
+ declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
40
+ declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
41
+ declare const BOND_MIN_CONFIRMATIONS = 6;
42
+ interface BondFields {
43
+ address: string;
44
+ outpoint: string;
45
+ subject: string;
46
+ nonce: string;
47
+ issued_at: string;
48
+ expires_at: string;
49
+ }
50
+ type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
51
+ interface BondCoin {
52
+ sats: number;
53
+ scriptPubKey: string;
54
+ confirmedHeight: number | null;
55
+ spent: boolean;
56
+ }
57
+ interface ChainTip {
58
+ height: number;
59
+ hash: string;
60
+ }
61
+ interface BondChainSource {
62
+ tip(): Promise<ChainTip>;
63
+ coin(outpoint: string): Promise<BondCoin | null>;
64
+ }
65
+ interface VerifiedBond {
66
+ bond_id: string;
67
+ subject: string;
68
+ address: string;
69
+ outpoint: string;
70
+ sats: number;
71
+ confirmed_height: number;
72
+ tip: ChainTip;
73
+ }
74
+ type BondResult = {
75
+ ok: true;
76
+ bond: VerifiedBond;
77
+ } | {
78
+ ok: false;
79
+ reason: BondReason;
80
+ };
81
+ declare function buildBondMessage(fields: BondFields): string;
82
+ declare function issueBondMessage(opts: {
83
+ address: string;
84
+ outpoint: string;
85
+ subject: string;
86
+ ttlSeconds?: number;
87
+ now?: Date;
88
+ }): {
89
+ message: string;
90
+ nonce: string;
91
+ expiresAt: number;
92
+ };
93
+ declare function parseBondMessage(message: string): BondFields | null;
94
+ declare function bondId(message: string): string;
95
+ declare function bondNullifier(audience: string, outpoint: string): string;
96
+ declare function bondFloors(sats: number, ageBlocks: number): {
97
+ sats_floor: number;
98
+ days_floor: number;
99
+ };
100
+ declare function verifyBondSignature(opts: {
101
+ message: string;
102
+ signature: string;
103
+ subject?: string;
104
+ now?: Date;
105
+ skewSeconds?: number;
106
+ }): {
107
+ ok: true;
108
+ fields: BondFields;
109
+ bond_id: string;
110
+ } | {
111
+ ok: false;
112
+ reason: BondReason;
113
+ };
114
+ declare function checkBondCoin(opts: {
115
+ address: string;
116
+ outpoint: string;
117
+ source: BondChainSource;
118
+ minConfirmations?: number;
119
+ }): Promise<{
120
+ ok: true;
121
+ sats: number;
122
+ confirmed_height: number;
123
+ tip: ChainTip;
124
+ } | {
125
+ ok: false;
126
+ reason: BondReason;
127
+ }>;
128
+ declare function verifyBond(opts: {
129
+ message: string;
130
+ signature: string;
131
+ source: BondChainSource;
132
+ subject?: string;
133
+ now?: Date;
134
+ minConfirmations?: number;
135
+ }): Promise<BondResult>;
136
+ declare function esploraSource(base?: string, opts?: {
137
+ timeoutMs?: number;
138
+ fetch?: typeof fetch;
139
+ }): BondChainSource;
140
+
39
141
  declare function isNip07Available(): boolean;
40
142
  declare function getNostrPublicKey(): Promise<string | null>;
41
143
  declare function signNostrEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent | null>;
@@ -53,4 +155,4 @@ interface StatusMeta {
53
155
  }
54
156
  declare const STATUS_META: Record<StatusCode, StatusMeta>;
55
157
 
56
- export { type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, VerifyOptions, check, getNip07Info, getNostrPublicKey, isNip07Available, nostrIdentifierForms, nostrPubkeyToHex, signNostrEvent };
158
+ export { BOND_DAYS_FLOORS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, type BondChainSource, type BondCoin, type BondFields, type BondReason, type BondResult, type ChainTip, type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, type VerifiedBond, VerifyOptions, bondFloors, bondId, bondNullifier, buildBondMessage, check, checkBondCoin, esploraSource, getNip07Info, getNostrPublicKey, isNip07Available, issueBondMessage, nostrIdentifierForms, nostrPubkeyToHex, parseBondMessage, signNostrEvent, verifyBond, verifyBondSignature };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-D9Tb1roj.js';
2
- export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-D9Tb1roj.js';
1
+ import { a as VerifyOptions, I as IdentityBinding, N as Network, c as NostrEvent, d as StatusCode } from './types-BBRMCgMo.js';
2
+ export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-BBRMCgMo.js';
3
3
  export { verify } from './verify.js';
4
4
  export { createAttestation, discoverAttestations, extractAttestationIdFromUrl, formatIdentitiesForDisplay, getAttestationsForAddress, getAttestationsForIdentity, getVerificationUrl, publishAttestation, verifyAttestationById } from './attestation.js';
5
5
  export { Challenge, IssueChallengeOptions, VerifyChallengeOptions, VerifyChallengeReason, VerifyChallengeResult, issueChallenge, verifyChallenge } from './challenge.js';
@@ -36,6 +36,108 @@ declare function check(params: CheckParams): Promise<CheckResult>;
36
36
  declare function nostrPubkeyToHex(value: string): string | null;
37
37
  declare function nostrIdentifierForms(identifier: string): string[];
38
38
 
39
+ declare const BOND_SATS_FLOORS: readonly [1000, 10000, 100000, 1000000, 10000000];
40
+ declare const BOND_DAYS_FLOORS: readonly [7, 30, 90, 365];
41
+ declare const BOND_MIN_CONFIRMATIONS = 6;
42
+ interface BondFields {
43
+ address: string;
44
+ outpoint: string;
45
+ subject: string;
46
+ nonce: string;
47
+ issued_at: string;
48
+ expires_at: string;
49
+ }
50
+ type BondReason = 'bond_malformed' | 'bond_subject_mismatch' | 'bond_expired' | 'bond_not_yet_valid' | 'sig_invalid' | 'coin_not_found' | 'coin_address_mismatch' | 'coin_unconfirmed' | 'coin_too_recent' | 'coin_spent' | 'chain_unavailable';
51
+ interface BondCoin {
52
+ sats: number;
53
+ scriptPubKey: string;
54
+ confirmedHeight: number | null;
55
+ spent: boolean;
56
+ }
57
+ interface ChainTip {
58
+ height: number;
59
+ hash: string;
60
+ }
61
+ interface BondChainSource {
62
+ tip(): Promise<ChainTip>;
63
+ coin(outpoint: string): Promise<BondCoin | null>;
64
+ }
65
+ interface VerifiedBond {
66
+ bond_id: string;
67
+ subject: string;
68
+ address: string;
69
+ outpoint: string;
70
+ sats: number;
71
+ confirmed_height: number;
72
+ tip: ChainTip;
73
+ }
74
+ type BondResult = {
75
+ ok: true;
76
+ bond: VerifiedBond;
77
+ } | {
78
+ ok: false;
79
+ reason: BondReason;
80
+ };
81
+ declare function buildBondMessage(fields: BondFields): string;
82
+ declare function issueBondMessage(opts: {
83
+ address: string;
84
+ outpoint: string;
85
+ subject: string;
86
+ ttlSeconds?: number;
87
+ now?: Date;
88
+ }): {
89
+ message: string;
90
+ nonce: string;
91
+ expiresAt: number;
92
+ };
93
+ declare function parseBondMessage(message: string): BondFields | null;
94
+ declare function bondId(message: string): string;
95
+ declare function bondNullifier(audience: string, outpoint: string): string;
96
+ declare function bondFloors(sats: number, ageBlocks: number): {
97
+ sats_floor: number;
98
+ days_floor: number;
99
+ };
100
+ declare function verifyBondSignature(opts: {
101
+ message: string;
102
+ signature: string;
103
+ subject?: string;
104
+ now?: Date;
105
+ skewSeconds?: number;
106
+ }): {
107
+ ok: true;
108
+ fields: BondFields;
109
+ bond_id: string;
110
+ } | {
111
+ ok: false;
112
+ reason: BondReason;
113
+ };
114
+ declare function checkBondCoin(opts: {
115
+ address: string;
116
+ outpoint: string;
117
+ source: BondChainSource;
118
+ minConfirmations?: number;
119
+ }): Promise<{
120
+ ok: true;
121
+ sats: number;
122
+ confirmed_height: number;
123
+ tip: ChainTip;
124
+ } | {
125
+ ok: false;
126
+ reason: BondReason;
127
+ }>;
128
+ declare function verifyBond(opts: {
129
+ message: string;
130
+ signature: string;
131
+ source: BondChainSource;
132
+ subject?: string;
133
+ now?: Date;
134
+ minConfirmations?: number;
135
+ }): Promise<BondResult>;
136
+ declare function esploraSource(base?: string, opts?: {
137
+ timeoutMs?: number;
138
+ fetch?: typeof fetch;
139
+ }): BondChainSource;
140
+
39
141
  declare function isNip07Available(): boolean;
40
142
  declare function getNostrPublicKey(): Promise<string | null>;
41
143
  declare function signNostrEvent(event: Omit<NostrEvent, 'id' | 'sig'>): Promise<NostrEvent | null>;
@@ -53,4 +155,4 @@ interface StatusMeta {
53
155
  }
54
156
  declare const STATUS_META: Record<StatusCode, StatusMeta>;
55
157
 
56
- export { type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, VerifyOptions, check, getNip07Info, getNostrPublicKey, isNip07Available, nostrIdentifierForms, nostrPubkeyToHex, signNostrEvent };
158
+ export { BOND_DAYS_FLOORS, BOND_MIN_CONFIRMATIONS, BOND_SATS_FLOORS, type BondChainSource, type BondCoin, type BondFields, type BondReason, type BondResult, type ChainTip, type CheckParams, type CheckResult, IdentityBinding, Network, NostrEvent, STATUS_META, type Severity, StatusCode, type StatusMeta, type VerifiedBond, VerifyOptions, bondFloors, bondId, bondNullifier, buildBondMessage, check, checkBondCoin, esploraSource, getNip07Info, getNostrPublicKey, isNip07Available, issueBondMessage, nostrIdentifierForms, nostrPubkeyToHex, parseBondMessage, signNostrEvent, verifyBond, verifyBondSignature };
package/dist/index.js CHANGED
@@ -3,8 +3,9 @@
3
3
  var base = require('@scure/base');
4
4
  var buffer = require('buffer');
5
5
  var bip322 = require('@orangecheck/bip322');
6
- var secp256k1_js = require('@noble/curves/secp256k1.js');
7
6
  var sha2_js = require('@noble/hashes/sha2.js');
7
+ var utils_js = require('@noble/hashes/utils.js');
8
+ var secp256k1_js = require('@noble/curves/secp256k1.js');
8
9
 
9
10
  // src/canonical.ts
10
11
  function toLowerHex(bytes) {
@@ -1026,7 +1027,7 @@ async function getAddressUtxos(address, network, opts) {
1026
1027
  log4.error({ endpoints, error: lastError?.message, network }, "All Esplora endpoints failed");
1027
1028
  throw new Error(`esplora_all_failed: ${lastError?.message || "unknown"}`);
1028
1029
  }
1029
- function computeMetrics(utxos, bondAmount) {
1030
+ function computeMetrics(utxos, bondAmount, coverSats) {
1030
1031
  const confirmedUtxos = utxos.filter((u) => u.status.confirmed);
1031
1032
  if (confirmedUtxos.length === 0) {
1032
1033
  return { sats_bonded: 0, days_unspent: 0, confirmed_balance: 0 };
@@ -1059,6 +1060,23 @@ function computeMetrics(utxos, bondAmount) {
1059
1060
  };
1060
1061
  }
1061
1062
  const sats_bonded = confirmed_balance;
1063
+ if (coverSats !== void 0 && coverSats > 0) {
1064
+ const sorted = [...confirmedUtxos].sort(
1065
+ (a, b) => (a.status.block_height ?? Infinity) - (b.status.block_height ?? Infinity)
1066
+ );
1067
+ let sum = 0;
1068
+ let youngestTime = 0;
1069
+ for (const utxo of sorted) {
1070
+ sum += utxo.value;
1071
+ youngestTime = Math.max(youngestTime, utxo.status.block_time || Date.now() / 1e3);
1072
+ if (sum >= coverSats) break;
1073
+ }
1074
+ const days_unspent2 = Math.max(
1075
+ 0,
1076
+ Math.floor((Date.now() / 1e3 - youngestTime) / (24 * 60 * 60))
1077
+ );
1078
+ return { sats_bonded, days_unspent: days_unspent2, confirmed_balance };
1079
+ }
1062
1080
  const oldestTime = Math.min(
1063
1081
  ...confirmedUtxos.map((u) => u.status.block_time || Date.now() / 1e3)
1064
1082
  );
@@ -1122,7 +1140,8 @@ async function verify(input, opts = {}) {
1122
1140
  });
1123
1141
  const { sats_bonded, days_unspent, confirmed_balance } = computeMetrics(
1124
1142
  utxos,
1125
- parsed.bond
1143
+ parsed.bond,
1144
+ opts.coverSats
1126
1145
  );
1127
1146
  if (parsed.bond !== void 0 && parsed.bond > 0) {
1128
1147
  if (confirmed_balance < parsed.bond) {
@@ -1227,7 +1246,7 @@ async function check(params) {
1227
1246
  sig: env.signature,
1228
1247
  scheme: env.scheme
1229
1248
  },
1230
- verifyOptions
1249
+ { ...verifyOptions, coverSats: minSats > 0 ? minSats : void 0 }
1231
1250
  );
1232
1251
  const sats = outcome.metrics?.sats_bonded ?? 0;
1233
1252
  const days = outcome.metrics?.days_unspent ?? 0;
@@ -1431,6 +1450,164 @@ async function verifyChallenge(opts) {
1431
1450
  ...purpose !== void 0 ? { purpose } : {}
1432
1451
  };
1433
1452
  }
1453
+ var HEADER = "orangecheck-bond";
1454
+ var ACK = "I pledge this coin to back this subject. It stays in my wallet.";
1455
+ var FIELDS = ["address", "outpoint", "subject", "nonce", "issued_at", "expires_at"];
1456
+ var BOND_SATS_FLOORS = [1e3, 1e4, 1e5, 1e6, 1e7];
1457
+ var BOND_DAYS_FLOORS = [7, 30, 90, 365];
1458
+ var BOND_MIN_CONFIRMATIONS = 6;
1459
+ var OUTPOINT_RE = /^[0-9a-f]{64}:(0|[1-9][0-9]{0,9})$/;
1460
+ var NONCE_RE = /^[0-9a-f]{32}$/;
1461
+ var TIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
1462
+ var SUBJECT_RE = /^[\x21-\x7e]{1,256}$/;
1463
+ function iso2(d) {
1464
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
1465
+ }
1466
+ function randomNonce2() {
1467
+ const b = new Uint8Array(16);
1468
+ globalThis.crypto.getRandomValues(b);
1469
+ return utils_js.bytesToHex(b);
1470
+ }
1471
+ function wellFormed(f) {
1472
+ return bip322.decodeAddress(f.address) !== null && OUTPOINT_RE.test(f.outpoint) && SUBJECT_RE.test(f.subject) && NONCE_RE.test(f.nonce) && TIME_RE.test(f.issued_at) && TIME_RE.test(f.expires_at) && Date.parse(f.expires_at) > Date.parse(f.issued_at);
1473
+ }
1474
+ function buildBondMessage(fields) {
1475
+ if (!wellFormed(fields)) throw new Error("bond fields are malformed");
1476
+ return [HEADER, ...FIELDS.map((k) => `${k}: ${fields[k]}`), `ack: ${ACK}`].join("\n") + "\n";
1477
+ }
1478
+ function issueBondMessage(opts) {
1479
+ const now = opts.now ?? /* @__PURE__ */ new Date();
1480
+ const expires = new Date(now.getTime() + (opts.ttlSeconds ?? 600) * 1e3);
1481
+ const nonce = randomNonce2();
1482
+ const message = buildBondMessage({
1483
+ address: opts.address,
1484
+ outpoint: opts.outpoint,
1485
+ subject: opts.subject,
1486
+ nonce,
1487
+ issued_at: iso2(now),
1488
+ expires_at: iso2(expires)
1489
+ });
1490
+ return { message, nonce, expiresAt: expires.getTime() };
1491
+ }
1492
+ function parseBondMessage(message) {
1493
+ if (!message.endsWith("\n") || message.includes("\r")) return null;
1494
+ const lines = message.slice(0, -1).split("\n");
1495
+ if (lines.length !== FIELDS.length + 2 || lines[0] !== HEADER) return null;
1496
+ if (lines[lines.length - 1] !== `ack: ${ACK}`) return null;
1497
+ const out = {};
1498
+ for (const [i, key] of FIELDS.entries()) {
1499
+ const line = lines[i + 1];
1500
+ const prefix = `${key}: `;
1501
+ if (!line.startsWith(prefix)) return null;
1502
+ out[key] = line.slice(prefix.length);
1503
+ }
1504
+ const fields = out;
1505
+ return wellFormed(fields) && buildBondMessage(fields) === message ? fields : null;
1506
+ }
1507
+ function bondId(message) {
1508
+ return utils_js.bytesToHex(sha2_js.sha256(utils_js.utf8ToBytes(message)));
1509
+ }
1510
+ function bondNullifier(audience, outpoint) {
1511
+ return utils_js.bytesToHex(sha2_js.sha256(utils_js.utf8ToBytes(`orangecheck-bond-nullifier
1512
+ ${audience}
1513
+ ${outpoint}`)));
1514
+ }
1515
+ function bondFloors(sats, ageBlocks) {
1516
+ const days = Math.floor(ageBlocks / 144);
1517
+ const floor = (ladder, v) => ladder.filter((f) => v >= f).reduce((a, b) => Math.max(a, b), 0);
1518
+ return { sats_floor: floor(BOND_SATS_FLOORS, sats), days_floor: floor(BOND_DAYS_FLOORS, days) };
1519
+ }
1520
+ function verifyBondSignature(opts) {
1521
+ const fields = parseBondMessage(opts.message);
1522
+ if (!fields) return { ok: false, reason: "bond_malformed" };
1523
+ if (opts.subject !== void 0 && fields.subject !== opts.subject) {
1524
+ return { ok: false, reason: "bond_subject_mismatch" };
1525
+ }
1526
+ const now = (opts.now ?? /* @__PURE__ */ new Date()).getTime();
1527
+ const skew = (opts.skewSeconds ?? 60) * 1e3;
1528
+ if (Date.parse(fields.issued_at) - skew > now) return { ok: false, reason: "bond_not_yet_valid" };
1529
+ if (Date.parse(fields.expires_at) + skew < now) return { ok: false, reason: "bond_expired" };
1530
+ if (!bip322.verifySignature(fields.address, opts.message, opts.signature)) {
1531
+ return { ok: false, reason: "sig_invalid" };
1532
+ }
1533
+ return { ok: true, fields, bond_id: bondId(opts.message) };
1534
+ }
1535
+ async function checkBondCoin(opts) {
1536
+ const decoded = bip322.decodeAddress(opts.address);
1537
+ if (!decoded || !OUTPOINT_RE.test(opts.outpoint)) return { ok: false, reason: "bond_malformed" };
1538
+ let coin;
1539
+ let tip;
1540
+ try {
1541
+ [coin, tip] = await Promise.all([opts.source.coin(opts.outpoint), opts.source.tip()]);
1542
+ } catch {
1543
+ return { ok: false, reason: "chain_unavailable" };
1544
+ }
1545
+ if (!coin) return { ok: false, reason: "coin_not_found" };
1546
+ if (coin.scriptPubKey !== utils_js.bytesToHex(decoded.scriptPubKey)) {
1547
+ return { ok: false, reason: "coin_address_mismatch" };
1548
+ }
1549
+ if (coin.confirmedHeight === null) return { ok: false, reason: "coin_unconfirmed" };
1550
+ if (coin.spent) return { ok: false, reason: "coin_spent" };
1551
+ const confirmations = tip.height - coin.confirmedHeight + 1;
1552
+ if (confirmations < (opts.minConfirmations ?? BOND_MIN_CONFIRMATIONS)) {
1553
+ return { ok: false, reason: "coin_too_recent" };
1554
+ }
1555
+ return { ok: true, sats: coin.sats, confirmed_height: coin.confirmedHeight, tip };
1556
+ }
1557
+ async function verifyBond(opts) {
1558
+ const sig = verifyBondSignature(opts);
1559
+ if (!sig.ok) return sig;
1560
+ const coin = await checkBondCoin({
1561
+ address: sig.fields.address,
1562
+ outpoint: sig.fields.outpoint,
1563
+ source: opts.source,
1564
+ minConfirmations: opts.minConfirmations
1565
+ });
1566
+ if (!coin.ok) return coin;
1567
+ return {
1568
+ ok: true,
1569
+ bond: {
1570
+ bond_id: sig.bond_id,
1571
+ subject: sig.fields.subject,
1572
+ address: sig.fields.address,
1573
+ outpoint: sig.fields.outpoint,
1574
+ sats: coin.sats,
1575
+ confirmed_height: coin.confirmed_height,
1576
+ tip: coin.tip
1577
+ }
1578
+ };
1579
+ }
1580
+ function esploraSource(base = "https://mempool.space/api", opts = {}) {
1581
+ const get = async (path) => {
1582
+ const r = await (opts.fetch ?? fetch)(`${base}${path}`, {
1583
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 5e3)
1584
+ });
1585
+ if (!r.ok && r.status !== 404) throw new Error(`esplora ${path}: HTTP ${r.status}`);
1586
+ return r;
1587
+ };
1588
+ return {
1589
+ async tip() {
1590
+ const [h, hash] = await Promise.all([get("/blocks/tip/height"), get("/blocks/tip/hash")]);
1591
+ return { height: Number(await h.text()), hash: (await hash.text()).trim() };
1592
+ },
1593
+ async coin(outpoint) {
1594
+ const [txid, voutStr] = outpoint.split(":");
1595
+ const vout = Number(voutStr);
1596
+ const txr = await get(`/tx/${txid}`);
1597
+ if (txr.status === 404) return null;
1598
+ const tx = await txr.json();
1599
+ const out = tx.vout[vout];
1600
+ if (!out) return null;
1601
+ const spend = await (await get(`/tx/${txid}/outspend/${vout}`)).json();
1602
+ return {
1603
+ sats: out.value,
1604
+ scriptPubKey: out.scriptpubkey.toLowerCase(),
1605
+ confirmedHeight: tx.status.confirmed ? tx.status.block_height ?? null : null,
1606
+ spent: spend.spent
1607
+ };
1608
+ }
1609
+ };
1610
+ }
1434
1611
 
1435
1612
  // src/utils/rate-limiter.ts
1436
1613
  var RateLimitError = class extends Error {
@@ -2220,17 +2397,26 @@ var STATUS_META = {
2220
2397
  }
2221
2398
  };
2222
2399
 
2400
+ exports.BOND_DAYS_FLOORS = BOND_DAYS_FLOORS;
2401
+ exports.BOND_MIN_CONFIRMATIONS = BOND_MIN_CONFIRMATIONS;
2402
+ exports.BOND_SATS_FLOORS = BOND_SATS_FLOORS;
2223
2403
  exports.DEFAULT_RELAYS = DEFAULT_RELAYS;
2224
2404
  exports.FANOUT_DEADLINE_MS = FANOUT_DEADLINE_MS;
2225
2405
  exports.STATUS_META = STATUS_META;
2406
+ exports.bondFloors = bondFloors;
2407
+ exports.bondId = bondId;
2408
+ exports.bondNullifier = bondNullifier;
2409
+ exports.buildBondMessage = buildBondMessage;
2226
2410
  exports.buildCanonicalMessage = buildCanonicalMessage;
2227
2411
  exports.check = check;
2412
+ exports.checkBondCoin = checkBondCoin;
2228
2413
  exports.computeAllScores = computeAllScores;
2229
2414
  exports.computeScore = computeScore;
2230
2415
  exports.createAttestation = createAttestation;
2231
2416
  exports.createAttestationEnvelope = createAttestationEnvelope;
2232
2417
  exports.createAttestationEvent = createAttestationEvent;
2233
2418
  exports.discoverAttestations = discoverAttestations;
2419
+ exports.esploraSource = esploraSource;
2234
2420
  exports.extractAttestationIdFromUrl = extractAttestationIdFromUrl;
2235
2421
  exports.formatIdentities = formatIdentities;
2236
2422
  exports.formatIdentitiesForDisplay = formatIdentitiesForDisplay;
@@ -2260,10 +2446,12 @@ exports.isNip07Available = isNip07Available;
2260
2446
  exports.isNostrIdentityVerified = isNostrIdentityVerified;
2261
2447
  exports.isValidDomain = isValidDomain;
2262
2448
  exports.isValidTweetUrl = isValidTweetUrl;
2449
+ exports.issueBondMessage = issueBondMessage;
2263
2450
  exports.issueChallenge = issueChallenge;
2264
2451
  exports.nostrIdentifierForms = nostrIdentifierForms;
2265
2452
  exports.nostrPubkeyToHex = nostrPubkeyToHex;
2266
2453
  exports.parseAttestationFromEvent = parseAttestationFromEvent;
2454
+ exports.parseBondMessage = parseBondMessage;
2267
2455
  exports.parseIdentities = parseIdentities;
2268
2456
  exports.publishAttestation = publishAttestation;
2269
2457
  exports.publishToRelays = publishToRelays;
@@ -2277,6 +2465,8 @@ exports.supportsAutomaticVerification = supportsAutomaticVerification;
2277
2465
  exports.verify = verify;
2278
2466
  exports.verifyAllIdentities = verifyAllIdentities;
2279
2467
  exports.verifyAttestationById = verifyAttestationById;
2468
+ exports.verifyBond = verifyBond;
2469
+ exports.verifyBondSignature = verifyBondSignature;
2280
2470
  exports.verifyChallenge = verifyChallenge;
2281
2471
  exports.verifyDnsIdentity = verifyDnsIdentity;
2282
2472
  exports.verifyGitHubIdentity = verifyGitHubIdentity;