@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 +36 -0
- package/dist/attestation.d.mts +1 -1
- package/dist/attestation.d.ts +1 -1
- package/dist/canonical.d.mts +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/challenge.d.mts +1 -1
- package/dist/challenge.d.ts +1 -1
- package/dist/identity.d.mts +1 -1
- package/dist/identity.d.ts +1 -1
- package/dist/index.d.mts +105 -3
- package/dist/index.d.ts +105 -3
- package/dist/index.js +194 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +183 -6
- package/dist/index.mjs.map +1 -1
- package/dist/nostr.d.mts +1 -1
- package/dist/nostr.d.ts +1 -1
- package/dist/{types-D9Tb1roj.d.mts → types-BBRMCgMo.d.mts} +1 -0
- package/dist/{types-D9Tb1roj.d.ts → types-BBRMCgMo.d.ts} +1 -0
- package/dist/verify.d.mts +1 -1
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +20 -2
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +20 -2
- package/dist/verify.mjs.map +1 -1
- package/package.json +1 -1
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>
|
package/dist/attestation.d.mts
CHANGED
package/dist/attestation.d.ts
CHANGED
package/dist/canonical.d.mts
CHANGED
package/dist/canonical.d.ts
CHANGED
package/dist/challenge.d.mts
CHANGED
package/dist/challenge.d.ts
CHANGED
package/dist/identity.d.mts
CHANGED
package/dist/identity.d.ts
CHANGED
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-
|
|
2
|
-
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-
|
|
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-
|
|
2
|
-
export { A as AttestationEnvelope, e as IdentityProtocol, M as Metrics, S as Scheme, V as VerifyInput, b as VerifyOutcome } from './types-
|
|
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;
|