@caypo/mpp-canton 0.1.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.
@@ -0,0 +1,40 @@
1
+
2
+ > @caypo/mpp-canton@0.1.0 build /Users/anil/Desktop/caypo/packages/mpp
3
+ > tsup
4
+
5
+ CLI Building entry: src/client.ts, src/index.ts, src/server.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /Users/anil/Desktop/caypo/packages/mpp/tsup.config.ts
9
+ CLI Target: es2022
10
+ CLI Cleaning output folder
11
+ ESM Build start
12
+ CJS Build start
13
+ ESM dist/index.js 497.00 B
14
+ ESM dist/chunk-NTWNP6H5.js 1.14 KB
15
+ ESM dist/server.js 193.00 B
16
+ ESM dist/chunk-757U7PM3.js 4.17 KB
17
+ ESM dist/client.js 145.00 B
18
+ ESM dist/chunk-5CWLHTUR.js 3.31 KB
19
+ ESM dist/index.js.map 893.00 B
20
+ ESM dist/chunk-NTWNP6H5.js.map 2.76 KB
21
+ ESM dist/client.js.map 71.00 B
22
+ ESM dist/chunk-757U7PM3.js.map 8.67 KB
23
+ ESM dist/server.js.map 71.00 B
24
+ ESM dist/chunk-5CWLHTUR.js.map 7.01 KB
25
+ ESM ⚡️ Build success in 10ms
26
+ CJS dist/server.cjs 5.57 KB
27
+ CJS dist/index.cjs 10.04 KB
28
+ CJS dist/client.cjs 6.38 KB
29
+ CJS dist/server.cjs.map 9.75 KB
30
+ CJS dist/index.cjs.map 19.18 KB
31
+ CJS dist/client.cjs.map 11.40 KB
32
+ CJS ⚡️ Build success in 10ms
33
+ DTS Build start
34
+ DTS ⚡️ Build success in 997ms
35
+ DTS dist/index.d.ts 2.60 KB
36
+ DTS dist/client.d.ts 839.00 B
37
+ DTS dist/server.d.ts 971.00 B
38
+ DTS dist/index.d.cts 2.60 KB
39
+ DTS dist/client.d.cts 839.00 B
40
+ DTS dist/server.d.cts 971.00 B
@@ -0,0 +1,16 @@
1
+
2
+ > @caypo/mpp-canton@0.1.0 test /Users/anil/Desktop/caypo/packages/mpp
3
+ > vitest run
4
+
5
+
6
+ RUN v3.2.4 /Users/anil/Desktop/caypo/packages/mpp
7
+
8
+ ✓ src/__tests__/method.test.ts (19 tests) 5ms
9
+ ✓ src/__tests__/client.test.ts (6 tests) 18ms
10
+ ✓ src/__tests__/server.test.ts (10 tests) 18ms
11
+
12
+ Test Files 3 passed (3)
13
+ Tests 35 passed (35)
14
+ Start at 19:11:45
15
+ Duration 296ms (transform 89ms, setup 0ms, collect 158ms, tests 41ms, environment 0ms, prepare 141ms)
16
+
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @caypo/mpp-canton
2
+
3
+ **Canton Network payment method for the [Machine Payments Protocol (MPP)](https://mpp.dev)**
4
+
5
+ Accept and make USDCx payments in any HTTP API using Canton's CIP-56 token standard with 1-step TransferPreapproval transfers.
6
+
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0%20%2F%20MIT-blue)](../../LICENSE-APACHE)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install @caypo/mpp-canton
13
+ ```
14
+
15
+ ## Accept Payments (Server)
16
+
17
+ ```typescript
18
+ import { cantonServer } from "@caypo/mpp-canton/server";
19
+
20
+ const server = cantonServer({
21
+ ledgerUrl: "http://localhost:7575",
22
+ token: process.env.CANTON_JWT,
23
+ userId: "ledger-api-user",
24
+ recipientPartyId: "Gateway::1220...",
25
+ network: "mainnet",
26
+ });
27
+
28
+ // When agent submits credential after paying:
29
+ const receipt = await server.verify({ credential });
30
+ // { method: "canton", reference: "upd-abc123", status: "success" }
31
+ ```
32
+
33
+ ## Make Payments (Client)
34
+
35
+ ```typescript
36
+ import { cantonClient } from "@caypo/mpp-canton/client";
37
+
38
+ const client = cantonClient({
39
+ ledgerUrl: "http://localhost:7575",
40
+ token: process.env.CANTON_JWT,
41
+ userId: "ledger-api-user",
42
+ partyId: "Agent::1220...",
43
+ network: "mainnet",
44
+ });
45
+
46
+ // When receiving a 402 challenge:
47
+ const credential = await client.createCredential({ challenge });
48
+ // Base64-encoded credential with updateId, completionOffset, sender, commandId
49
+ ```
50
+
51
+ ## Method Definition
52
+
53
+ ```typescript
54
+ import { cantonMethod } from "@caypo/mpp-canton";
55
+
56
+ cantonMethod.name; // "canton"
57
+ cantonMethod.intent; // "charge"
58
+ ```
59
+
60
+ ## Schemas
61
+
62
+ ```typescript
63
+ import { requestSchema, credentialPayloadSchema } from "@caypo/mpp-canton";
64
+
65
+ // Validate payment requests
66
+ requestSchema.parse({ amount: "0.01", currency: "USDCx", recipient: "...", network: "mainnet" });
67
+
68
+ // Validate credentials
69
+ credentialPayloadSchema.parse({ updateId: "...", completionOffset: 42, sender: "...", commandId: "..." });
70
+ ```
71
+
72
+ ## Error Handling
73
+
74
+ ```typescript
75
+ import { MppVerificationError } from "@caypo/mpp-canton";
76
+
77
+ try {
78
+ await server.verify({ credential });
79
+ } catch (err) {
80
+ if (err instanceof MppVerificationError) {
81
+ console.log(err.problemCode); // "verification-failed" | "payment-insufficient"
82
+ }
83
+ }
84
+ ```
85
+
86
+ ## How It Works
87
+
88
+ The Canton MPP method uses **CIP-56 TransferPreapproval** for 1-step transfers:
89
+
90
+ 1. Service returns `402` with `WWW-Authenticate: Payment method="canton", amount="0.01", recipient="...", network="..."`
91
+ 2. Agent exercises `TransferFactory_Transfer` on Canton (requires recipient's TransferPreapproval)
92
+ 3. Agent builds credential with `{ updateId, completionOffset, sender, commandId }`
93
+ 4. Service verifies by fetching the transaction from Canton ledger
94
+ 5. Service confirms: Holding created for recipient, amount >= required, correct sender
95
+
96
+ ## Links
97
+
98
+ - [MPP Protocol](https://mpp.dev) — Machine Payments Protocol by Stripe and Tempo
99
+ - [Canton Network](https://canton.network) — Privacy-enabled institutional blockchain
100
+ - [CIP-56](https://canton.network) — Canton token standard
101
+
102
+ ## License
103
+
104
+ Apache-2.0 OR MIT
package/SPEC.md ADDED
@@ -0,0 +1,269 @@
1
+ # @cayvox/mpp-canton — MPP Payment Method Specification (Production)
2
+
3
+ ## Transfer Flow for MPP
4
+
5
+ **The critical insight:** The MPP gateway server MUST have a `TransferPreapproval` contract active. This enables 1-step transfers from agents — no receiver acceptance needed.
6
+
7
+ ```
8
+ Agent Gateway (Canton) Canton Ledger
9
+ | | |
10
+ |-- GET /api/data ------------->| |
11
+ |<-- 402 + Challenge -----------| |
12
+ | | |
13
+ | (Agent exercises TransferFactory_Transfer |
14
+ | using gateway's TransferPreapproval) |
15
+ | | |
16
+ |-- POST /v2/commands/submit-and-wait -------------------------------->|
17
+ |<-- { updateId, completionOffset } ----------------------------------|
18
+ | | |
19
+ |-- GET /api/data + credential->| |
20
+ | |-- verify via /v2/updates/... ----->|
21
+ | |<-- transaction tree ---------------|
22
+ |<-- 200 + Receipt + data ------| |
23
+ ```
24
+
25
+ ## Method Definition
26
+
27
+ ```typescript
28
+ import { Method, z } from "mppx";
29
+
30
+ export const cantonMethod = Method.from({
31
+ intent: "charge",
32
+ name: "canton",
33
+ schema: {
34
+ credential: {
35
+ payload: z.object({
36
+ updateId: z.string().min(1),
37
+ completionOffset: z.number().int(),
38
+ sender: z.string().min(1), // e.g., "Agent::1220abcd..."
39
+ commandId: z.string().min(1),
40
+ }),
41
+ },
42
+ request: z.object({
43
+ amount: z.string().regex(/^\d+\.?\d{0,10}$/),
44
+ currency: z.enum(["USDCx", "CC"]),
45
+ recipient: z.string().min(1), // e.g., "Gateway::1220efgh..."
46
+ network: z.enum(["mainnet", "testnet", "devnet"]),
47
+ description: z.string().optional(),
48
+ expiry: z.number().int().min(1).max(3600).default(300),
49
+ }),
50
+ },
51
+ });
52
+ ```
53
+
54
+ Note: Canton uses `Numeric 10` (10 decimal places) internally. USDCx has 6 meaningful decimals but the on-chain representation may use 10.
55
+
56
+ ## Client Implementation
57
+
58
+ ```typescript
59
+ import { Method, Credential } from "mppx";
60
+ import { cantonMethod } from "./method";
61
+
62
+ export interface CantonClientConfig {
63
+ ledgerUrl: string; // e.g., "http://localhost:7575"
64
+ token: string; // JWT bearer token
65
+ userId: string; // Ledger API user ID
66
+ partyId: string; // Agent's party ID (e.g., "Agent::1220...")
67
+ network: "mainnet" | "testnet" | "devnet";
68
+ }
69
+
70
+ export function cantonClient(config: CantonClientConfig) {
71
+ return Method.toClient(cantonMethod, {
72
+ async createCredential({ challenge }) {
73
+ // 1. Validate network
74
+ if (challenge.request.network !== config.network) {
75
+ throw new Error(`Network mismatch: expected ${config.network}`);
76
+ }
77
+
78
+ // 2. Query agent's USDCx holdings
79
+ const holdingsResponse = await fetch(
80
+ `${config.ledgerUrl}/v2/state/active-contracts`,
81
+ {
82
+ method: "POST",
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ "Authorization": `Bearer ${config.token}`,
86
+ },
87
+ body: JSON.stringify({
88
+ eventFormat: {
89
+ filtersByParty: {
90
+ [config.partyId]: {
91
+ cumulative: [{
92
+ identifierFilter: {
93
+ TemplateFilter: {
94
+ value: { templateId: USDCX_HOLDING_TEMPLATE_ID }
95
+ }
96
+ }
97
+ }]
98
+ }
99
+ },
100
+ verbose: true
101
+ },
102
+ activeAtOffset: await getLedgerEnd(config),
103
+ }),
104
+ }
105
+ );
106
+
107
+ // 3. Select holdings covering the amount
108
+ const holdings = parseHoldings(await holdingsResponse.json());
109
+ const selected = selectHoldings(holdings, challenge.request.amount);
110
+
111
+ // 4. Generate unique commandId
112
+ const commandId = crypto.randomUUID();
113
+
114
+ // 5. Exercise TransferFactory_Transfer via submit-and-wait
115
+ const submitResponse = await fetch(
116
+ `${config.ledgerUrl}/v2/commands/submit-and-wait`,
117
+ {
118
+ method: "POST",
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ "Authorization": `Bearer ${config.token}`,
122
+ },
123
+ body: JSON.stringify({
124
+ commands: [{
125
+ ExerciseCommand: {
126
+ templateId: TRANSFER_FACTORY_TEMPLATE_ID,
127
+ contractId: selected.transferFactoryContractId,
128
+ choice: "TransferFactory_Transfer",
129
+ choiceArgument: {
130
+ sender: config.partyId,
131
+ receiver: challenge.request.recipient,
132
+ amount: challenge.request.amount,
133
+ instrumentId: USDCX_INSTRUMENT_ID,
134
+ inputHoldingCids: selected.holdingContractIds,
135
+ meta: {},
136
+ },
137
+ },
138
+ }],
139
+ userId: config.userId,
140
+ commandId,
141
+ actAs: [config.partyId],
142
+ readAs: [config.partyId],
143
+ }),
144
+ }
145
+ );
146
+
147
+ const result = await submitResponse.json();
148
+
149
+ // 6. Return credential
150
+ return Credential.serialize({
151
+ challenge,
152
+ payload: {
153
+ updateId: result.updateId,
154
+ completionOffset: result.completionOffset,
155
+ sender: config.partyId,
156
+ commandId,
157
+ },
158
+ });
159
+ },
160
+ });
161
+ }
162
+ ```
163
+
164
+ ## Server Implementation
165
+
166
+ ```typescript
167
+ import { Method, Receipt } from "mppx";
168
+ import { cantonMethod } from "./method";
169
+
170
+ export interface CantonServerConfig {
171
+ ledgerUrl: string;
172
+ token: string;
173
+ userId: string;
174
+ recipientPartyId: string;
175
+ network: "mainnet" | "testnet" | "devnet";
176
+ }
177
+
178
+ export function cantonServer(config: CantonServerConfig) {
179
+ return Method.toServer(cantonMethod, {
180
+ async verify({ credential }) {
181
+ const { updateId, completionOffset, sender, commandId } = credential.payload;
182
+ const { amount, recipient, network } = credential.challenge.request;
183
+
184
+ // 1. Network check
185
+ if (network !== config.network) {
186
+ throw new Error("Network mismatch");
187
+ }
188
+
189
+ // 2. Recipient check
190
+ if (recipient !== config.recipientPartyId) {
191
+ throw new Error("Recipient mismatch");
192
+ }
193
+
194
+ // 3. Fetch transaction by updateId
195
+ const txResponse = await fetch(
196
+ `${config.ledgerUrl}/v2/updates/transaction-by-id/${updateId}`,
197
+ {
198
+ headers: {
199
+ "Authorization": `Bearer ${config.token}`,
200
+ },
201
+ }
202
+ );
203
+
204
+ if (!txResponse.ok) {
205
+ throw new Error("Transaction not found on Canton ledger");
206
+ }
207
+
208
+ const tx = await txResponse.json();
209
+
210
+ // 4. Verify: find the created Holding event for recipient
211
+ const recipientHolding = findCreatedHolding(tx, config.recipientPartyId);
212
+ if (!recipientHolding) {
213
+ throw new Error("No holding created for recipient");
214
+ }
215
+
216
+ // 5. Verify amount >= required
217
+ if (parseFloat(recipientHolding.amount) < parseFloat(amount)) {
218
+ throw new Error(`Insufficient: ${recipientHolding.amount} < ${amount}`);
219
+ }
220
+
221
+ // 6. Verify sender
222
+ const senderEvent = findExercisedEvent(tx, sender);
223
+ if (!senderEvent) {
224
+ throw new Error("Sender mismatch");
225
+ }
226
+
227
+ // 7. Return receipt
228
+ return Receipt.from({
229
+ method: "canton",
230
+ reference: updateId,
231
+ status: "success",
232
+ timestamp: new Date().toISOString(),
233
+ });
234
+ },
235
+ });
236
+ }
237
+ ```
238
+
239
+ ## Gateway TransferPreapproval Setup
240
+
241
+ The gateway MUST run this setup once before accepting payments:
242
+
243
+ ```typescript
244
+ // One-time setup: Create TransferPreapproval for gateway party
245
+ // This allows agents to do 1-step transfers to the gateway
246
+ async function setupGatewayPreapproval(config: CantonServerConfig) {
247
+ // Option A: Via validator API (if using Splice wallet)
248
+ // POST /v0/admin/external-party/setup-proposal
249
+
250
+ // Option B: Via Ledger API (direct)
251
+ // Exercise the appropriate TransferPreapproval creation choice
252
+ // on the token registry
253
+
254
+ // The preapproval must be renewed before expiry (default: $1/year fee)
255
+ }
256
+ ```
257
+
258
+ ## Error Mapping
259
+
260
+ | Canton Error | MPP Problem Code | HTTP |
261
+ |-------------|-----------------|------|
262
+ | Transaction not found | verification-failed | 402 |
263
+ | Amount insufficient | payment-insufficient | 402 |
264
+ | Wrong recipient | verification-failed | 402 |
265
+ | Wrong network | verification-failed | 402 |
266
+ | Duplicate commandId | verification-failed | 402 |
267
+ | INVALID_ARGUMENT | malformed-credential | 402 |
268
+ | PERMISSION_DENIED | verification-failed | 402 |
269
+ | UNAVAILABLE | N/A | 503 |
@@ -0,0 +1,111 @@
1
+ import {
2
+ cantonMethod
3
+ } from "./chunk-NTWNP6H5.js";
4
+
5
+ // src/server.ts
6
+ import { Method, Receipt } from "mppx";
7
+ var MppVerificationError = class extends Error {
8
+ problemCode;
9
+ constructor(message, problemCode) {
10
+ super(message);
11
+ this.name = "MppVerificationError";
12
+ this.problemCode = problemCode;
13
+ }
14
+ };
15
+ function findCreatedHolding(tx, recipientParty) {
16
+ const events = tx.eventsById ?? {};
17
+ for (const evt of Object.values(events)) {
18
+ if (evt.createdEvent) {
19
+ const signatories = evt.createdEvent.signatories ?? [];
20
+ const witnesses = evt.createdEvent.witnessParties ?? [];
21
+ const allParties = [...signatories, ...witnesses];
22
+ if (allParties.includes(recipientParty)) {
23
+ const amount = evt.createdEvent.createArgument?.amount;
24
+ if (typeof amount === "string") {
25
+ return { amount };
26
+ }
27
+ }
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ function findExercisedEvent(tx, senderParty) {
33
+ const events = tx.eventsById ?? {};
34
+ for (const evt of Object.values(events)) {
35
+ if (evt.exercisedEvent) {
36
+ const acting = evt.exercisedEvent.actingParties ?? [];
37
+ if (acting.includes(senderParty)) {
38
+ return true;
39
+ }
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+ function cantonServer(config) {
45
+ return Method.toServer(cantonMethod, {
46
+ async verify({
47
+ credential
48
+ }) {
49
+ const { updateId, sender } = credential.payload;
50
+ const { amount, recipient, network } = credential.challenge.request;
51
+ if (network !== config.network) {
52
+ throw new MppVerificationError(
53
+ `Network mismatch: credential is for ${network}, server is on ${config.network}`,
54
+ "verification-failed"
55
+ );
56
+ }
57
+ if (recipient !== config.recipientPartyId) {
58
+ throw new MppVerificationError(
59
+ `Recipient mismatch: credential targets ${recipient}, server is ${config.recipientPartyId}`,
60
+ "verification-failed"
61
+ );
62
+ }
63
+ const txResponse = await fetch(
64
+ `${config.ledgerUrl}/v2/updates/transaction-by-id/${encodeURIComponent(updateId)}`,
65
+ {
66
+ headers: {
67
+ Authorization: `Bearer ${config.token}`
68
+ }
69
+ }
70
+ );
71
+ if (!txResponse.ok) {
72
+ throw new MppVerificationError(
73
+ "Transaction not found on Canton ledger",
74
+ "verification-failed"
75
+ );
76
+ }
77
+ const tx = await txResponse.json();
78
+ const recipientHolding = findCreatedHolding(tx, config.recipientPartyId);
79
+ if (!recipientHolding) {
80
+ throw new MppVerificationError(
81
+ "No holding created for recipient in transaction",
82
+ "verification-failed"
83
+ );
84
+ }
85
+ if (parseFloat(recipientHolding.amount) < parseFloat(amount)) {
86
+ throw new MppVerificationError(
87
+ `Payment insufficient: received ${recipientHolding.amount}, required ${amount}`,
88
+ "payment-insufficient"
89
+ );
90
+ }
91
+ if (!findExercisedEvent(tx, sender)) {
92
+ throw new MppVerificationError(
93
+ `Sender mismatch: ${sender} did not execute the transfer`,
94
+ "verification-failed"
95
+ );
96
+ }
97
+ return Receipt.from({
98
+ method: "canton",
99
+ reference: updateId,
100
+ status: "success",
101
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
102
+ });
103
+ }
104
+ });
105
+ }
106
+
107
+ export {
108
+ MppVerificationError,
109
+ cantonServer
110
+ };
111
+ //# sourceMappingURL=chunk-5CWLHTUR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"sourcesContent":["/**\n * Canton MPP server — used by gateways to verify payments.\n *\n * Verification flow:\n * 1. Check network matches server config\n * 2. Check recipient matches server's party\n * 3. Fetch transaction from ledger by updateId\n * 4. Find Created Holding event for recipient, verify amount >= required\n * 5. Find Exercised event proving the sender executed the transfer\n * 6. Return receipt\n */\n\nimport { Method, Receipt, type CredentialData } from \"mppx\";\nimport { cantonMethod } from \"./method.js\";\nimport type { CantonCredentialPayload, CantonRequest } from \"./schemas.js\";\n\nexport type CantonNetwork = \"mainnet\" | \"testnet\" | \"devnet\";\n\nexport interface CantonMppServerConfig {\n ledgerUrl: string;\n token: string;\n userId: string;\n recipientPartyId: string;\n network: CantonNetwork;\n}\n\nexport class MppVerificationError extends Error {\n readonly problemCode: string;\n\n constructor(message: string, problemCode: string) {\n super(message);\n this.name = \"MppVerificationError\";\n this.problemCode = problemCode;\n }\n}\n\ninterface TransactionEvent {\n createdEvent?: {\n contractId: string;\n templateId: string;\n createArgument: Record<string, unknown>;\n witnessParties: string[];\n signatories: string[];\n };\n exercisedEvent?: {\n contractId: string;\n choice: string;\n actingParties: string[];\n };\n}\n\ninterface TransactionData {\n updateId: string;\n eventsById?: Record<string, TransactionEvent>;\n rootEventIds?: string[];\n}\n\nfunction findCreatedHolding(\n tx: TransactionData,\n recipientParty: string,\n): { amount: string } | null {\n const events = tx.eventsById ?? {};\n\n for (const evt of Object.values(events)) {\n if (evt.createdEvent) {\n const signatories = evt.createdEvent.signatories ?? [];\n const witnesses = evt.createdEvent.witnessParties ?? [];\n const allParties = [...signatories, ...witnesses];\n\n if (allParties.includes(recipientParty)) {\n const amount = evt.createdEvent.createArgument?.amount;\n if (typeof amount === \"string\") {\n return { amount };\n }\n }\n }\n }\n\n return null;\n}\n\nfunction findExercisedEvent(\n tx: TransactionData,\n senderParty: string,\n): boolean {\n const events = tx.eventsById ?? {};\n\n for (const evt of Object.values(events)) {\n if (evt.exercisedEvent) {\n const acting = evt.exercisedEvent.actingParties ?? [];\n if (acting.includes(senderParty)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\nexport function cantonServer(config: CantonMppServerConfig) {\n return Method.toServer(cantonMethod, {\n async verify({\n credential,\n }: {\n credential: CredentialData<CantonCredentialPayload, CantonRequest>;\n }) {\n const { updateId, sender } = credential.payload;\n const { amount, recipient, network } = credential.challenge.request;\n\n // 1. Network check\n if (network !== config.network) {\n throw new MppVerificationError(\n `Network mismatch: credential is for ${network}, server is on ${config.network}`,\n \"verification-failed\",\n );\n }\n\n // 2. Recipient check\n if (recipient !== config.recipientPartyId) {\n throw new MppVerificationError(\n `Recipient mismatch: credential targets ${recipient}, server is ${config.recipientPartyId}`,\n \"verification-failed\",\n );\n }\n\n // 3. Fetch transaction by updateId\n const txResponse = await fetch(\n `${config.ledgerUrl}/v2/updates/transaction-by-id/${encodeURIComponent(updateId)}`,\n {\n headers: {\n Authorization: `Bearer ${config.token}`,\n },\n },\n );\n\n if (!txResponse.ok) {\n throw new MppVerificationError(\n \"Transaction not found on Canton ledger\",\n \"verification-failed\",\n );\n }\n\n const tx = (await txResponse.json()) as TransactionData;\n\n // 4. Find created Holding for recipient and verify amount\n const recipientHolding = findCreatedHolding(tx, config.recipientPartyId);\n if (!recipientHolding) {\n throw new MppVerificationError(\n \"No holding created for recipient in transaction\",\n \"verification-failed\",\n );\n }\n\n if (parseFloat(recipientHolding.amount) < parseFloat(amount)) {\n throw new MppVerificationError(\n `Payment insufficient: received ${recipientHolding.amount}, required ${amount}`,\n \"payment-insufficient\",\n );\n }\n\n // 5. Verify sender\n if (!findExercisedEvent(tx, sender)) {\n throw new MppVerificationError(\n `Sender mismatch: ${sender} did not execute the transfer`,\n \"verification-failed\",\n );\n }\n\n // 6. Return receipt\n return Receipt.from({\n method: \"canton\",\n reference: updateId,\n status: \"success\",\n timestamp: new Date().toISOString(),\n });\n },\n });\n}\n"],"mappings":";;;;;AAYA,SAAS,QAAQ,eAAoC;AAc9C,IAAM,uBAAN,cAAmC,MAAM;AAAA,EACrC;AAAA,EAET,YAAY,SAAiB,aAAqB;AAChD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,cAAc;AAAA,EACrB;AACF;AAuBA,SAAS,mBACP,IACA,gBAC2B;AAC3B,QAAM,SAAS,GAAG,cAAc,CAAC;AAEjC,aAAW,OAAO,OAAO,OAAO,MAAM,GAAG;AACvC,QAAI,IAAI,cAAc;AACpB,YAAM,cAAc,IAAI,aAAa,eAAe,CAAC;AACrD,YAAM,YAAY,IAAI,aAAa,kBAAkB,CAAC;AACtD,YAAM,aAAa,CAAC,GAAG,aAAa,GAAG,SAAS;AAEhD,UAAI,WAAW,SAAS,cAAc,GAAG;AACvC,cAAM,SAAS,IAAI,aAAa,gBAAgB;AAChD,YAAI,OAAO,WAAW,UAAU;AAC9B,iBAAO,EAAE,OAAO;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,mBACP,IACA,aACS;AACT,QAAM,SAAS,GAAG,cAAc,CAAC;AAEjC,aAAW,OAAO,OAAO,OAAO,MAAM,GAAG;AACvC,QAAI,IAAI,gBAAgB;AACtB,YAAM,SAAS,IAAI,eAAe,iBAAiB,CAAC;AACpD,UAAI,OAAO,SAAS,WAAW,GAAG;AAChC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,aAAa,QAA+B;AAC1D,SAAO,OAAO,SAAS,cAAc;AAAA,IACnC,MAAM,OAAO;AAAA,MACX;AAAA,IACF,GAEG;AACD,YAAM,EAAE,UAAU,OAAO,IAAI,WAAW;AACxC,YAAM,EAAE,QAAQ,WAAW,QAAQ,IAAI,WAAW,UAAU;AAG5D,UAAI,YAAY,OAAO,SAAS;AAC9B,cAAM,IAAI;AAAA,UACR,uCAAuC,OAAO,kBAAkB,OAAO,OAAO;AAAA,UAC9E;AAAA,QACF;AAAA,MACF;AAGA,UAAI,cAAc,OAAO,kBAAkB;AACzC,cAAM,IAAI;AAAA,UACR,0CAA0C,SAAS,eAAe,OAAO,gBAAgB;AAAA,UACzF;AAAA,QACF;AAAA,MACF;AAGA,YAAM,aAAa,MAAM;AAAA,QACvB,GAAG,OAAO,SAAS,iCAAiC,mBAAmB,QAAQ,CAAC;AAAA,QAChF;AAAA,UACE,SAAS;AAAA,YACP,eAAe,UAAU,OAAO,KAAK;AAAA,UACvC;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,WAAW,IAAI;AAClB,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAEA,YAAM,KAAM,MAAM,WAAW,KAAK;AAGlC,YAAM,mBAAmB,mBAAmB,IAAI,OAAO,gBAAgB;AACvE,UAAI,CAAC,kBAAkB;AACrB,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAEA,UAAI,WAAW,iBAAiB,MAAM,IAAI,WAAW,MAAM,GAAG;AAC5D,cAAM,IAAI;AAAA,UACR,kCAAkC,iBAAiB,MAAM,cAAc,MAAM;AAAA,UAC7E;AAAA,QACF;AAAA,MACF;AAGA,UAAI,CAAC,mBAAmB,IAAI,MAAM,GAAG;AACnC,cAAM,IAAI;AAAA,UACR,oBAAoB,MAAM;AAAA,UAC1B;AAAA,QACF;AAAA,MACF;AAGA,aAAO,QAAQ,KAAK;AAAA,QAClB,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MACpC,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,140 @@
1
+ import {
2
+ cantonMethod
3
+ } from "./chunk-NTWNP6H5.js";
4
+
5
+ // src/client.ts
6
+ import { Credential, Method } from "mppx";
7
+ var USDCX_HOLDING_TEMPLATE_ID = "Splice.Api.Token.HoldingV1:Holding";
8
+ var TRANSFER_FACTORY_TEMPLATE_ID = "Splice.Api.Token.TransferFactoryV1:TransferFactory";
9
+ var USDCX_INSTRUMENT_ID = "USDCx";
10
+ async function fetchJson(url, token, init) {
11
+ const response = await fetch(url, {
12
+ ...init,
13
+ headers: {
14
+ "Content-Type": "application/json",
15
+ Authorization: `Bearer ${token}`,
16
+ ...init?.headers
17
+ }
18
+ });
19
+ if (!response.ok) {
20
+ const text = await response.text().catch(() => "");
21
+ throw new Error(`Canton API error: HTTP ${response.status} \u2014 ${text}`);
22
+ }
23
+ return response.json();
24
+ }
25
+ async function getLedgerEnd(config) {
26
+ const data = await fetchJson(`${config.ledgerUrl}/v2/state/ledger-end`, config.token);
27
+ return data.offset;
28
+ }
29
+ async function queryHoldings(config) {
30
+ const offset = await getLedgerEnd(config);
31
+ const data = await fetchJson(`${config.ledgerUrl}/v2/state/active-contracts`, config.token, {
32
+ method: "POST",
33
+ body: JSON.stringify({
34
+ eventFormat: {
35
+ filtersByParty: {
36
+ [config.partyId]: {
37
+ cumulative: [
38
+ {
39
+ identifierFilter: {
40
+ TemplateFilter: {
41
+ value: { templateId: USDCX_HOLDING_TEMPLATE_ID }
42
+ }
43
+ }
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ verbose: true
49
+ },
50
+ activeAtOffset: offset
51
+ })
52
+ });
53
+ return (data.contractEntry ?? []).filter((e) => e.createdEvent != null).map((e) => ({
54
+ contractId: e.createdEvent.contractId,
55
+ amount: e.createdEvent.createArgument.amount
56
+ }));
57
+ }
58
+ function selectHoldings(holdings, requiredAmount) {
59
+ if (holdings.length === 0) {
60
+ throw new Error(`Insufficient balance: no holdings available`);
61
+ }
62
+ const sorted = [...holdings].sort(
63
+ (a, b) => parseFloat(b.amount) - parseFloat(a.amount)
64
+ );
65
+ for (let i = sorted.length - 1; i >= 0; i--) {
66
+ if (parseFloat(sorted[i].amount) >= parseFloat(requiredAmount)) {
67
+ return [sorted[i].contractId];
68
+ }
69
+ }
70
+ let total = 0;
71
+ const selected = [];
72
+ for (const h of sorted) {
73
+ selected.push(h.contractId);
74
+ total += parseFloat(h.amount);
75
+ if (total >= parseFloat(requiredAmount)) {
76
+ return selected;
77
+ }
78
+ }
79
+ throw new Error(
80
+ `Insufficient balance: have ${total}, need ${requiredAmount}`
81
+ );
82
+ }
83
+ function cantonClient(config) {
84
+ return Method.toClient(cantonMethod, {
85
+ async createCredential({ challenge }) {
86
+ if (challenge.request.network !== config.network) {
87
+ throw new Error(
88
+ `Network mismatch: challenge requires ${challenge.request.network}, agent configured for ${config.network}`
89
+ );
90
+ }
91
+ const holdings = await queryHoldings(config);
92
+ const selectedCids = selectHoldings(holdings, challenge.request.amount);
93
+ const commandId = crypto.randomUUID();
94
+ const result = await fetchJson(
95
+ `${config.ledgerUrl}/v2/commands/submit-and-wait`,
96
+ config.token,
97
+ {
98
+ method: "POST",
99
+ body: JSON.stringify({
100
+ commands: [
101
+ {
102
+ ExerciseCommand: {
103
+ templateId: TRANSFER_FACTORY_TEMPLATE_ID,
104
+ contractId: selectedCids[0],
105
+ choice: "TransferFactory_Transfer",
106
+ choiceArgument: {
107
+ sender: config.partyId,
108
+ receiver: challenge.request.recipient,
109
+ amount: challenge.request.amount,
110
+ instrumentId: USDCX_INSTRUMENT_ID,
111
+ inputHoldingCids: selectedCids,
112
+ meta: {}
113
+ }
114
+ }
115
+ }
116
+ ],
117
+ userId: config.userId,
118
+ commandId,
119
+ actAs: [config.partyId],
120
+ readAs: [config.partyId]
121
+ })
122
+ }
123
+ );
124
+ return Credential.serialize({
125
+ challenge,
126
+ payload: {
127
+ updateId: result.updateId,
128
+ completionOffset: result.completionOffset,
129
+ sender: config.partyId,
130
+ commandId
131
+ }
132
+ });
133
+ }
134
+ });
135
+ }
136
+
137
+ export {
138
+ cantonClient
139
+ };
140
+ //# sourceMappingURL=chunk-757U7PM3.js.map