@inversealtruism/cairn-sdk 0.2.2 → 0.3.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/dist/board.cjs +8 -1
- package/dist/board.js +2 -2
- package/dist/{chunk-3EXZQPPT.js → chunk-4RDXEW35.js} +1 -1
- package/dist/{chunk-ZJURNM5P.js → chunk-E2Z3XSCW.js} +50 -1
- package/dist/{chunk-HI5STMH3.js → chunk-NLCF5T2F.js} +1 -1
- package/dist/{chunk-QRVMYGSL.js → chunk-QV2GIXO7.js} +23 -9
- package/dist/{chunk-LCE4IWMW.js → chunk-UMXLWUEF.js} +1 -1
- package/dist/connect.cjs +68 -8
- package/dist/connect.d.cts +17 -2
- package/dist/connect.d.ts +17 -2
- package/dist/connect.js +2 -2
- package/dist/content.cjs +8 -1
- package/dist/content.js +2 -2
- package/dist/controller.cjs +68 -8
- package/dist/controller.js +3 -3
- package/dist/index.cjs +220 -29
- package/dist/index.d.cts +111 -5
- package/dist/index.d.ts +111 -5
- package/dist/index.js +141 -13
- package/dist/react.cjs +68 -8
- package/dist/react.js +3 -3
- package/package.json +25 -18
package/dist/controller.cjs
CHANGED
|
@@ -25,7 +25,7 @@ __export(controller_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(controller_exports);
|
|
26
26
|
|
|
27
27
|
// src/errors.ts
|
|
28
|
-
var SDK_VERSION = "0.
|
|
28
|
+
var SDK_VERSION = "0.3.0";
|
|
29
29
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
30
30
|
var CairnError = class extends Error {
|
|
31
31
|
/** Stable machine code for programmatic branching. */
|
|
@@ -36,6 +36,12 @@ var CairnError = class extends Error {
|
|
|
36
36
|
docsPath;
|
|
37
37
|
/** The cairn-sdk version that produced this error. */
|
|
38
38
|
version = SDK_VERSION;
|
|
39
|
+
/**
|
|
40
|
+
* Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
|
|
41
|
+
* declines (the default); `true` only for provably transient infra faults where nothing was signed.
|
|
42
|
+
* A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
|
|
43
|
+
*/
|
|
44
|
+
retryable;
|
|
39
45
|
constructor(shortMessage, opts = {}) {
|
|
40
46
|
const docs = opts.docsPath;
|
|
41
47
|
super(
|
|
@@ -46,6 +52,7 @@ var CairnError = class extends Error {
|
|
|
46
52
|
this.code = opts.code ?? "UNKNOWN";
|
|
47
53
|
this.shortMessage = shortMessage;
|
|
48
54
|
this.docsPath = docs;
|
|
55
|
+
this.retryable = opts.retryable ?? false;
|
|
49
56
|
}
|
|
50
57
|
/**
|
|
51
58
|
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
@@ -82,11 +89,43 @@ var UnsupportedMethodError = class extends CairnError {
|
|
|
82
89
|
super(message, { code: "UNSUPPORTED_METHOD", docsPath: "/errors#unsupported-method" });
|
|
83
90
|
}
|
|
84
91
|
};
|
|
92
|
+
var SubmitInFlightError = class extends CairnError {
|
|
93
|
+
/** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
|
|
94
|
+
txid;
|
|
95
|
+
/** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
|
|
96
|
+
maybeSent = true;
|
|
97
|
+
constructor(message, code, txid) {
|
|
98
|
+
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false });
|
|
99
|
+
this.txid = txid;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
85
102
|
var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
|
|
103
|
+
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
|
|
104
|
+
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
105
|
+
"FILL_UNSAFE",
|
|
106
|
+
"FILL_WRONG_TARGET",
|
|
107
|
+
"SOURCE_DIVERGENCE",
|
|
108
|
+
"VERIFY_TAMPER",
|
|
109
|
+
"SUBMIT_REJECTED",
|
|
110
|
+
"BAD_REQUEST",
|
|
111
|
+
"BAD_FEE",
|
|
112
|
+
"BAD_OUTPUTS",
|
|
113
|
+
"NO_OUTPUTS",
|
|
114
|
+
"ZERO_ADDR_REFUSED",
|
|
115
|
+
"TOO_MANY_INPUTS",
|
|
116
|
+
"INSUFFICIENT",
|
|
117
|
+
"FEE_CAP",
|
|
118
|
+
"FEE_TOO_LOW",
|
|
119
|
+
"ACCOUNT_CHANGED",
|
|
120
|
+
"GHOST_INPUTS_SKIPPED",
|
|
121
|
+
"NOTHING_TO_CONSOLIDATE"
|
|
122
|
+
]);
|
|
86
123
|
function mapProviderError(error, code) {
|
|
87
124
|
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
88
125
|
if (code === "WALLET_LOCKED") return new WalletLockedError(error);
|
|
89
126
|
if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
|
|
127
|
+
if (code && NESTED_TRANSIENT.has(code)) return new CairnError(error || code, { code, retryable: true });
|
|
128
|
+
if (code && NESTED_TERMINAL.has(code)) return new CairnError(error || code, { code, retryable: false });
|
|
90
129
|
if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
|
|
91
130
|
const e = String(error || "").toLowerCase();
|
|
92
131
|
if (e.includes("rejected by user")) return new UserRejectedError(error);
|
|
@@ -94,6 +133,14 @@ function mapProviderError(error, code) {
|
|
|
94
133
|
if (e.includes("unsupported dapp method") || e.includes("predates") || e.includes("too old")) return new UnsupportedMethodError(error);
|
|
95
134
|
return new CairnError(error || "Unknown wallet error");
|
|
96
135
|
}
|
|
136
|
+
function mapSubmitResultError(result) {
|
|
137
|
+
const code = result?.code;
|
|
138
|
+
const msg = typeof result?.error === "string" && result.error || code || "The wallet refused the transaction.";
|
|
139
|
+
if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE") {
|
|
140
|
+
return new SubmitInFlightError(msg, code, typeof result?.txid === "string" ? result.txid : void 0);
|
|
141
|
+
}
|
|
142
|
+
return mapProviderError(msg, code);
|
|
143
|
+
}
|
|
97
144
|
|
|
98
145
|
// src/connect.ts
|
|
99
146
|
function detectProvider(opts = {}) {
|
|
@@ -142,6 +189,13 @@ function unwrap(reply) {
|
|
|
142
189
|
if (reply && reply.ok) return reply.result;
|
|
143
190
|
throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
144
191
|
}
|
|
192
|
+
function unwrapWrite(reply) {
|
|
193
|
+
const result = unwrap(reply);
|
|
194
|
+
if (result && typeof result === "object" && result.ok === false) {
|
|
195
|
+
throw mapSubmitResultError(result);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
145
199
|
var WalletConnection = class {
|
|
146
200
|
provider;
|
|
147
201
|
_addr = null;
|
|
@@ -208,12 +262,18 @@ var WalletConnection = class {
|
|
|
208
262
|
return null;
|
|
209
263
|
}
|
|
210
264
|
}
|
|
211
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* CairnX fill: pay + attest in ONE tx (atomic SETTLEMENT SHAPE, both revert together). Always prompts with
|
|
267
|
+
* clear-signing. The payment recipient in `params.outputs` is RESOLVER-TRUSTED here: this SDK does NOT
|
|
268
|
+
* SPV-verify it, so a lying resolver can redirect the payment. A dApp settling real value MUST corroborate
|
|
269
|
+
* the offer on-chain before building `outputs` (F13). The wallet's own fill-SPV boundary (cairn-wallet
|
|
270
|
+
* 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
|
|
271
|
+
*/
|
|
212
272
|
fillOffer(params) {
|
|
213
273
|
if (typeof this.provider.fillOffer !== "function") {
|
|
214
274
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
215
275
|
}
|
|
216
|
-
return Promise.resolve(this.provider.fillOffer(params)).then(
|
|
276
|
+
return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
|
|
217
277
|
}
|
|
218
278
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
219
279
|
async getPermissions() {
|
|
@@ -246,26 +306,26 @@ var WalletConnection = class {
|
|
|
246
306
|
}
|
|
247
307
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
248
308
|
send(params) {
|
|
249
|
-
return Promise.resolve(this.provider.send(params)).then(
|
|
309
|
+
return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
|
|
250
310
|
}
|
|
251
311
|
/** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
|
|
252
312
|
propose(params) {
|
|
253
|
-
return Promise.resolve(this.provider.propose(params)).then(
|
|
313
|
+
return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
|
|
254
314
|
}
|
|
255
315
|
/** Attest/support a proposal (always prompts). */
|
|
256
316
|
attest(params) {
|
|
257
|
-
return Promise.resolve(this.provider.attest(params)).then(
|
|
317
|
+
return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
|
|
258
318
|
}
|
|
259
319
|
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
260
320
|
sealClaim(params) {
|
|
261
|
-
return Promise.resolve(this.provider.sealClaim(params)).then(
|
|
321
|
+
return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
|
|
262
322
|
}
|
|
263
323
|
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
264
324
|
revealClaim(txid) {
|
|
265
325
|
if (typeof txid !== "string" || !txid) {
|
|
266
326
|
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
267
327
|
}
|
|
268
|
-
return Promise.resolve(this.provider.revealClaim(txid)).then(
|
|
328
|
+
return Promise.resolve(this.provider.revealClaim(txid)).then(unwrapWrite);
|
|
269
329
|
}
|
|
270
330
|
};
|
|
271
331
|
async function getWallet(opts = {}) {
|
package/dist/controller.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -35,22 +35,27 @@ __export(index_exports, {
|
|
|
35
35
|
NotInstalledError: () => NotInstalledError,
|
|
36
36
|
RegistryClient: () => RegistryClient,
|
|
37
37
|
SDK_VERSION: () => SDK_VERSION,
|
|
38
|
+
SubmitInFlightError: () => SubmitInFlightError,
|
|
38
39
|
UnsupportedMethodError: () => UnsupportedMethodError,
|
|
39
40
|
UserRejectedError: () => UserRejectedError,
|
|
40
41
|
WalletConnection: () => WalletConnection,
|
|
41
42
|
WalletLockedError: () => WalletLockedError,
|
|
43
|
+
bindOfferTerms: () => bindOfferTerms,
|
|
42
44
|
connect: () => connect,
|
|
43
45
|
detectProvider: () => detectProvider,
|
|
44
46
|
discoverProviders: () => discoverProviders,
|
|
45
47
|
errorCode: () => errorCode,
|
|
48
|
+
feeBpsAt: () => feeBpsAt,
|
|
46
49
|
getWallet: () => getWallet,
|
|
47
50
|
isInstalled: () => isInstalled,
|
|
48
|
-
mapProviderError: () => mapProviderError
|
|
51
|
+
mapProviderError: () => mapProviderError,
|
|
52
|
+
mapSubmitResultError: () => mapSubmitResultError,
|
|
53
|
+
preverifyOffer: () => preverifyOffer
|
|
49
54
|
});
|
|
50
55
|
module.exports = __toCommonJS(index_exports);
|
|
51
56
|
|
|
52
57
|
// src/errors.ts
|
|
53
|
-
var SDK_VERSION = "0.
|
|
58
|
+
var SDK_VERSION = "0.3.0";
|
|
54
59
|
var DOCS_BASE = "https://cairn-substrate.com/docs/sdk";
|
|
55
60
|
var CairnError = class extends Error {
|
|
56
61
|
/** Stable machine code for programmatic branching. */
|
|
@@ -61,6 +66,12 @@ var CairnError = class extends Error {
|
|
|
61
66
|
docsPath;
|
|
62
67
|
/** The cairn-sdk version that produced this error. */
|
|
63
68
|
version = SDK_VERSION;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a dApp may safely AUTO-retry this failure. `false` for fund-safety refusals and user-intent
|
|
71
|
+
* declines (the default); `true` only for provably transient infra faults where nothing was signed.
|
|
72
|
+
* A `SubmitInFlightError` is `false` too: reconcile its `txid`, never blind-retry.
|
|
73
|
+
*/
|
|
74
|
+
retryable;
|
|
64
75
|
constructor(shortMessage, opts = {}) {
|
|
65
76
|
const docs = opts.docsPath;
|
|
66
77
|
super(
|
|
@@ -71,6 +82,7 @@ var CairnError = class extends Error {
|
|
|
71
82
|
this.code = opts.code ?? "UNKNOWN";
|
|
72
83
|
this.shortMessage = shortMessage;
|
|
73
84
|
this.docsPath = docs;
|
|
85
|
+
this.retryable = opts.retryable ?? false;
|
|
74
86
|
}
|
|
75
87
|
/**
|
|
76
88
|
* Walk the `cause` chain. With a predicate, returns the FIRST matching error (or null). Without one,
|
|
@@ -125,11 +137,43 @@ var ContentVerificationError = class extends CairnError {
|
|
|
125
137
|
this.expected = expected;
|
|
126
138
|
}
|
|
127
139
|
};
|
|
140
|
+
var SubmitInFlightError = class extends CairnError {
|
|
141
|
+
/** The locally-computed consensus txid to reconcile against the chain (may be undefined if the wallet omitted it). */
|
|
142
|
+
txid;
|
|
143
|
+
/** Always true: the tx MAY be in the mempool. Reconcile via `txid`; never blind-retry. */
|
|
144
|
+
maybeSent = true;
|
|
145
|
+
constructor(message, code, txid3) {
|
|
146
|
+
super(message, { code, docsPath: "/errors#submit-inflight", retryable: false });
|
|
147
|
+
this.txid = txid3;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
128
150
|
var NATIVE_CODES = /* @__PURE__ */ new Set(["ACCOUNT_CHANGED", "FIRST_PARTY_ONLY", "RATE_LIMITED", "APPROVAL_CLOSED", "FORBIDDEN", "INTERNAL"]);
|
|
151
|
+
var NESTED_TRANSIENT = /* @__PURE__ */ new Set(["VERIFY_UNAVAILABLE", "OFFER_UNKNOWN"]);
|
|
152
|
+
var NESTED_TERMINAL = /* @__PURE__ */ new Set([
|
|
153
|
+
"FILL_UNSAFE",
|
|
154
|
+
"FILL_WRONG_TARGET",
|
|
155
|
+
"SOURCE_DIVERGENCE",
|
|
156
|
+
"VERIFY_TAMPER",
|
|
157
|
+
"SUBMIT_REJECTED",
|
|
158
|
+
"BAD_REQUEST",
|
|
159
|
+
"BAD_FEE",
|
|
160
|
+
"BAD_OUTPUTS",
|
|
161
|
+
"NO_OUTPUTS",
|
|
162
|
+
"ZERO_ADDR_REFUSED",
|
|
163
|
+
"TOO_MANY_INPUTS",
|
|
164
|
+
"INSUFFICIENT",
|
|
165
|
+
"FEE_CAP",
|
|
166
|
+
"FEE_TOO_LOW",
|
|
167
|
+
"ACCOUNT_CHANGED",
|
|
168
|
+
"GHOST_INPUTS_SKIPPED",
|
|
169
|
+
"NOTHING_TO_CONSOLIDATE"
|
|
170
|
+
]);
|
|
129
171
|
function mapProviderError(error, code) {
|
|
130
172
|
if (code === "USER_REJECTED" || code === "APPROVAL_CLOSED") return new UserRejectedError(error);
|
|
131
173
|
if (code === "WALLET_LOCKED") return new WalletLockedError(error);
|
|
132
174
|
if (code === "UNSUPPORTED_METHOD" || code === "UNKNOWN_KIND") return new UnsupportedMethodError(error);
|
|
175
|
+
if (code && NESTED_TRANSIENT.has(code)) return new CairnError(error || code, { code, retryable: true });
|
|
176
|
+
if (code && NESTED_TERMINAL.has(code)) return new CairnError(error || code, { code, retryable: false });
|
|
133
177
|
if (code && NATIVE_CODES.has(code)) return new CairnError(error || code, { code });
|
|
134
178
|
const e = String(error || "").toLowerCase();
|
|
135
179
|
if (e.includes("rejected by user")) return new UserRejectedError(error);
|
|
@@ -137,6 +181,14 @@ function mapProviderError(error, code) {
|
|
|
137
181
|
if (e.includes("unsupported dapp method") || e.includes("predates") || e.includes("too old")) return new UnsupportedMethodError(error);
|
|
138
182
|
return new CairnError(error || "Unknown wallet error");
|
|
139
183
|
}
|
|
184
|
+
function mapSubmitResultError(result) {
|
|
185
|
+
const code = result?.code;
|
|
186
|
+
const msg = typeof result?.error === "string" && result.error || code || "The wallet refused the transaction.";
|
|
187
|
+
if (code === "SUBMIT_MAYBE_INFLIGHT" || code === "SUBMIT_DUPLICATE") {
|
|
188
|
+
return new SubmitInFlightError(msg, code, typeof result?.txid === "string" ? result.txid : void 0);
|
|
189
|
+
}
|
|
190
|
+
return mapProviderError(msg, code);
|
|
191
|
+
}
|
|
140
192
|
function errorCode(err) {
|
|
141
193
|
return err instanceof CairnError ? err.code : "UNKNOWN";
|
|
142
194
|
}
|
|
@@ -442,23 +494,23 @@ var BoardClient = class {
|
|
|
442
494
|
const expiresEpoch = Math.floor(height / import_csd_codec3.EPOCH_LEN) + lifetime;
|
|
443
495
|
const fee = input.fee ?? import_csd_codec3.MIN_FEE_PROPOSE;
|
|
444
496
|
const r = await wallet.propose({ domain: content.domain, payloadHash: ph, uri, expiresEpoch, fee });
|
|
445
|
-
const
|
|
446
|
-
if (!
|
|
447
|
-
const reg = await this.tryRegister(content,
|
|
448
|
-
return { txid:
|
|
497
|
+
const txid3 = String(r.txid ?? "");
|
|
498
|
+
if (!txid3) throw new CairnError("Propose submitted but no txid was returned by the wallet.");
|
|
499
|
+
const reg = await this.tryRegister(content, txid3);
|
|
500
|
+
return { txid: txid3, payloadHash: ph, content, registered: reg.ok, registerError: reg.error };
|
|
449
501
|
}
|
|
450
502
|
/**
|
|
451
503
|
* Register off-chain content for an already-submitted proposal txid. Idempotent.
|
|
452
504
|
* Returns { ok } — the cairn server verifies sha256(canonical) == on-chain payload_hash.
|
|
453
505
|
*/
|
|
454
|
-
async registerContent(content,
|
|
506
|
+
async registerContent(content, txid3) {
|
|
455
507
|
try {
|
|
456
508
|
const res = await this.http.postJson("/api/content", {
|
|
457
509
|
domain: content.domain,
|
|
458
510
|
title: content.title,
|
|
459
511
|
body: content.body,
|
|
460
512
|
links: content.links,
|
|
461
|
-
txid:
|
|
513
|
+
txid: txid3
|
|
462
514
|
});
|
|
463
515
|
return { ok: !!res.ok, error: res.ok ? void 0 : res.error };
|
|
464
516
|
} catch (e) {
|
|
@@ -482,12 +534,12 @@ var BoardClient = class {
|
|
|
482
534
|
if (!Number.isFinite(h)) throw new CairnError("Could not read chain tip height for expiry computation.");
|
|
483
535
|
return h;
|
|
484
536
|
}
|
|
485
|
-
async tryRegister(content,
|
|
537
|
+
async tryRegister(content, txid3) {
|
|
486
538
|
const delays = [0, 1500, 3e3, 5e3];
|
|
487
539
|
let last = { ok: false, error: "not attempted" };
|
|
488
540
|
for (const d of delays) {
|
|
489
541
|
if (d) await sleep(d);
|
|
490
|
-
last = await this.registerContent(content,
|
|
542
|
+
last = await this.registerContent(content, txid3);
|
|
491
543
|
if (last.ok) return last;
|
|
492
544
|
if (!/not found yet|not found/i.test(last.error ?? "")) return last;
|
|
493
545
|
}
|
|
@@ -594,10 +646,10 @@ var IndexerClient = class {
|
|
|
594
646
|
* same-origin RPC safe is the forward PoW re-verification, not an origin gate (which no longer exists).
|
|
595
647
|
* Fails closed: a malformed proof or a chain-disagreement returns "not-found" (the latter with equivocation:true).
|
|
596
648
|
*/
|
|
597
|
-
async verifyInclusion(
|
|
649
|
+
async verifyInclusion(txid3) {
|
|
598
650
|
let proof;
|
|
599
651
|
try {
|
|
600
|
-
proof = await this.txMerkleProof(
|
|
652
|
+
proof = await this.txMerkleProof(txid3);
|
|
601
653
|
} catch {
|
|
602
654
|
return { included: false, trustLevel: "not-found", reason: "no merkle proof (tx not indexed)" };
|
|
603
655
|
}
|
|
@@ -606,7 +658,7 @@ var IndexerClient = class {
|
|
|
606
658
|
}
|
|
607
659
|
let folds = false;
|
|
608
660
|
try {
|
|
609
|
-
folds = (0, import_csd_codec4.verifyMerkleProof)(
|
|
661
|
+
folds = (0, import_csd_codec4.verifyMerkleProof)(txid3, proof.pos, proof.merkle, proof.merkle_root);
|
|
610
662
|
} catch {
|
|
611
663
|
return { included: false, trustLevel: "not-found", blockHeight: proof.block_height, reason: "malformed merkle proof" };
|
|
612
664
|
}
|
|
@@ -1031,6 +1083,13 @@ function unwrap(reply) {
|
|
|
1031
1083
|
if (reply && reply.ok) return reply.result;
|
|
1032
1084
|
throw mapProviderError(reply?.error ?? "Unknown wallet error", reply?.code);
|
|
1033
1085
|
}
|
|
1086
|
+
function unwrapWrite(reply) {
|
|
1087
|
+
const result = unwrap(reply);
|
|
1088
|
+
if (result && typeof result === "object" && result.ok === false) {
|
|
1089
|
+
throw mapSubmitResultError(result);
|
|
1090
|
+
}
|
|
1091
|
+
return result;
|
|
1092
|
+
}
|
|
1034
1093
|
var WalletConnection = class {
|
|
1035
1094
|
provider;
|
|
1036
1095
|
_addr = null;
|
|
@@ -1097,12 +1156,18 @@ var WalletConnection = class {
|
|
|
1097
1156
|
return null;
|
|
1098
1157
|
}
|
|
1099
1158
|
}
|
|
1100
|
-
/**
|
|
1159
|
+
/**
|
|
1160
|
+
* CairnX fill: pay + attest in ONE tx (atomic SETTLEMENT SHAPE, both revert together). Always prompts with
|
|
1161
|
+
* clear-signing. The payment recipient in `params.outputs` is RESOLVER-TRUSTED here: this SDK does NOT
|
|
1162
|
+
* SPV-verify it, so a lying resolver can redirect the payment. A dApp settling real value MUST corroborate
|
|
1163
|
+
* the offer on-chain before building `outputs` (F13). The wallet's own fill-SPV boundary (cairn-wallet
|
|
1164
|
+
* 0.2.60+, which binds payment/rebate/fee to the merkle-proven offer) is the payment-grade check; see FillParams.
|
|
1165
|
+
*/
|
|
1101
1166
|
fillOffer(params) {
|
|
1102
1167
|
if (typeof this.provider.fillOffer !== "function") {
|
|
1103
1168
|
return Promise.reject(new UnsupportedMethodError("this wallet predates fillOffer() \u2014 ask the user to update the Cairn Wallet"));
|
|
1104
1169
|
}
|
|
1105
|
-
return Promise.resolve(this.provider.fillOffer(params)).then(
|
|
1170
|
+
return Promise.resolve(this.provider.fillOffer(params)).then(unwrapWrite);
|
|
1106
1171
|
}
|
|
1107
1172
|
/** This origin's current permission grant (EIP-2255-style). Silent; [] if none / unsupported. */
|
|
1108
1173
|
async getPermissions() {
|
|
@@ -1135,26 +1200,26 @@ var WalletConnection = class {
|
|
|
1135
1200
|
}
|
|
1136
1201
|
/** Send CSD (always prompts with clear-signing; wallet picks inputs + returns change to itself). */
|
|
1137
1202
|
send(params) {
|
|
1138
|
-
return Promise.resolve(this.provider.send(params)).then(
|
|
1203
|
+
return Promise.resolve(this.provider.send(params)).then(unwrapWrite);
|
|
1139
1204
|
}
|
|
1140
1205
|
/** Low-level Propose (always prompts). Prefer `cairn.board.propose()` which computes the hash/uri/expiry. */
|
|
1141
1206
|
propose(params) {
|
|
1142
|
-
return Promise.resolve(this.provider.propose(params)).then(
|
|
1207
|
+
return Promise.resolve(this.provider.propose(params)).then(unwrapWrite);
|
|
1143
1208
|
}
|
|
1144
1209
|
/** Attest/support a proposal (always prompts). */
|
|
1145
1210
|
attest(params) {
|
|
1146
|
-
return Promise.resolve(this.provider.attest(params)).then(
|
|
1211
|
+
return Promise.resolve(this.provider.attest(params)).then(unwrapWrite);
|
|
1147
1212
|
}
|
|
1148
1213
|
/** Seal a commit-reveal claim (always prompts; salt stays inside the wallet). */
|
|
1149
1214
|
sealClaim(params) {
|
|
1150
|
-
return Promise.resolve(this.provider.sealClaim(params)).then(
|
|
1215
|
+
return Promise.resolve(this.provider.sealClaim(params)).then(unwrapWrite);
|
|
1151
1216
|
}
|
|
1152
1217
|
/** Reveal a previously sealed claim by its sealing txid (always prompts). */
|
|
1153
|
-
revealClaim(
|
|
1154
|
-
if (typeof
|
|
1218
|
+
revealClaim(txid3) {
|
|
1219
|
+
if (typeof txid3 !== "string" || !txid3) {
|
|
1155
1220
|
return Promise.reject(new UnsupportedMethodError("revealClaim requires the sealing txid"));
|
|
1156
1221
|
}
|
|
1157
|
-
return Promise.resolve(this.provider.revealClaim(
|
|
1222
|
+
return Promise.resolve(this.provider.revealClaim(txid3)).then(unwrapWrite);
|
|
1158
1223
|
}
|
|
1159
1224
|
};
|
|
1160
1225
|
async function getWallet(opts = {}) {
|
|
@@ -1166,6 +1231,106 @@ async function connect(opts = {}) {
|
|
|
1166
1231
|
return { wallet, address };
|
|
1167
1232
|
}
|
|
1168
1233
|
|
|
1234
|
+
// src/fillverify.ts
|
|
1235
|
+
var import_csd_client3 = require("@inversealtruism/csd-client");
|
|
1236
|
+
var import_csd_codec6 = require("@inversealtruism/csd-codec");
|
|
1237
|
+
var import_cairnx_core2 = require("@inversealtruism/cairnx-core");
|
|
1238
|
+
var ADDR = /^0x[0-9a-f]{40}$/;
|
|
1239
|
+
var HASH = /^0x[0-9a-f]{64}$/;
|
|
1240
|
+
var COINBASE = "0x" + "00".repeat(32);
|
|
1241
|
+
var feeBpsAt = (height) => height >= import_cairnx_core2.V11_HEIGHT ? height >= import_cairnx_core2.V16_HEIGHT ? import_cairnx_core2.FEE_BPS_V16 : import_cairnx_core2.FEE_BPS : 0;
|
|
1242
|
+
function bindOfferTerms(servedOffer, t) {
|
|
1243
|
+
const o = servedOffer;
|
|
1244
|
+
const s = (v) => v === void 0 || v === null ? "" : String(v).toLowerCase();
|
|
1245
|
+
if (Number(o?.height) !== t.height) return true;
|
|
1246
|
+
if (Number(o?.feeBps) !== t.feeBps) return true;
|
|
1247
|
+
if (t.value !== void 0 && String(o?.want?.value) !== t.value) return true;
|
|
1248
|
+
if (s(o?.taker) !== s(t.taker)) return true;
|
|
1249
|
+
if (s(o?.bid) !== s(t.bid)) return true;
|
|
1250
|
+
const om = o?.min;
|
|
1251
|
+
if ((om !== void 0 && om !== null) !== (t.min !== void 0)) return true;
|
|
1252
|
+
if (t.min !== void 0 && String(om) !== t.min) return true;
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
async function provenAuthor(client, provenTx) {
|
|
1256
|
+
const in0 = provenTx.inputs?.[0];
|
|
1257
|
+
if (!in0 || String(in0.prevTxid).toLowerCase() === COINBASE && Number(in0.vout) === 4294967295) return null;
|
|
1258
|
+
let info;
|
|
1259
|
+
try {
|
|
1260
|
+
info = await client.tx(String(in0.prevTxid));
|
|
1261
|
+
} catch {
|
|
1262
|
+
return null;
|
|
1263
|
+
}
|
|
1264
|
+
const body = info?.tx ?? info;
|
|
1265
|
+
if (!body || !Array.isArray(body.outputs) || !Array.isArray(body.inputs)) return null;
|
|
1266
|
+
let src;
|
|
1267
|
+
try {
|
|
1268
|
+
src = (0, import_csd_client3.rpcTxToTx)(body);
|
|
1269
|
+
} catch {
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
if (String((0, import_csd_codec6.txid)(src)).toLowerCase() !== String(in0.prevTxid).toLowerCase()) return null;
|
|
1273
|
+
const out = src.outputs?.[Number(in0.vout)];
|
|
1274
|
+
const spk = out?.scriptPubkey ? String(out.scriptPubkey).toLowerCase() : null;
|
|
1275
|
+
return spk && ADDR.test(spk) ? spk : null;
|
|
1276
|
+
}
|
|
1277
|
+
async function preverifyOffer(opts) {
|
|
1278
|
+
const id = String(opts.offerId).toLowerCase();
|
|
1279
|
+
if (!HASH.test(id)) return { ok: false, trust: "unverified", reason: "invalid offer id" };
|
|
1280
|
+
let incl;
|
|
1281
|
+
try {
|
|
1282
|
+
incl = await opts.light.verifyTxInclusion(id);
|
|
1283
|
+
} catch (e) {
|
|
1284
|
+
return { ok: false, trust: "unverified", transient: true, reason: `couldn't reach the chain to verify the offer (${e?.message ?? e})` };
|
|
1285
|
+
}
|
|
1286
|
+
if (!incl.included || incl.trustLevel !== "verified-inclusion" || !incl.tx || !incl.appPayloadHash)
|
|
1287
|
+
return { ok: false, trust: "unverified", transient: true, blockHeight: incl.blockHeight, reason: incl.reason ?? `offer not merkle-proven on-chain (${incl.trustLevel})` };
|
|
1288
|
+
const blockHeight = Number(incl.blockHeight);
|
|
1289
|
+
let provenTx, provenId;
|
|
1290
|
+
try {
|
|
1291
|
+
provenTx = (0, import_csd_client3.rpcTxToTx)(incl.tx);
|
|
1292
|
+
provenId = (0, import_csd_codec6.txid)(provenTx);
|
|
1293
|
+
} catch {
|
|
1294
|
+
return { ok: false, trust: "unverified", reason: "the on-chain offer tx couldn't be re-derived" };
|
|
1295
|
+
}
|
|
1296
|
+
if (String(provenId).toLowerCase() !== id) return { ok: false, trust: "unverified", reason: "the merkle-proven tx doesn't match the offer id" };
|
|
1297
|
+
const app = incl.tx.app;
|
|
1298
|
+
if (!app || app.type !== "Propose") return { ok: false, trust: "unverified", reason: "the offer tx is not a Propose" };
|
|
1299
|
+
let rec;
|
|
1300
|
+
try {
|
|
1301
|
+
rec = (0, import_cairnx_core2.parseRecord)(app.uri, incl.appPayloadHash);
|
|
1302
|
+
} catch {
|
|
1303
|
+
rec = null;
|
|
1304
|
+
}
|
|
1305
|
+
if (!rec || rec.t !== "offer") return { ok: false, trust: "unverified", reason: "the offer record doesn't bind to its on-chain commitment" };
|
|
1306
|
+
if (String((0, import_csd_codec6.payloadHash)(rec)).toLowerCase() !== String(incl.appPayloadHash).toLowerCase())
|
|
1307
|
+
return { ok: false, trust: "unverified", reason: "the offer record doesn't match the on-chain (merkle-proven) commitment" };
|
|
1308
|
+
const seller = await provenAuthor(opts.client, provenTx);
|
|
1309
|
+
if (!seller) return { ok: false, trust: "verified", transient: true, blockHeight, reason: "couldn't prove the offer's on-chain author yet; the chain view may be catching up, try again" };
|
|
1310
|
+
const w = rec.want ?? {};
|
|
1311
|
+
const payto = w.payto && ADDR.test(String(w.payto).toLowerCase()) ? String(w.payto).toLowerCase() : seller;
|
|
1312
|
+
const terms = {
|
|
1313
|
+
height: blockHeight,
|
|
1314
|
+
feeBps: feeBpsAt(blockHeight),
|
|
1315
|
+
value: w.value !== void 0 ? String(w.value) : void 0,
|
|
1316
|
+
taker: rec.taker !== void 0 ? String(rec.taker).toLowerCase() : void 0,
|
|
1317
|
+
bid: rec.bid !== void 0 ? String(rec.bid).toLowerCase() : void 0,
|
|
1318
|
+
min: rec.min !== void 0 ? String(rec.min) : void 0
|
|
1319
|
+
};
|
|
1320
|
+
const base = { payto, seller, terms, blockHeight };
|
|
1321
|
+
if (opts.servedOffer !== void 0 && opts.servedOffer !== null) {
|
|
1322
|
+
const so = opts.servedOffer;
|
|
1323
|
+
const servedPayto = so?.want?.payto !== void 0 && so?.want?.payto !== null ? String(so.want.payto).toLowerCase() : "";
|
|
1324
|
+
if (servedPayto && servedPayto !== payto)
|
|
1325
|
+
return { ok: false, trust: "verified", reason: "the served payment recipient doesn't match the offer's on-chain author/record", ...base };
|
|
1326
|
+
if (so?.seller !== void 0 && so?.seller !== null && String(so.seller).toLowerCase() !== seller)
|
|
1327
|
+
return { ok: false, trust: "verified", reason: "the served seller doesn't match the offer's on-chain author (a lying resolver may be redirecting your payment)", ...base };
|
|
1328
|
+
if (bindOfferTerms(opts.servedOffer, terms))
|
|
1329
|
+
return { ok: false, trust: "verified", reason: "the served fee/rebate/partial terms don't match the offer's on-chain record (a lying resolver could mis-size the fill, causing a rejected fill after your payment)", ...base };
|
|
1330
|
+
}
|
|
1331
|
+
return { ok: true, trust: "verified", ...base };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1169
1334
|
// src/controller.ts
|
|
1170
1335
|
var DISCONNECTED = Object.freeze({ status: "disconnected", account: null, error: null });
|
|
1171
1336
|
var CairnController = class {
|
|
@@ -1307,18 +1472,39 @@ var Cairn = class {
|
|
|
1307
1472
|
async verifiedHeaderMerkleAt(height) {
|
|
1308
1473
|
if (!Number.isInteger(height) || height < this._spvCp.height)
|
|
1309
1474
|
throw new Error(`height ${height} is below the SPV checkpoint ${this._spvCp.height} (cannot PoW-verify backward)`);
|
|
1310
|
-
|
|
1311
|
-
const lc = this._spvLight;
|
|
1312
|
-
if (!this._spvSeeded) {
|
|
1313
|
-
await lc.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
|
|
1314
|
-
this._spvSeeded = true;
|
|
1315
|
-
}
|
|
1475
|
+
const lc = await this.seededSpvLight();
|
|
1316
1476
|
const haveTo = lc.baseHeight + lc.chain.length - 1;
|
|
1317
1477
|
if (haveTo < height) await lc.sync(height);
|
|
1318
1478
|
const vh = lc.chain[height - lc.baseHeight];
|
|
1319
1479
|
if (!vh) throw new Error(`light client has no verified header at ${height}`);
|
|
1320
1480
|
return String(vh.header.merkle);
|
|
1321
1481
|
}
|
|
1482
|
+
/** The PoW-verifying SPV light client, seeded ONCE from the pinned checkpoint (shared by the M3 header-merkle
|
|
1483
|
+
* bind and the F13 offer pre-verify). Reused across calls; the first call near tip syncs the post-checkpoint span. */
|
|
1484
|
+
async seededSpvLight() {
|
|
1485
|
+
if (!this._spvLight) this._spvLight = new import_csd_light.LightClient({ client: this.chain.client, checkpoints: { [this._spvCp.height]: this._spvCp.hash } });
|
|
1486
|
+
if (!this._spvSeeded) {
|
|
1487
|
+
await this._spvLight.syncFromCheckpoint(this._spvCp.height, this._spvCp.hash);
|
|
1488
|
+
this._spvSeeded = true;
|
|
1489
|
+
}
|
|
1490
|
+
return this._spvLight;
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* F13 — corroborate an offer ON-CHAIN before building a `fillOffer` payment's `outputs`. Merkle-proves the
|
|
1494
|
+
* offer into the PoW-verified header chain (from the pinned SPV checkpoint), binds the record to its on-chain
|
|
1495
|
+
* commitment, derives the payment recipient + seller from the offer's on-chain AUTHOR (the funding input's
|
|
1496
|
+
* prevout owner, txid-committed, NOT the malleable scriptSig), and, if you pass the resolver-SERVED `offer`
|
|
1497
|
+
* object, binds its payto/seller + fee/rebate/partial terms to the proven ones. Returns a TRUST-LABELED result
|
|
1498
|
+
* (fail-closed on a positive mismatch, fail-soft `transient` on a lagging / below-checkpoint chain view).
|
|
1499
|
+
*
|
|
1500
|
+
* This is best-effort corroboration, NOT the payment-grade boundary: the Cairn Wallet's OWN on-device
|
|
1501
|
+
* fill-SPV (0.2.60+, fails-closed before signing) is that. A dApp settling real value should still clear this
|
|
1502
|
+
* before building `outputs`, so it never hands the wallet a payment a lying resolver would redirect.
|
|
1503
|
+
*/
|
|
1504
|
+
async verifyOfferForFill(offerId, servedOffer) {
|
|
1505
|
+
const light = await this.seededSpvLight();
|
|
1506
|
+
return preverifyOffer({ light, client: this.chain.client, offerId, servedOffer });
|
|
1507
|
+
}
|
|
1322
1508
|
/**
|
|
1323
1509
|
* Detect + connect the Cairn Wallet (browser only). Wires the wallet into
|
|
1324
1510
|
* `cairn.board` for signed writes. Resolves to the connected address.
|
|
@@ -1355,15 +1541,20 @@ var Cairn = class {
|
|
|
1355
1541
|
NotInstalledError,
|
|
1356
1542
|
RegistryClient,
|
|
1357
1543
|
SDK_VERSION,
|
|
1544
|
+
SubmitInFlightError,
|
|
1358
1545
|
UnsupportedMethodError,
|
|
1359
1546
|
UserRejectedError,
|
|
1360
1547
|
WalletConnection,
|
|
1361
1548
|
WalletLockedError,
|
|
1549
|
+
bindOfferTerms,
|
|
1362
1550
|
connect,
|
|
1363
1551
|
detectProvider,
|
|
1364
1552
|
discoverProviders,
|
|
1365
1553
|
errorCode,
|
|
1554
|
+
feeBpsAt,
|
|
1366
1555
|
getWallet,
|
|
1367
1556
|
isInstalled,
|
|
1368
|
-
mapProviderError
|
|
1557
|
+
mapProviderError,
|
|
1558
|
+
mapSubmitResultError,
|
|
1559
|
+
preverifyOffer
|
|
1369
1560
|
});
|