@integraledger/lcp-binding-canton-x402 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/dist/adapter.d.ts +38 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +72 -2
- package/dist/adapter.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +109 -8
- package/src/index.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @integraledger/lcp-binding-canton-x402
|
|
2
2
|
|
|
3
|
+
## 0.18.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- @integraledger/lcp-binding-core@0.18.1
|
|
8
|
+
- @integraledger/lcp-kernel@0.18.1
|
|
9
|
+
|
|
10
|
+
## 0.18.0
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- @integraledger/lcp-binding-core@0.18.0
|
|
15
|
+
- @integraledger/lcp-kernel@0.18.0
|
|
16
|
+
|
|
3
17
|
## 0.17.0
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -93,7 +93,9 @@ carrier is used. No private key is needed; a party's cooperation is.
|
|
|
93
93
|
|
|
94
94
|
**Not forward-indexable.** A participant's update stream is one participant's view, not a chain-global
|
|
95
95
|
index, and the memo is a metadata value rather than an indexed key. `enumerate` scans one party's visible
|
|
96
|
-
transfers and says so
|
|
96
|
+
transfers and says so — and its `limit` is required, because the scan depth is a bound somebody has to
|
|
97
|
+
choose and this package does not even guess the endpoint PATH it reads from. Left optional it was
|
|
98
|
+
forwarded verbatim, so the deployment's own default governed and a settlement past it came back as `[]`.
|
|
97
99
|
|
|
98
100
|
**Scoped to x402.** The manifest declares `protocol: "x402"` because the field, its 256-byte ceiling and
|
|
99
101
|
its enforcement are all x402's. A Canton Coin payment settled outside x402 does not get this carrier — and
|
package/dist/adapter.d.ts
CHANGED
|
@@ -66,8 +66,18 @@ export interface CantonX402Settlement {
|
|
|
66
66
|
export interface CantonX402Reader {
|
|
67
67
|
/** One settled transfer by ledger update id, or `null` if the participant has no such update. */
|
|
68
68
|
transferView(updateId: string): Promise<CantonX402TransferView | null>;
|
|
69
|
-
/**
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Update ids of transfers visible to `party`, most recent first. A participant view, not an index.
|
|
71
|
+
*
|
|
72
|
+
* ⛔ **`limit` IS REQUIRED, AND THAT IS THIS RAIL'S ANSWER TO THE SCAN QUESTION.** It used to be
|
|
73
|
+
* optional and forwarded verbatim, so an absent one handed the depth of the scan to whatever the
|
|
74
|
+
* deployment's endpoint defaults to — and a settlement past that default came back as an empty array,
|
|
75
|
+
* indistinguishable from "this party has none". This package will not invent the bound either: it does
|
|
76
|
+
* not even guess the PATH this call goes to ({@link CantonX402ReaderConfig.transfersPath}), so it has no
|
|
77
|
+
* standing to guess that endpoint's page size. The caller names the depth, and a full result is then
|
|
78
|
+
* exactly what the caller asked for rather than a truncation nobody chose.
|
|
79
|
+
*/
|
|
80
|
+
transfersFor(party: string, limit: number): Promise<string[]>;
|
|
71
81
|
}
|
|
72
82
|
/** The Canton x402 rail's surface. `propose` returns an `extra` fragment for the SELLER to merge into its
|
|
73
83
|
* `PaymentRequirements` — this rail's weld is committed by the seller and echoed by the payer, unlike the
|
|
@@ -86,8 +96,10 @@ export interface CantonX402Adapter {
|
|
|
86
96
|
recover(ref: CantonX402SettlementRef, reader: CantonX402Reader): Promise<Outcome<`0x${string}`>>;
|
|
87
97
|
/** Report the `settled` transition, with the asset the weld is attached to. */
|
|
88
98
|
observe(ref: CantonX402SettlementRef, reader: CantonX402Reader): Promise<Outcome<CantonX402Settlement>>;
|
|
89
|
-
/** Scan one party's visible transfers for `atrHash` — a participant view, never a global index.
|
|
90
|
-
|
|
99
|
+
/** Scan one party's visible transfers for `atrHash` — a participant view, never a global index.
|
|
100
|
+
* `limit` is the scan DEPTH and it is REQUIRED: see {@link CantonX402Reader.transfersFor} for why this
|
|
101
|
+
* package will not pick it, and why an absent one used to mean the endpoint picked instead. */
|
|
102
|
+
enumerate(atrHash: string, party: string, reader: CantonX402Reader, limit: number): Promise<CantonX402SettlementRef[]>;
|
|
91
103
|
}
|
|
92
104
|
/** Config for a live participant reader over a deployment's HTTP ledger surface. */
|
|
93
105
|
export interface CantonX402ReaderConfig {
|
|
@@ -118,7 +130,29 @@ export interface CantonX402ReaderConfig {
|
|
|
118
130
|
* it shipped as `/v1/updates/transfers`.
|
|
119
131
|
*/
|
|
120
132
|
transfersPath: string;
|
|
133
|
+
/**
|
|
134
|
+
* Per-request deadline in ms; defaults to {@link CANTON_X402_DEFAULT_TIMEOUT_MS}.
|
|
135
|
+
*
|
|
136
|
+
* ⛔⛔ **`fetch` HAS NO TIMEOUT OF ITS OWN, AND NOTHING HERE SUPPLIED ONE.** A participant that accepted
|
|
137
|
+
* the connection and never answered hung `recover`, `observe` and `enumerate` FOREVER — no error, no
|
|
138
|
+
* refusal, no return. On a surface whose whole contract is to hand back an `Outcome` that is worse than
|
|
139
|
+
* a failure: a caller can retry a refusal and cannot retry a promise that never settles. It bites hardest
|
|
140
|
+
* on `enumerate`, which reads every listed update in a loop, so one unanswered transfer stalls the scan
|
|
141
|
+
* of a party's entire history. The endpoint is a counterparty's own service — this package does not even
|
|
142
|
+
* guess its PATH, so it certainly cannot assume its liveness.
|
|
143
|
+
*/
|
|
144
|
+
timeoutMs?: number;
|
|
121
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* The default per-request deadline on a participant HTTP call — the same 10s `evidence`'s hardened
|
|
148
|
+
* resolver uses, because it is the same kind of budget: one round trip to a counterparty's endpoint, not
|
|
149
|
+
* a long-poll and not a stream.
|
|
150
|
+
*
|
|
151
|
+
* Deliberately generous rather than tight. A participant under load legitimately takes seconds to answer,
|
|
152
|
+
* and a deadline below what the endpoint needs turns its slow honest answers into transport faults — a
|
|
153
|
+
* different wrong answer, not a fix.
|
|
154
|
+
*/
|
|
155
|
+
export declare const CANTON_X402_DEFAULT_TIMEOUT_MS = 10000;
|
|
122
156
|
/**
|
|
123
157
|
* A live `CantonX402Reader` over a participant's HTTP ledger surface, PURE `fetch` — no Daml SDK.
|
|
124
158
|
*
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAIhF;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,kGAAkG;IAClG,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvC,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IACvE
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAIhF;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,kGAAkG;IAClG,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvC,yFAAyF;IACzF,QAAQ,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED,mFAAmF;AACnF,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,KAAK,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IACvE;;;;;;;;;;OAUG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC/D;AAED;;;6GAG6G;AAC7G,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,sGAAsG;IACtG,OAAO,CACL,GAAG,EAAE,uBAAuB,EAC5B,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC;IACnC,+EAA+E;IAC/E,OAAO,CACL,GAAG,EAAE,uBAAuB,EAC5B,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAC1C;;oGAEgG;IAChG,SAAS,CACP,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,gBAAgB,EACxB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAAC;CACvC;AAED,oFAAoF;AACpF,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;;OAeG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,QAAS,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,sBAAsB,GAC1B,gBAAgB,CAuElB;AAyBD;;;8GAG8G;AAC9G,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,eAAe,GACxB,iBAAiB,CAsHnB"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { atrHashEquals, isAtrHash } from "@integraledger/lcp-kernel";
|
|
2
2
|
import { readTransferMemoAtrHash, x402MemoRequirement } from "./memo.js";
|
|
3
|
+
/**
|
|
4
|
+
* The default per-request deadline on a participant HTTP call — the same 10s `evidence`'s hardened
|
|
5
|
+
* resolver uses, because it is the same kind of budget: one round trip to a counterparty's endpoint, not
|
|
6
|
+
* a long-poll and not a stream.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately generous rather than tight. A participant under load legitimately takes seconds to answer,
|
|
9
|
+
* and a deadline below what the endpoint needs turns its slow honest answers into transport faults — a
|
|
10
|
+
* different wrong answer, not a fix.
|
|
11
|
+
*/
|
|
12
|
+
export const CANTON_X402_DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
13
|
/**
|
|
4
14
|
* A live `CantonX402Reader` over a participant's HTTP ledger surface, PURE `fetch` — no Daml SDK.
|
|
5
15
|
*
|
|
@@ -24,6 +34,7 @@ export function makeCantonX402Reader(cfg) {
|
|
|
24
34
|
throw new Error("makeCantonX402Reader: transferPath is empty — this package does not guess your participant's update endpoint");
|
|
25
35
|
if (cfg.transfersPath.length === 0)
|
|
26
36
|
throw new Error("makeCantonX402Reader: transfersPath is empty — this package does not guess your participant's update endpoint");
|
|
37
|
+
const timeoutMs = cfg.timeoutMs ?? CANTON_X402_DEFAULT_TIMEOUT_MS;
|
|
27
38
|
async function ledgerCall(path, body) {
|
|
28
39
|
const res = await fetch(`${cfg.jsonLedgerUrl}${path}`, {
|
|
29
40
|
method: "POST",
|
|
@@ -32,6 +43,10 @@ export function makeCantonX402Reader(cfg) {
|
|
|
32
43
|
authorization: `Bearer ${cfg.bearerJwt}`,
|
|
33
44
|
},
|
|
34
45
|
body: JSON.stringify(body),
|
|
46
|
+
// Without this the call never comes back on a participant that accepts and does not answer — see
|
|
47
|
+
// CantonX402ReaderConfig.timeoutMs. `AbortSignal.timeout` rejects with a `TimeoutError`, a throw out
|
|
48
|
+
// of the reader port and therefore a LOUD failure, as every other transport fault here is.
|
|
49
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
35
50
|
});
|
|
36
51
|
if (!res.ok) {
|
|
37
52
|
const text = await res.text().catch(() => "");
|
|
@@ -50,13 +65,44 @@ export function makeCantonX402Reader(cfg) {
|
|
|
50
65
|
return result ?? null;
|
|
51
66
|
},
|
|
52
67
|
async transfersFor(party, limit) {
|
|
53
|
-
|
|
68
|
+
const result = await ledgerCall(cfg.transfersPath, {
|
|
54
69
|
party,
|
|
55
|
-
|
|
70
|
+
limit,
|
|
56
71
|
});
|
|
72
|
+
// ⛔⛔ **A `result: null` PASSED THE ENVELOPE CHECK AND CAME BACK AS AN ARRAY.** `ledgerCall` refuses
|
|
73
|
+
// an ABSENT `result` and `null` is present, so this returned `null` under a declared `string[]` and
|
|
74
|
+
// `enumerate`'s `for…of` threw `TypeError: updateIds is not iterable` — an exception out of a
|
|
75
|
+
// refuse-don't-throw surface, raised by the shape of a counterparty's response. The array is the
|
|
76
|
+
// check rather than not-null, and so is the ELEMENT type: a list carrying a `null` id would be
|
|
77
|
+
// POSTed straight back as `{ updateId: null }` and read as a transfer that is merely absent.
|
|
78
|
+
if (!Array.isArray(result) || result.some((id) => typeof id !== "string"))
|
|
79
|
+
throw new Error(`Daml ${cfg.transfersPath} returned a result that is not an array of update ids — this endpoint is not answering the transfer-listing shape this reader was pointed at`);
|
|
80
|
+
return result;
|
|
57
81
|
},
|
|
58
82
|
};
|
|
59
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* The asset fields a transfer view must actually carry, named — empty when the view is complete.
|
|
86
|
+
*
|
|
87
|
+
* Non-empty STRINGS, not merely present values: `receiver: ""` names no party and `amount: ""` is no
|
|
88
|
+
* quantity, and a reader that fills its required fields with blanks has answered the type checker rather
|
|
89
|
+
* than the question. `instrumentId` is checked through both of its own halves for the same reason — a
|
|
90
|
+
* `{ admin: "", id: "" }` identifies no instrument.
|
|
91
|
+
*/
|
|
92
|
+
function missingAssetFields(view) {
|
|
93
|
+
const text = (v) => typeof v === "string" && v.length > 0;
|
|
94
|
+
const out = [];
|
|
95
|
+
if (!text(view.receiver))
|
|
96
|
+
out.push("receiver");
|
|
97
|
+
if (!text(view.amount))
|
|
98
|
+
out.push("amount");
|
|
99
|
+
if (view.instrumentId === null ||
|
|
100
|
+
typeof view.instrumentId !== "object" ||
|
|
101
|
+
!text(view.instrumentId.admin) ||
|
|
102
|
+
!text(view.instrumentId.id))
|
|
103
|
+
out.push("instrumentId");
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
60
106
|
/** Construct the Canton x402 adapter. **The manifest is injected, not baked in** — pass this package's own
|
|
61
107
|
* `CANTON_X402_MANIFEST`; a manifest whose `rail` is not `"canton:x402"` throws, because an adapter over
|
|
62
108
|
* another rail's manifest would publish that rail's claims as its own. Nothing has to be deployed first:
|
|
@@ -88,6 +134,26 @@ export function createCantonX402Adapter(manifest) {
|
|
|
88
134
|
code: "canton/no-lcp-memo",
|
|
89
135
|
detail: `the transfer at updateId ${ref.updateId} carries no well-formed atrHash under x402.memo`,
|
|
90
136
|
};
|
|
137
|
+
// ⛔⛔ **THE MEMO WAS CHECKED AND THE ASSET WAS NOT, UNDER TYPES THAT SAID BOTH WERE THERE.**
|
|
138
|
+
// `CantonX402TransferView` declares `receiver`, `amount` and `instrumentId` as REQUIRED, but the view
|
|
139
|
+
// comes off a counterparty's HTTP endpoint through `JSON.parse` — a shape no type checks at runtime —
|
|
140
|
+
// so a reader that omitted them handed back `undefined` and this function copied it into a
|
|
141
|
+
// `CantonX402Settlement` whose own types promise strings. The result is a settled verdict carrying
|
|
142
|
+
// `receiver: undefined`, which a consumer then compares against the merchant it expected and finds
|
|
143
|
+
// unequal, or renders, or writes into a record.
|
|
144
|
+
//
|
|
145
|
+
// ⭐ And it is what makes `assetBinding: "carried"` in this manifest a TRUE claim rather than a hopeful
|
|
146
|
+
// one. That axis says a consumer can reach the asset the weld is attached to. Two lines above, an
|
|
147
|
+
// absent memo refuses loudly by name; the asset fields are the other half of the same promise and were
|
|
148
|
+
// not checked at all. One shape of missing evidence was a refusal and the other was a success.
|
|
149
|
+
const missing = missingAssetFields(view);
|
|
150
|
+
if (missing.length > 0)
|
|
151
|
+
return {
|
|
152
|
+
refused: true,
|
|
153
|
+
haltClass: "verification-failure",
|
|
154
|
+
code: "canton/incomplete-transfer-view",
|
|
155
|
+
detail: `the transfer at updateId ${ref.updateId} carries a well-formed atrHash but no ${missing.join(", ")} — this manifest declares assetBinding "carried", so a settlement whose asset cannot be read is not one this rail can report`,
|
|
156
|
+
};
|
|
91
157
|
return {
|
|
92
158
|
ok: true,
|
|
93
159
|
value: {
|
|
@@ -118,6 +184,10 @@ export function createCantonX402Adapter(manifest) {
|
|
|
118
184
|
// it would produce is indistinguishable from "this party has no settlements".
|
|
119
185
|
if (!isAtrHash(atrHash))
|
|
120
186
|
throw new Error(`enumerate: atrHash must be a 0x-prefixed 32-byte value, got "${atrHash}"`);
|
|
187
|
+
// The same reasoning as the atrHash above, applied to the bound: a scan of zero (or of half a
|
|
188
|
+
// transfer) returns [], which is the answer that cannot be told apart from "no settlements".
|
|
189
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
190
|
+
throw new Error(`enumerate: limit must be a positive integer — it is the scan DEPTH and this package will not choose it, got ${limit}`);
|
|
121
191
|
const updateIds = await reader.transfersFor(party, limit);
|
|
122
192
|
const out = [];
|
|
123
193
|
for (const updateId of updateIds) {
|
package/dist/adapter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { type CantonX402Adapter, type CantonX402Reader, type CantonX402ReaderConfig, type CantonX402Settlement, type CantonX402SettlementRef, type CantonX402TransferView, createCantonX402Adapter, makeCantonX402Reader, } from "./adapter.js";
|
|
2
|
-
export { CANTON_X402_MEMO_KEY, CANTON_X402_MEMO_MAX_BYTES, type CantonX402Network, type CantonX402NetworkConfig, getCantonX402Config, } from "./constants.js";
|
|
1
|
+
export { CANTON_X402_DEFAULT_TIMEOUT_MS, type CantonX402Adapter, type CantonX402Reader, type CantonX402ReaderConfig, type CantonX402Settlement, type CantonX402SettlementRef, type CantonX402TransferView, createCantonX402Adapter, makeCantonX402Reader, } from "./adapter.js";
|
|
2
|
+
export { CANTON_X402_COLLECTION_PATH, CANTON_X402_MEMO_KEY, CANTON_X402_MEMO_MAX_BYTES, type CantonX402Network, type CantonX402NetworkConfig, getCantonX402Config, } from "./constants.js";
|
|
3
3
|
export { CANTON_X402_MANIFEST } from "./manifest.js";
|
|
4
4
|
export { decodeTransferMemo, encodeTransferMemo, readTransferMemoAtrHash, x402MemoRequirement, } from "./memo.js";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAC9B,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { createCantonX402Adapter, makeCantonX402Reader, } from "./adapter.js";
|
|
2
|
-
export { CANTON_X402_MEMO_KEY, CANTON_X402_MEMO_MAX_BYTES, getCantonX402Config, } from "./constants.js";
|
|
1
|
+
export { CANTON_X402_DEFAULT_TIMEOUT_MS, createCantonX402Adapter, makeCantonX402Reader, } from "./adapter.js";
|
|
2
|
+
export { CANTON_X402_COLLECTION_PATH, CANTON_X402_MEMO_KEY, CANTON_X402_MEMO_MAX_BYTES, getCantonX402Config, } from "./constants.js";
|
|
3
3
|
export { CANTON_X402_MANIFEST } from "./manifest.js";
|
|
4
4
|
export { decodeTransferMemo, encodeTransferMemo, readTransferMemoAtrHash, x402MemoRequirement, } from "./memo.js";
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,8BAA8B,EAO9B,uBAAuB,EACvB,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAG1B,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,GACpB,MAAM,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@integraledger/lcp-binding-canton-x402",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Welds an ATR hash into a Canton Coin settlement over x402's exact scheme.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lcp",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://github.com/IntegraLedger/integra-protocol/tree/main/packages/binding-canton-x402#readme",
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@integraledger/lcp-binding-core": "0.
|
|
47
|
-
"@integraledger/lcp-kernel": "0.
|
|
46
|
+
"@integraledger/lcp-binding-core": "0.18.1",
|
|
47
|
+
"@integraledger/lcp-kernel": "0.18.1"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "24.13.3",
|
package/src/adapter.ts
CHANGED
|
@@ -66,8 +66,18 @@ export interface CantonX402Settlement {
|
|
|
66
66
|
export interface CantonX402Reader {
|
|
67
67
|
/** One settled transfer by ledger update id, or `null` if the participant has no such update. */
|
|
68
68
|
transferView(updateId: string): Promise<CantonX402TransferView | null>;
|
|
69
|
-
/**
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Update ids of transfers visible to `party`, most recent first. A participant view, not an index.
|
|
71
|
+
*
|
|
72
|
+
* ⛔ **`limit` IS REQUIRED, AND THAT IS THIS RAIL'S ANSWER TO THE SCAN QUESTION.** It used to be
|
|
73
|
+
* optional and forwarded verbatim, so an absent one handed the depth of the scan to whatever the
|
|
74
|
+
* deployment's endpoint defaults to — and a settlement past that default came back as an empty array,
|
|
75
|
+
* indistinguishable from "this party has none". This package will not invent the bound either: it does
|
|
76
|
+
* not even guess the PATH this call goes to ({@link CantonX402ReaderConfig.transfersPath}), so it has no
|
|
77
|
+
* standing to guess that endpoint's page size. The caller names the depth, and a full result is then
|
|
78
|
+
* exactly what the caller asked for rather than a truncation nobody chose.
|
|
79
|
+
*/
|
|
80
|
+
transfersFor(party: string, limit: number): Promise<string[]>;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
/** The Canton x402 rail's surface. `propose` returns an `extra` fragment for the SELLER to merge into its
|
|
@@ -91,12 +101,14 @@ export interface CantonX402Adapter {
|
|
|
91
101
|
ref: CantonX402SettlementRef,
|
|
92
102
|
reader: CantonX402Reader,
|
|
93
103
|
): Promise<Outcome<CantonX402Settlement>>;
|
|
94
|
-
/** Scan one party's visible transfers for `atrHash` — a participant view, never a global index.
|
|
104
|
+
/** Scan one party's visible transfers for `atrHash` — a participant view, never a global index.
|
|
105
|
+
* `limit` is the scan DEPTH and it is REQUIRED: see {@link CantonX402Reader.transfersFor} for why this
|
|
106
|
+
* package will not pick it, and why an absent one used to mean the endpoint picked instead. */
|
|
95
107
|
enumerate(
|
|
96
108
|
atrHash: string,
|
|
97
109
|
party: string,
|
|
98
110
|
reader: CantonX402Reader,
|
|
99
|
-
limit
|
|
111
|
+
limit: number,
|
|
100
112
|
): Promise<CantonX402SettlementRef[]>;
|
|
101
113
|
}
|
|
102
114
|
|
|
@@ -129,8 +141,31 @@ export interface CantonX402ReaderConfig {
|
|
|
129
141
|
* it shipped as `/v1/updates/transfers`.
|
|
130
142
|
*/
|
|
131
143
|
transfersPath: string;
|
|
144
|
+
/**
|
|
145
|
+
* Per-request deadline in ms; defaults to {@link CANTON_X402_DEFAULT_TIMEOUT_MS}.
|
|
146
|
+
*
|
|
147
|
+
* ⛔⛔ **`fetch` HAS NO TIMEOUT OF ITS OWN, AND NOTHING HERE SUPPLIED ONE.** A participant that accepted
|
|
148
|
+
* the connection and never answered hung `recover`, `observe` and `enumerate` FOREVER — no error, no
|
|
149
|
+
* refusal, no return. On a surface whose whole contract is to hand back an `Outcome` that is worse than
|
|
150
|
+
* a failure: a caller can retry a refusal and cannot retry a promise that never settles. It bites hardest
|
|
151
|
+
* on `enumerate`, which reads every listed update in a loop, so one unanswered transfer stalls the scan
|
|
152
|
+
* of a party's entire history. The endpoint is a counterparty's own service — this package does not even
|
|
153
|
+
* guess its PATH, so it certainly cannot assume its liveness.
|
|
154
|
+
*/
|
|
155
|
+
timeoutMs?: number;
|
|
132
156
|
}
|
|
133
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The default per-request deadline on a participant HTTP call — the same 10s `evidence`'s hardened
|
|
160
|
+
* resolver uses, because it is the same kind of budget: one round trip to a counterparty's endpoint, not
|
|
161
|
+
* a long-poll and not a stream.
|
|
162
|
+
*
|
|
163
|
+
* Deliberately generous rather than tight. A participant under load legitimately takes seconds to answer,
|
|
164
|
+
* and a deadline below what the endpoint needs turns its slow honest answers into transport faults — a
|
|
165
|
+
* different wrong answer, not a fix.
|
|
166
|
+
*/
|
|
167
|
+
export const CANTON_X402_DEFAULT_TIMEOUT_MS = 10_000;
|
|
168
|
+
|
|
134
169
|
/**
|
|
135
170
|
* A live `CantonX402Reader` over a participant's HTTP ledger surface, PURE `fetch` — no Daml SDK.
|
|
136
171
|
*
|
|
@@ -162,6 +197,8 @@ export function makeCantonX402Reader(
|
|
|
162
197
|
"makeCantonX402Reader: transfersPath is empty — this package does not guess your participant's update endpoint",
|
|
163
198
|
);
|
|
164
199
|
|
|
200
|
+
const timeoutMs = cfg.timeoutMs ?? CANTON_X402_DEFAULT_TIMEOUT_MS;
|
|
201
|
+
|
|
165
202
|
async function ledgerCall<T>(path: string, body: unknown): Promise<T> {
|
|
166
203
|
const res = await fetch(`${cfg.jsonLedgerUrl}${path}`, {
|
|
167
204
|
method: "POST",
|
|
@@ -170,6 +207,10 @@ export function makeCantonX402Reader(
|
|
|
170
207
|
authorization: `Bearer ${cfg.bearerJwt}`,
|
|
171
208
|
},
|
|
172
209
|
body: JSON.stringify(body),
|
|
210
|
+
// Without this the call never comes back on a participant that accepts and does not answer — see
|
|
211
|
+
// CantonX402ReaderConfig.timeoutMs. `AbortSignal.timeout` rejects with a `TimeoutError`, a throw out
|
|
212
|
+
// of the reader port and therefore a LOUD failure, as every other transport fault here is.
|
|
213
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
173
214
|
});
|
|
174
215
|
if (!res.ok) {
|
|
175
216
|
const text = await res.text().catch(() => "");
|
|
@@ -193,15 +234,49 @@ export function makeCantonX402Reader(
|
|
|
193
234
|
);
|
|
194
235
|
return result ?? null;
|
|
195
236
|
},
|
|
196
|
-
async transfersFor(party: string, limit
|
|
197
|
-
|
|
237
|
+
async transfersFor(party: string, limit: number): Promise<string[]> {
|
|
238
|
+
const result = await ledgerCall<unknown>(cfg.transfersPath, {
|
|
198
239
|
party,
|
|
199
|
-
|
|
240
|
+
limit,
|
|
200
241
|
});
|
|
242
|
+
// ⛔⛔ **A `result: null` PASSED THE ENVELOPE CHECK AND CAME BACK AS AN ARRAY.** `ledgerCall` refuses
|
|
243
|
+
// an ABSENT `result` and `null` is present, so this returned `null` under a declared `string[]` and
|
|
244
|
+
// `enumerate`'s `for…of` threw `TypeError: updateIds is not iterable` — an exception out of a
|
|
245
|
+
// refuse-don't-throw surface, raised by the shape of a counterparty's response. The array is the
|
|
246
|
+
// check rather than not-null, and so is the ELEMENT type: a list carrying a `null` id would be
|
|
247
|
+
// POSTed straight back as `{ updateId: null }` and read as a transfer that is merely absent.
|
|
248
|
+
if (!Array.isArray(result) || result.some((id) => typeof id !== "string"))
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Daml ${cfg.transfersPath} returned a result that is not an array of update ids — this endpoint is not answering the transfer-listing shape this reader was pointed at`,
|
|
251
|
+
);
|
|
252
|
+
return result as string[];
|
|
201
253
|
},
|
|
202
254
|
};
|
|
203
255
|
}
|
|
204
256
|
|
|
257
|
+
/**
|
|
258
|
+
* The asset fields a transfer view must actually carry, named — empty when the view is complete.
|
|
259
|
+
*
|
|
260
|
+
* Non-empty STRINGS, not merely present values: `receiver: ""` names no party and `amount: ""` is no
|
|
261
|
+
* quantity, and a reader that fills its required fields with blanks has answered the type checker rather
|
|
262
|
+
* than the question. `instrumentId` is checked through both of its own halves for the same reason — a
|
|
263
|
+
* `{ admin: "", id: "" }` identifies no instrument.
|
|
264
|
+
*/
|
|
265
|
+
function missingAssetFields(view: CantonX402TransferView): string[] {
|
|
266
|
+
const text = (v: unknown): boolean => typeof v === "string" && v.length > 0;
|
|
267
|
+
const out: string[] = [];
|
|
268
|
+
if (!text(view.receiver)) out.push("receiver");
|
|
269
|
+
if (!text(view.amount)) out.push("amount");
|
|
270
|
+
if (
|
|
271
|
+
view.instrumentId === null ||
|
|
272
|
+
typeof view.instrumentId !== "object" ||
|
|
273
|
+
!text(view.instrumentId.admin) ||
|
|
274
|
+
!text(view.instrumentId.id)
|
|
275
|
+
)
|
|
276
|
+
out.push("instrumentId");
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
205
280
|
/** Construct the Canton x402 adapter. **The manifest is injected, not baked in** — pass this package's own
|
|
206
281
|
* `CANTON_X402_MANIFEST`; a manifest whose `rail` is not `"canton:x402"` throws, because an adapter over
|
|
207
282
|
* another rail's manifest would publish that rail's claims as its own. Nothing has to be deployed first:
|
|
@@ -241,6 +316,26 @@ export function createCantonX402Adapter(
|
|
|
241
316
|
code: "canton/no-lcp-memo",
|
|
242
317
|
detail: `the transfer at updateId ${ref.updateId} carries no well-formed atrHash under x402.memo`,
|
|
243
318
|
};
|
|
319
|
+
// ⛔⛔ **THE MEMO WAS CHECKED AND THE ASSET WAS NOT, UNDER TYPES THAT SAID BOTH WERE THERE.**
|
|
320
|
+
// `CantonX402TransferView` declares `receiver`, `amount` and `instrumentId` as REQUIRED, but the view
|
|
321
|
+
// comes off a counterparty's HTTP endpoint through `JSON.parse` — a shape no type checks at runtime —
|
|
322
|
+
// so a reader that omitted them handed back `undefined` and this function copied it into a
|
|
323
|
+
// `CantonX402Settlement` whose own types promise strings. The result is a settled verdict carrying
|
|
324
|
+
// `receiver: undefined`, which a consumer then compares against the merchant it expected and finds
|
|
325
|
+
// unequal, or renders, or writes into a record.
|
|
326
|
+
//
|
|
327
|
+
// ⭐ And it is what makes `assetBinding: "carried"` in this manifest a TRUE claim rather than a hopeful
|
|
328
|
+
// one. That axis says a consumer can reach the asset the weld is attached to. Two lines above, an
|
|
329
|
+
// absent memo refuses loudly by name; the asset fields are the other half of the same promise and were
|
|
330
|
+
// not checked at all. One shape of missing evidence was a refusal and the other was a success.
|
|
331
|
+
const missing = missingAssetFields(view);
|
|
332
|
+
if (missing.length > 0)
|
|
333
|
+
return {
|
|
334
|
+
refused: true,
|
|
335
|
+
haltClass: "verification-failure",
|
|
336
|
+
code: "canton/incomplete-transfer-view",
|
|
337
|
+
detail: `the transfer at updateId ${ref.updateId} carries a well-formed atrHash but no ${missing.join(", ")} — this manifest declares assetBinding "carried", so a settlement whose asset cannot be read is not one this rail can report`,
|
|
338
|
+
};
|
|
244
339
|
return {
|
|
245
340
|
ok: true,
|
|
246
341
|
value: {
|
|
@@ -281,7 +376,7 @@ export function createCantonX402Adapter(
|
|
|
281
376
|
atrHash: string,
|
|
282
377
|
party: string,
|
|
283
378
|
reader: CantonX402Reader,
|
|
284
|
-
limit
|
|
379
|
+
limit: number,
|
|
285
380
|
): Promise<CantonX402SettlementRef[]> {
|
|
286
381
|
// Fail-fast, like propose: a malformed atrHash can never match a decoded memo, and the silent []
|
|
287
382
|
// it would produce is indistinguishable from "this party has no settlements".
|
|
@@ -289,6 +384,12 @@ export function createCantonX402Adapter(
|
|
|
289
384
|
throw new Error(
|
|
290
385
|
`enumerate: atrHash must be a 0x-prefixed 32-byte value, got "${atrHash}"`,
|
|
291
386
|
);
|
|
387
|
+
// The same reasoning as the atrHash above, applied to the bound: a scan of zero (or of half a
|
|
388
|
+
// transfer) returns [], which is the answer that cannot be told apart from "no settlements".
|
|
389
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
390
|
+
throw new Error(
|
|
391
|
+
`enumerate: limit must be a positive integer — it is the scan DEPTH and this package will not choose it, got ${limit}`,
|
|
392
|
+
);
|
|
292
393
|
const updateIds = await reader.transfersFor(party, limit);
|
|
293
394
|
const out: CantonX402SettlementRef[] = [];
|
|
294
395
|
for (const updateId of updateIds) {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export {
|
|
2
|
+
CANTON_X402_DEFAULT_TIMEOUT_MS,
|
|
2
3
|
type CantonX402Adapter,
|
|
3
4
|
type CantonX402Reader,
|
|
4
5
|
type CantonX402ReaderConfig,
|
|
@@ -9,6 +10,7 @@ export {
|
|
|
9
10
|
makeCantonX402Reader,
|
|
10
11
|
} from "./adapter.js";
|
|
11
12
|
export {
|
|
13
|
+
CANTON_X402_COLLECTION_PATH,
|
|
12
14
|
CANTON_X402_MEMO_KEY,
|
|
13
15
|
CANTON_X402_MEMO_MAX_BYTES,
|
|
14
16
|
type CantonX402Network,
|