@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.
- package/.turbo/turbo-build.log +40 -0
- package/.turbo/turbo-test.log +16 -0
- package/README.md +104 -0
- package/SPEC.md +269 -0
- package/dist/chunk-5CWLHTUR.js +111 -0
- package/dist/chunk-5CWLHTUR.js.map +1 -0
- package/dist/chunk-757U7PM3.js +140 -0
- package/dist/chunk-757U7PM3.js.map +1 -0
- package/dist/chunk-NTWNP6H5.js +43 -0
- package/dist/chunk-NTWNP6H5.js.map +1 -0
- package/dist/client.cjs +200 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +24 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.js +8 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +319 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +172 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +28 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -0
- package/package.json +64 -0
- package/src/__tests__/client.test.ts +216 -0
- package/src/__tests__/method.test.ts +140 -0
- package/src/__tests__/server.test.ts +361 -0
- package/src/client.ts +198 -0
- package/src/index.ts +29 -0
- package/src/method.ts +21 -0
- package/src/schemas.ts +33 -0
- package/src/server.ts +178 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["/**\n * Canton MPP client — used by agents to pay for services.\n *\n * Flow:\n * 1. Receive 402 challenge with amount, recipient, network\n * 2. Validate network matches agent config\n * 3. Query agent's USDCx holdings via Ledger API\n * 4. Select holdings covering the required amount\n * 5. Exercise TransferFactory_Transfer (1-step, requires recipient TransferPreapproval)\n * 6. Return serialized credential with updateId + completionOffset\n */\n\nimport { Credential, Method, type Challenge } from \"mppx\";\nimport { cantonMethod } from \"./method.js\";\nimport type { CantonRequest } from \"./schemas.js\";\n\nconst USDCX_HOLDING_TEMPLATE_ID = \"Splice.Api.Token.HoldingV1:Holding\";\nconst TRANSFER_FACTORY_TEMPLATE_ID = \"Splice.Api.Token.TransferFactoryV1:TransferFactory\";\nconst USDCX_INSTRUMENT_ID = \"USDCx\";\n\nexport type CantonNetwork = \"mainnet\" | \"testnet\" | \"devnet\";\n\nexport interface CantonMppClientConfig {\n ledgerUrl: string;\n token: string;\n userId: string;\n partyId: string;\n network: CantonNetwork;\n}\n\ninterface HoldingContract {\n contractId: string;\n amount: string;\n}\n\nasync function fetchJson(url: string, token: string, init?: RequestInit): Promise<unknown> {\n const response = await fetch(url, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n ...(init?.headers as Record<string, string>),\n },\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new Error(`Canton API error: HTTP ${response.status} — ${text}`);\n }\n\n return response.json();\n}\n\nasync function getLedgerEnd(config: CantonMppClientConfig): Promise<number> {\n const data = (await fetchJson(`${config.ledgerUrl}/v2/state/ledger-end`, config.token)) as {\n offset: number;\n };\n return data.offset;\n}\n\nasync function queryHoldings(config: CantonMppClientConfig): Promise<HoldingContract[]> {\n const offset = await getLedgerEnd(config);\n\n const data = (await fetchJson(`${config.ledgerUrl}/v2/state/active-contracts`, config.token, {\n method: \"POST\",\n body: JSON.stringify({\n eventFormat: {\n filtersByParty: {\n [config.partyId]: {\n cumulative: [\n {\n identifierFilter: {\n TemplateFilter: {\n value: { templateId: USDCX_HOLDING_TEMPLATE_ID },\n },\n },\n },\n ],\n },\n },\n verbose: true,\n },\n activeAtOffset: offset,\n }),\n })) as {\n contractEntry?: Array<{\n createdEvent?: { contractId: string; createArgument: { amount: string } };\n }>;\n };\n\n return (data.contractEntry ?? [])\n .filter((e) => e.createdEvent != null)\n .map((e) => ({\n contractId: e.createdEvent!.contractId,\n amount: e.createdEvent!.createArgument.amount,\n }));\n}\n\nfunction selectHoldings(\n holdings: HoldingContract[],\n requiredAmount: string,\n): string[] {\n if (holdings.length === 0) {\n throw new Error(`Insufficient balance: no holdings available`);\n }\n\n // Sort descending\n const sorted = [...holdings].sort(\n (a, b) => parseFloat(b.amount) - parseFloat(a.amount),\n );\n\n // Try single holding\n for (let i = sorted.length - 1; i >= 0; i--) {\n if (parseFloat(sorted[i].amount) >= parseFloat(requiredAmount)) {\n return [sorted[i].contractId];\n }\n }\n\n // Accumulate multiple\n let total = 0;\n const selected: string[] = [];\n for (const h of sorted) {\n selected.push(h.contractId);\n total += parseFloat(h.amount);\n if (total >= parseFloat(requiredAmount)) {\n return selected;\n }\n }\n\n throw new Error(\n `Insufficient balance: have ${total}, need ${requiredAmount}`,\n );\n}\n\nexport function cantonClient(config: CantonMppClientConfig) {\n return Method.toClient(cantonMethod, {\n async createCredential({ challenge }: { challenge: Challenge<CantonRequest> }) {\n // 1. Validate network\n if (challenge.request.network !== config.network) {\n throw new Error(\n `Network mismatch: challenge requires ${challenge.request.network}, agent configured for ${config.network}`,\n );\n }\n\n // 2. Query holdings\n const holdings = await queryHoldings(config);\n\n // 3. Select holdings covering amount\n const selectedCids = selectHoldings(holdings, challenge.request.amount);\n\n // 4. Generate commandId\n const commandId = crypto.randomUUID();\n\n // 5. Exercise TransferFactory_Transfer\n const result = (await fetchJson(\n `${config.ledgerUrl}/v2/commands/submit-and-wait`,\n config.token,\n {\n method: \"POST\",\n body: JSON.stringify({\n commands: [\n {\n ExerciseCommand: {\n templateId: TRANSFER_FACTORY_TEMPLATE_ID,\n contractId: selectedCids[0],\n choice: \"TransferFactory_Transfer\",\n choiceArgument: {\n sender: config.partyId,\n receiver: challenge.request.recipient,\n amount: challenge.request.amount,\n instrumentId: USDCX_INSTRUMENT_ID,\n inputHoldingCids: selectedCids,\n meta: {},\n },\n },\n },\n ],\n userId: config.userId,\n commandId,\n actAs: [config.partyId],\n readAs: [config.partyId],\n }),\n },\n )) as { updateId: string; completionOffset: number };\n\n // 6. Return serialized credential\n return Credential.serialize({\n challenge,\n payload: {\n updateId: result.updateId,\n completionOffset: result.completionOffset,\n sender: config.partyId,\n commandId,\n },\n });\n },\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/schemas.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var requestSchema = z.object({
|
|
4
|
+
amount: z.string().regex(/^\d+\.?\d{0,10}$/, "Invalid amount format"),
|
|
5
|
+
currency: z.enum(["USDCx", "CC"]),
|
|
6
|
+
recipient: z.string().min(1, "Recipient party ID required"),
|
|
7
|
+
network: z.enum(["mainnet", "testnet", "devnet"]),
|
|
8
|
+
description: z.string().optional(),
|
|
9
|
+
expiry: z.number().int().min(1).max(3600).default(300)
|
|
10
|
+
});
|
|
11
|
+
var credentialPayloadSchema = z.object({
|
|
12
|
+
updateId: z.string().min(1, "updateId required"),
|
|
13
|
+
completionOffset: z.number().int(),
|
|
14
|
+
sender: z.string().min(1, "Sender party ID required"),
|
|
15
|
+
commandId: z.string().min(1, "commandId required")
|
|
16
|
+
});
|
|
17
|
+
var receiptSchema = z.object({
|
|
18
|
+
method: z.literal("canton"),
|
|
19
|
+
reference: z.string().min(1),
|
|
20
|
+
status: z.enum(["success", "failed"]),
|
|
21
|
+
timestamp: z.string()
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// src/method.ts
|
|
25
|
+
import { Method } from "mppx";
|
|
26
|
+
var cantonMethod = Method.from({
|
|
27
|
+
intent: "charge",
|
|
28
|
+
name: "canton",
|
|
29
|
+
schema: {
|
|
30
|
+
credential: {
|
|
31
|
+
payload: credentialPayloadSchema
|
|
32
|
+
},
|
|
33
|
+
request: requestSchema
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
requestSchema,
|
|
39
|
+
credentialPayloadSchema,
|
|
40
|
+
receiptSchema,
|
|
41
|
+
cantonMethod
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=chunk-NTWNP6H5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schemas.ts","../src/method.ts"],"sourcesContent":["/**\n * Zod schemas for Canton MPP payment method.\n * Exported separately for reuse in validation, gateway middleware, etc.\n */\n\nimport { z } from \"zod\";\n\nexport const requestSchema = z.object({\n amount: z.string().regex(/^\\d+\\.?\\d{0,10}$/, \"Invalid amount format\"),\n currency: z.enum([\"USDCx\", \"CC\"]),\n recipient: z.string().min(1, \"Recipient party ID required\"),\n network: z.enum([\"mainnet\", \"testnet\", \"devnet\"]),\n description: z.string().optional(),\n expiry: z.number().int().min(1).max(3600).default(300),\n});\n\nexport const credentialPayloadSchema = z.object({\n updateId: z.string().min(1, \"updateId required\"),\n completionOffset: z.number().int(),\n sender: z.string().min(1, \"Sender party ID required\"),\n commandId: z.string().min(1, \"commandId required\"),\n});\n\nexport const receiptSchema = z.object({\n method: z.literal(\"canton\"),\n reference: z.string().min(1),\n status: z.enum([\"success\", \"failed\"]),\n timestamp: z.string(),\n});\n\nexport type CantonRequest = z.infer<typeof requestSchema>;\nexport type CantonCredentialPayload = z.infer<typeof credentialPayloadSchema>;\nexport type CantonReceipt = z.infer<typeof receiptSchema>;\n","/**\n * Canton MPP method definition.\n *\n * Defines the \"canton\" payment method using the mppx Method.from() pattern.\n * The method specifies the request and credential schemas that both\n * client (agent) and server (gateway) use.\n */\n\nimport { Method } from \"mppx\";\nimport { credentialPayloadSchema, requestSchema } from \"./schemas.js\";\n\nexport const cantonMethod = Method.from({\n intent: \"charge\",\n name: \"canton\",\n schema: {\n credential: {\n payload: credentialPayloadSchema,\n },\n request: requestSchema,\n },\n});\n"],"mappings":";AAKA,SAAS,SAAS;AAEX,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,QAAQ,EAAE,OAAO,EAAE,MAAM,oBAAoB,uBAAuB;AAAA,EACpE,UAAU,EAAE,KAAK,CAAC,SAAS,IAAI,CAAC;AAAA,EAChC,WAAW,EAAE,OAAO,EAAE,IAAI,GAAG,6BAA6B;AAAA,EAC1D,SAAS,EAAE,KAAK,CAAC,WAAW,WAAW,QAAQ,CAAC;AAAA,EAChD,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,EAAE,QAAQ,GAAG;AACvD,CAAC;AAEM,IAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,UAAU,EAAE,OAAO,EAAE,IAAI,GAAG,mBAAmB;AAAA,EAC/C,kBAAkB,EAAE,OAAO,EAAE,IAAI;AAAA,EACjC,QAAQ,EAAE,OAAO,EAAE,IAAI,GAAG,0BAA0B;AAAA,EACpD,WAAW,EAAE,OAAO,EAAE,IAAI,GAAG,oBAAoB;AACnD,CAAC;AAEM,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,QAAQ,EAAE,QAAQ,QAAQ;AAAA,EAC1B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,QAAQ,EAAE,KAAK,CAAC,WAAW,QAAQ,CAAC;AAAA,EACpC,WAAW,EAAE,OAAO;AACtB,CAAC;;;ACpBD,SAAS,cAAc;AAGhB,IAAM,eAAe,OAAO,KAAK;AAAA,EACtC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,QAAQ;AAAA,IACN,YAAY;AAAA,MACV,SAAS;AAAA,IACX;AAAA,IACA,SAAS;AAAA,EACX;AACF,CAAC;","names":[]}
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
cantonClient: () => cantonClient
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(client_exports);
|
|
26
|
+
var import_mppx2 = require("mppx");
|
|
27
|
+
|
|
28
|
+
// src/method.ts
|
|
29
|
+
var import_mppx = require("mppx");
|
|
30
|
+
|
|
31
|
+
// src/schemas.ts
|
|
32
|
+
var import_zod = require("zod");
|
|
33
|
+
var requestSchema = import_zod.z.object({
|
|
34
|
+
amount: import_zod.z.string().regex(/^\d+\.?\d{0,10}$/, "Invalid amount format"),
|
|
35
|
+
currency: import_zod.z.enum(["USDCx", "CC"]),
|
|
36
|
+
recipient: import_zod.z.string().min(1, "Recipient party ID required"),
|
|
37
|
+
network: import_zod.z.enum(["mainnet", "testnet", "devnet"]),
|
|
38
|
+
description: import_zod.z.string().optional(),
|
|
39
|
+
expiry: import_zod.z.number().int().min(1).max(3600).default(300)
|
|
40
|
+
});
|
|
41
|
+
var credentialPayloadSchema = import_zod.z.object({
|
|
42
|
+
updateId: import_zod.z.string().min(1, "updateId required"),
|
|
43
|
+
completionOffset: import_zod.z.number().int(),
|
|
44
|
+
sender: import_zod.z.string().min(1, "Sender party ID required"),
|
|
45
|
+
commandId: import_zod.z.string().min(1, "commandId required")
|
|
46
|
+
});
|
|
47
|
+
var receiptSchema = import_zod.z.object({
|
|
48
|
+
method: import_zod.z.literal("canton"),
|
|
49
|
+
reference: import_zod.z.string().min(1),
|
|
50
|
+
status: import_zod.z.enum(["success", "failed"]),
|
|
51
|
+
timestamp: import_zod.z.string()
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/method.ts
|
|
55
|
+
var cantonMethod = import_mppx.Method.from({
|
|
56
|
+
intent: "charge",
|
|
57
|
+
name: "canton",
|
|
58
|
+
schema: {
|
|
59
|
+
credential: {
|
|
60
|
+
payload: credentialPayloadSchema
|
|
61
|
+
},
|
|
62
|
+
request: requestSchema
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/client.ts
|
|
67
|
+
var USDCX_HOLDING_TEMPLATE_ID = "Splice.Api.Token.HoldingV1:Holding";
|
|
68
|
+
var TRANSFER_FACTORY_TEMPLATE_ID = "Splice.Api.Token.TransferFactoryV1:TransferFactory";
|
|
69
|
+
var USDCX_INSTRUMENT_ID = "USDCx";
|
|
70
|
+
async function fetchJson(url, token, init) {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
...init,
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
Authorization: `Bearer ${token}`,
|
|
76
|
+
...init?.headers
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const text = await response.text().catch(() => "");
|
|
81
|
+
throw new Error(`Canton API error: HTTP ${response.status} \u2014 ${text}`);
|
|
82
|
+
}
|
|
83
|
+
return response.json();
|
|
84
|
+
}
|
|
85
|
+
async function getLedgerEnd(config) {
|
|
86
|
+
const data = await fetchJson(`${config.ledgerUrl}/v2/state/ledger-end`, config.token);
|
|
87
|
+
return data.offset;
|
|
88
|
+
}
|
|
89
|
+
async function queryHoldings(config) {
|
|
90
|
+
const offset = await getLedgerEnd(config);
|
|
91
|
+
const data = await fetchJson(`${config.ledgerUrl}/v2/state/active-contracts`, config.token, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
eventFormat: {
|
|
95
|
+
filtersByParty: {
|
|
96
|
+
[config.partyId]: {
|
|
97
|
+
cumulative: [
|
|
98
|
+
{
|
|
99
|
+
identifierFilter: {
|
|
100
|
+
TemplateFilter: {
|
|
101
|
+
value: { templateId: USDCX_HOLDING_TEMPLATE_ID }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
verbose: true
|
|
109
|
+
},
|
|
110
|
+
activeAtOffset: offset
|
|
111
|
+
})
|
|
112
|
+
});
|
|
113
|
+
return (data.contractEntry ?? []).filter((e) => e.createdEvent != null).map((e) => ({
|
|
114
|
+
contractId: e.createdEvent.contractId,
|
|
115
|
+
amount: e.createdEvent.createArgument.amount
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
function selectHoldings(holdings, requiredAmount) {
|
|
119
|
+
if (holdings.length === 0) {
|
|
120
|
+
throw new Error(`Insufficient balance: no holdings available`);
|
|
121
|
+
}
|
|
122
|
+
const sorted = [...holdings].sort(
|
|
123
|
+
(a, b) => parseFloat(b.amount) - parseFloat(a.amount)
|
|
124
|
+
);
|
|
125
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
126
|
+
if (parseFloat(sorted[i].amount) >= parseFloat(requiredAmount)) {
|
|
127
|
+
return [sorted[i].contractId];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let total = 0;
|
|
131
|
+
const selected = [];
|
|
132
|
+
for (const h of sorted) {
|
|
133
|
+
selected.push(h.contractId);
|
|
134
|
+
total += parseFloat(h.amount);
|
|
135
|
+
if (total >= parseFloat(requiredAmount)) {
|
|
136
|
+
return selected;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Insufficient balance: have ${total}, need ${requiredAmount}`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
function cantonClient(config) {
|
|
144
|
+
return import_mppx2.Method.toClient(cantonMethod, {
|
|
145
|
+
async createCredential({ challenge }) {
|
|
146
|
+
if (challenge.request.network !== config.network) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Network mismatch: challenge requires ${challenge.request.network}, agent configured for ${config.network}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const holdings = await queryHoldings(config);
|
|
152
|
+
const selectedCids = selectHoldings(holdings, challenge.request.amount);
|
|
153
|
+
const commandId = crypto.randomUUID();
|
|
154
|
+
const result = await fetchJson(
|
|
155
|
+
`${config.ledgerUrl}/v2/commands/submit-and-wait`,
|
|
156
|
+
config.token,
|
|
157
|
+
{
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
commands: [
|
|
161
|
+
{
|
|
162
|
+
ExerciseCommand: {
|
|
163
|
+
templateId: TRANSFER_FACTORY_TEMPLATE_ID,
|
|
164
|
+
contractId: selectedCids[0],
|
|
165
|
+
choice: "TransferFactory_Transfer",
|
|
166
|
+
choiceArgument: {
|
|
167
|
+
sender: config.partyId,
|
|
168
|
+
receiver: challenge.request.recipient,
|
|
169
|
+
amount: challenge.request.amount,
|
|
170
|
+
instrumentId: USDCX_INSTRUMENT_ID,
|
|
171
|
+
inputHoldingCids: selectedCids,
|
|
172
|
+
meta: {}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
userId: config.userId,
|
|
178
|
+
commandId,
|
|
179
|
+
actAs: [config.partyId],
|
|
180
|
+
readAs: [config.partyId]
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
return import_mppx2.Credential.serialize({
|
|
185
|
+
challenge,
|
|
186
|
+
payload: {
|
|
187
|
+
updateId: result.updateId,
|
|
188
|
+
completionOffset: result.completionOffset,
|
|
189
|
+
sender: config.partyId,
|
|
190
|
+
commandId
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
197
|
+
0 && (module.exports = {
|
|
198
|
+
cantonClient
|
|
199
|
+
});
|
|
200
|
+
//# sourceMappingURL=client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/method.ts","../src/schemas.ts"],"sourcesContent":["/**\n * Canton MPP client — used by agents to pay for services.\n *\n * Flow:\n * 1. Receive 402 challenge with amount, recipient, network\n * 2. Validate network matches agent config\n * 3. Query agent's USDCx holdings via Ledger API\n * 4. Select holdings covering the required amount\n * 5. Exercise TransferFactory_Transfer (1-step, requires recipient TransferPreapproval)\n * 6. Return serialized credential with updateId + completionOffset\n */\n\nimport { Credential, Method, type Challenge } from \"mppx\";\nimport { cantonMethod } from \"./method.js\";\nimport type { CantonRequest } from \"./schemas.js\";\n\nconst USDCX_HOLDING_TEMPLATE_ID = \"Splice.Api.Token.HoldingV1:Holding\";\nconst TRANSFER_FACTORY_TEMPLATE_ID = \"Splice.Api.Token.TransferFactoryV1:TransferFactory\";\nconst USDCX_INSTRUMENT_ID = \"USDCx\";\n\nexport type CantonNetwork = \"mainnet\" | \"testnet\" | \"devnet\";\n\nexport interface CantonMppClientConfig {\n ledgerUrl: string;\n token: string;\n userId: string;\n partyId: string;\n network: CantonNetwork;\n}\n\ninterface HoldingContract {\n contractId: string;\n amount: string;\n}\n\nasync function fetchJson(url: string, token: string, init?: RequestInit): Promise<unknown> {\n const response = await fetch(url, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${token}`,\n ...(init?.headers as Record<string, string>),\n },\n });\n\n if (!response.ok) {\n const text = await response.text().catch(() => \"\");\n throw new Error(`Canton API error: HTTP ${response.status} — ${text}`);\n }\n\n return response.json();\n}\n\nasync function getLedgerEnd(config: CantonMppClientConfig): Promise<number> {\n const data = (await fetchJson(`${config.ledgerUrl}/v2/state/ledger-end`, config.token)) as {\n offset: number;\n };\n return data.offset;\n}\n\nasync function queryHoldings(config: CantonMppClientConfig): Promise<HoldingContract[]> {\n const offset = await getLedgerEnd(config);\n\n const data = (await fetchJson(`${config.ledgerUrl}/v2/state/active-contracts`, config.token, {\n method: \"POST\",\n body: JSON.stringify({\n eventFormat: {\n filtersByParty: {\n [config.partyId]: {\n cumulative: [\n {\n identifierFilter: {\n TemplateFilter: {\n value: { templateId: USDCX_HOLDING_TEMPLATE_ID },\n },\n },\n },\n ],\n },\n },\n verbose: true,\n },\n activeAtOffset: offset,\n }),\n })) as {\n contractEntry?: Array<{\n createdEvent?: { contractId: string; createArgument: { amount: string } };\n }>;\n };\n\n return (data.contractEntry ?? [])\n .filter((e) => e.createdEvent != null)\n .map((e) => ({\n contractId: e.createdEvent!.contractId,\n amount: e.createdEvent!.createArgument.amount,\n }));\n}\n\nfunction selectHoldings(\n holdings: HoldingContract[],\n requiredAmount: string,\n): string[] {\n if (holdings.length === 0) {\n throw new Error(`Insufficient balance: no holdings available`);\n }\n\n // Sort descending\n const sorted = [...holdings].sort(\n (a, b) => parseFloat(b.amount) - parseFloat(a.amount),\n );\n\n // Try single holding\n for (let i = sorted.length - 1; i >= 0; i--) {\n if (parseFloat(sorted[i].amount) >= parseFloat(requiredAmount)) {\n return [sorted[i].contractId];\n }\n }\n\n // Accumulate multiple\n let total = 0;\n const selected: string[] = [];\n for (const h of sorted) {\n selected.push(h.contractId);\n total += parseFloat(h.amount);\n if (total >= parseFloat(requiredAmount)) {\n return selected;\n }\n }\n\n throw new Error(\n `Insufficient balance: have ${total}, need ${requiredAmount}`,\n );\n}\n\nexport function cantonClient(config: CantonMppClientConfig) {\n return Method.toClient(cantonMethod, {\n async createCredential({ challenge }: { challenge: Challenge<CantonRequest> }) {\n // 1. Validate network\n if (challenge.request.network !== config.network) {\n throw new Error(\n `Network mismatch: challenge requires ${challenge.request.network}, agent configured for ${config.network}`,\n );\n }\n\n // 2. Query holdings\n const holdings = await queryHoldings(config);\n\n // 3. Select holdings covering amount\n const selectedCids = selectHoldings(holdings, challenge.request.amount);\n\n // 4. Generate commandId\n const commandId = crypto.randomUUID();\n\n // 5. Exercise TransferFactory_Transfer\n const result = (await fetchJson(\n `${config.ledgerUrl}/v2/commands/submit-and-wait`,\n config.token,\n {\n method: \"POST\",\n body: JSON.stringify({\n commands: [\n {\n ExerciseCommand: {\n templateId: TRANSFER_FACTORY_TEMPLATE_ID,\n contractId: selectedCids[0],\n choice: \"TransferFactory_Transfer\",\n choiceArgument: {\n sender: config.partyId,\n receiver: challenge.request.recipient,\n amount: challenge.request.amount,\n instrumentId: USDCX_INSTRUMENT_ID,\n inputHoldingCids: selectedCids,\n meta: {},\n },\n },\n },\n ],\n userId: config.userId,\n commandId,\n actAs: [config.partyId],\n readAs: [config.partyId],\n }),\n },\n )) as { updateId: string; completionOffset: number };\n\n // 6. Return serialized credential\n return Credential.serialize({\n challenge,\n payload: {\n updateId: result.updateId,\n completionOffset: result.completionOffset,\n sender: config.partyId,\n commandId,\n },\n });\n },\n });\n}\n","/**\n * Canton MPP method definition.\n *\n * Defines the \"canton\" payment method using the mppx Method.from() pattern.\n * The method specifies the request and credential schemas that both\n * client (agent) and server (gateway) use.\n */\n\nimport { Method } from \"mppx\";\nimport { credentialPayloadSchema, requestSchema } from \"./schemas.js\";\n\nexport const cantonMethod = Method.from({\n intent: \"charge\",\n name: \"canton\",\n schema: {\n credential: {\n payload: credentialPayloadSchema,\n },\n request: requestSchema,\n },\n});\n","/**\n * Zod schemas for Canton MPP payment method.\n * Exported separately for reuse in validation, gateway middleware, etc.\n */\n\nimport { z } from \"zod\";\n\nexport const requestSchema = z.object({\n amount: z.string().regex(/^\\d+\\.?\\d{0,10}$/, \"Invalid amount format\"),\n currency: z.enum([\"USDCx\", \"CC\"]),\n recipient: z.string().min(1, \"Recipient party ID required\"),\n network: z.enum([\"mainnet\", \"testnet\", \"devnet\"]),\n description: z.string().optional(),\n expiry: z.number().int().min(1).max(3600).default(300),\n});\n\nexport const credentialPayloadSchema = z.object({\n updateId: z.string().min(1, \"updateId required\"),\n completionOffset: z.number().int(),\n sender: z.string().min(1, \"Sender party ID required\"),\n commandId: z.string().min(1, \"commandId required\"),\n});\n\nexport const receiptSchema = z.object({\n method: z.literal(\"canton\"),\n reference: z.string().min(1),\n status: z.enum([\"success\", \"failed\"]),\n timestamp: z.string(),\n});\n\nexport type CantonRequest = z.infer<typeof requestSchema>;\nexport type CantonCredentialPayload = z.infer<typeof credentialPayloadSchema>;\nexport type CantonReceipt = z.infer<typeof receiptSchema>;\n"],"mappings":"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","names":["import_mppx"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as mppx from 'mppx';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canton MPP client — used by agents to pay for services.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Receive 402 challenge with amount, recipient, network
|
|
8
|
+
* 2. Validate network matches agent config
|
|
9
|
+
* 3. Query agent's USDCx holdings via Ledger API
|
|
10
|
+
* 4. Select holdings covering the required amount
|
|
11
|
+
* 5. Exercise TransferFactory_Transfer (1-step, requires recipient TransferPreapproval)
|
|
12
|
+
* 6. Return serialized credential with updateId + completionOffset
|
|
13
|
+
*/
|
|
14
|
+
type CantonNetwork = "mainnet" | "testnet" | "devnet";
|
|
15
|
+
interface CantonMppClientConfig {
|
|
16
|
+
ledgerUrl: string;
|
|
17
|
+
token: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
partyId: string;
|
|
20
|
+
network: CantonNetwork;
|
|
21
|
+
}
|
|
22
|
+
declare function cantonClient(config: CantonMppClientConfig): mppx.ClientHandler<unknown>;
|
|
23
|
+
|
|
24
|
+
export { type CantonMppClientConfig, type CantonNetwork, cantonClient };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as mppx from 'mppx';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canton MPP client — used by agents to pay for services.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Receive 402 challenge with amount, recipient, network
|
|
8
|
+
* 2. Validate network matches agent config
|
|
9
|
+
* 3. Query agent's USDCx holdings via Ledger API
|
|
10
|
+
* 4. Select holdings covering the required amount
|
|
11
|
+
* 5. Exercise TransferFactory_Transfer (1-step, requires recipient TransferPreapproval)
|
|
12
|
+
* 6. Return serialized credential with updateId + completionOffset
|
|
13
|
+
*/
|
|
14
|
+
type CantonNetwork = "mainnet" | "testnet" | "devnet";
|
|
15
|
+
interface CantonMppClientConfig {
|
|
16
|
+
ledgerUrl: string;
|
|
17
|
+
token: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
partyId: string;
|
|
20
|
+
network: CantonNetwork;
|
|
21
|
+
}
|
|
22
|
+
declare function cantonClient(config: CantonMppClientConfig): mppx.ClientHandler<unknown>;
|
|
23
|
+
|
|
24
|
+
export { type CantonMppClientConfig, type CantonNetwork, cantonClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MPP_CANTON_VERSION: () => MPP_CANTON_VERSION,
|
|
24
|
+
MppVerificationError: () => MppVerificationError,
|
|
25
|
+
cantonClient: () => cantonClient,
|
|
26
|
+
cantonMethod: () => cantonMethod,
|
|
27
|
+
cantonServer: () => cantonServer,
|
|
28
|
+
credentialPayloadSchema: () => credentialPayloadSchema,
|
|
29
|
+
receiptSchema: () => receiptSchema,
|
|
30
|
+
requestSchema: () => requestSchema
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/method.ts
|
|
35
|
+
var import_mppx = require("mppx");
|
|
36
|
+
|
|
37
|
+
// src/schemas.ts
|
|
38
|
+
var import_zod = require("zod");
|
|
39
|
+
var requestSchema = import_zod.z.object({
|
|
40
|
+
amount: import_zod.z.string().regex(/^\d+\.?\d{0,10}$/, "Invalid amount format"),
|
|
41
|
+
currency: import_zod.z.enum(["USDCx", "CC"]),
|
|
42
|
+
recipient: import_zod.z.string().min(1, "Recipient party ID required"),
|
|
43
|
+
network: import_zod.z.enum(["mainnet", "testnet", "devnet"]),
|
|
44
|
+
description: import_zod.z.string().optional(),
|
|
45
|
+
expiry: import_zod.z.number().int().min(1).max(3600).default(300)
|
|
46
|
+
});
|
|
47
|
+
var credentialPayloadSchema = import_zod.z.object({
|
|
48
|
+
updateId: import_zod.z.string().min(1, "updateId required"),
|
|
49
|
+
completionOffset: import_zod.z.number().int(),
|
|
50
|
+
sender: import_zod.z.string().min(1, "Sender party ID required"),
|
|
51
|
+
commandId: import_zod.z.string().min(1, "commandId required")
|
|
52
|
+
});
|
|
53
|
+
var receiptSchema = import_zod.z.object({
|
|
54
|
+
method: import_zod.z.literal("canton"),
|
|
55
|
+
reference: import_zod.z.string().min(1),
|
|
56
|
+
status: import_zod.z.enum(["success", "failed"]),
|
|
57
|
+
timestamp: import_zod.z.string()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// src/method.ts
|
|
61
|
+
var cantonMethod = import_mppx.Method.from({
|
|
62
|
+
intent: "charge",
|
|
63
|
+
name: "canton",
|
|
64
|
+
schema: {
|
|
65
|
+
credential: {
|
|
66
|
+
payload: credentialPayloadSchema
|
|
67
|
+
},
|
|
68
|
+
request: requestSchema
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// src/client.ts
|
|
73
|
+
var import_mppx2 = require("mppx");
|
|
74
|
+
var USDCX_HOLDING_TEMPLATE_ID = "Splice.Api.Token.HoldingV1:Holding";
|
|
75
|
+
var TRANSFER_FACTORY_TEMPLATE_ID = "Splice.Api.Token.TransferFactoryV1:TransferFactory";
|
|
76
|
+
var USDCX_INSTRUMENT_ID = "USDCx";
|
|
77
|
+
async function fetchJson(url, token, init) {
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
...init,
|
|
80
|
+
headers: {
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
Authorization: `Bearer ${token}`,
|
|
83
|
+
...init?.headers
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const text = await response.text().catch(() => "");
|
|
88
|
+
throw new Error(`Canton API error: HTTP ${response.status} \u2014 ${text}`);
|
|
89
|
+
}
|
|
90
|
+
return response.json();
|
|
91
|
+
}
|
|
92
|
+
async function getLedgerEnd(config) {
|
|
93
|
+
const data = await fetchJson(`${config.ledgerUrl}/v2/state/ledger-end`, config.token);
|
|
94
|
+
return data.offset;
|
|
95
|
+
}
|
|
96
|
+
async function queryHoldings(config) {
|
|
97
|
+
const offset = await getLedgerEnd(config);
|
|
98
|
+
const data = await fetchJson(`${config.ledgerUrl}/v2/state/active-contracts`, config.token, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
eventFormat: {
|
|
102
|
+
filtersByParty: {
|
|
103
|
+
[config.partyId]: {
|
|
104
|
+
cumulative: [
|
|
105
|
+
{
|
|
106
|
+
identifierFilter: {
|
|
107
|
+
TemplateFilter: {
|
|
108
|
+
value: { templateId: USDCX_HOLDING_TEMPLATE_ID }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
verbose: true
|
|
116
|
+
},
|
|
117
|
+
activeAtOffset: offset
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
return (data.contractEntry ?? []).filter((e) => e.createdEvent != null).map((e) => ({
|
|
121
|
+
contractId: e.createdEvent.contractId,
|
|
122
|
+
amount: e.createdEvent.createArgument.amount
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
function selectHoldings(holdings, requiredAmount) {
|
|
126
|
+
if (holdings.length === 0) {
|
|
127
|
+
throw new Error(`Insufficient balance: no holdings available`);
|
|
128
|
+
}
|
|
129
|
+
const sorted = [...holdings].sort(
|
|
130
|
+
(a, b) => parseFloat(b.amount) - parseFloat(a.amount)
|
|
131
|
+
);
|
|
132
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
133
|
+
if (parseFloat(sorted[i].amount) >= parseFloat(requiredAmount)) {
|
|
134
|
+
return [sorted[i].contractId];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let total = 0;
|
|
138
|
+
const selected = [];
|
|
139
|
+
for (const h of sorted) {
|
|
140
|
+
selected.push(h.contractId);
|
|
141
|
+
total += parseFloat(h.amount);
|
|
142
|
+
if (total >= parseFloat(requiredAmount)) {
|
|
143
|
+
return selected;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Insufficient balance: have ${total}, need ${requiredAmount}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
function cantonClient(config) {
|
|
151
|
+
return import_mppx2.Method.toClient(cantonMethod, {
|
|
152
|
+
async createCredential({ challenge }) {
|
|
153
|
+
if (challenge.request.network !== config.network) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Network mismatch: challenge requires ${challenge.request.network}, agent configured for ${config.network}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const holdings = await queryHoldings(config);
|
|
159
|
+
const selectedCids = selectHoldings(holdings, challenge.request.amount);
|
|
160
|
+
const commandId = crypto.randomUUID();
|
|
161
|
+
const result = await fetchJson(
|
|
162
|
+
`${config.ledgerUrl}/v2/commands/submit-and-wait`,
|
|
163
|
+
config.token,
|
|
164
|
+
{
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
commands: [
|
|
168
|
+
{
|
|
169
|
+
ExerciseCommand: {
|
|
170
|
+
templateId: TRANSFER_FACTORY_TEMPLATE_ID,
|
|
171
|
+
contractId: selectedCids[0],
|
|
172
|
+
choice: "TransferFactory_Transfer",
|
|
173
|
+
choiceArgument: {
|
|
174
|
+
sender: config.partyId,
|
|
175
|
+
receiver: challenge.request.recipient,
|
|
176
|
+
amount: challenge.request.amount,
|
|
177
|
+
instrumentId: USDCX_INSTRUMENT_ID,
|
|
178
|
+
inputHoldingCids: selectedCids,
|
|
179
|
+
meta: {}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
userId: config.userId,
|
|
185
|
+
commandId,
|
|
186
|
+
actAs: [config.partyId],
|
|
187
|
+
readAs: [config.partyId]
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
return import_mppx2.Credential.serialize({
|
|
192
|
+
challenge,
|
|
193
|
+
payload: {
|
|
194
|
+
updateId: result.updateId,
|
|
195
|
+
completionOffset: result.completionOffset,
|
|
196
|
+
sender: config.partyId,
|
|
197
|
+
commandId
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/server.ts
|
|
205
|
+
var import_mppx3 = require("mppx");
|
|
206
|
+
var MppVerificationError = class extends Error {
|
|
207
|
+
problemCode;
|
|
208
|
+
constructor(message, problemCode) {
|
|
209
|
+
super(message);
|
|
210
|
+
this.name = "MppVerificationError";
|
|
211
|
+
this.problemCode = problemCode;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function findCreatedHolding(tx, recipientParty) {
|
|
215
|
+
const events = tx.eventsById ?? {};
|
|
216
|
+
for (const evt of Object.values(events)) {
|
|
217
|
+
if (evt.createdEvent) {
|
|
218
|
+
const signatories = evt.createdEvent.signatories ?? [];
|
|
219
|
+
const witnesses = evt.createdEvent.witnessParties ?? [];
|
|
220
|
+
const allParties = [...signatories, ...witnesses];
|
|
221
|
+
if (allParties.includes(recipientParty)) {
|
|
222
|
+
const amount = evt.createdEvent.createArgument?.amount;
|
|
223
|
+
if (typeof amount === "string") {
|
|
224
|
+
return { amount };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
function findExercisedEvent(tx, senderParty) {
|
|
232
|
+
const events = tx.eventsById ?? {};
|
|
233
|
+
for (const evt of Object.values(events)) {
|
|
234
|
+
if (evt.exercisedEvent) {
|
|
235
|
+
const acting = evt.exercisedEvent.actingParties ?? [];
|
|
236
|
+
if (acting.includes(senderParty)) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
function cantonServer(config) {
|
|
244
|
+
return import_mppx3.Method.toServer(cantonMethod, {
|
|
245
|
+
async verify({
|
|
246
|
+
credential
|
|
247
|
+
}) {
|
|
248
|
+
const { updateId, sender } = credential.payload;
|
|
249
|
+
const { amount, recipient, network } = credential.challenge.request;
|
|
250
|
+
if (network !== config.network) {
|
|
251
|
+
throw new MppVerificationError(
|
|
252
|
+
`Network mismatch: credential is for ${network}, server is on ${config.network}`,
|
|
253
|
+
"verification-failed"
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (recipient !== config.recipientPartyId) {
|
|
257
|
+
throw new MppVerificationError(
|
|
258
|
+
`Recipient mismatch: credential targets ${recipient}, server is ${config.recipientPartyId}`,
|
|
259
|
+
"verification-failed"
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const txResponse = await fetch(
|
|
263
|
+
`${config.ledgerUrl}/v2/updates/transaction-by-id/${encodeURIComponent(updateId)}`,
|
|
264
|
+
{
|
|
265
|
+
headers: {
|
|
266
|
+
Authorization: `Bearer ${config.token}`
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
if (!txResponse.ok) {
|
|
271
|
+
throw new MppVerificationError(
|
|
272
|
+
"Transaction not found on Canton ledger",
|
|
273
|
+
"verification-failed"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const tx = await txResponse.json();
|
|
277
|
+
const recipientHolding = findCreatedHolding(tx, config.recipientPartyId);
|
|
278
|
+
if (!recipientHolding) {
|
|
279
|
+
throw new MppVerificationError(
|
|
280
|
+
"No holding created for recipient in transaction",
|
|
281
|
+
"verification-failed"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
if (parseFloat(recipientHolding.amount) < parseFloat(amount)) {
|
|
285
|
+
throw new MppVerificationError(
|
|
286
|
+
`Payment insufficient: received ${recipientHolding.amount}, required ${amount}`,
|
|
287
|
+
"payment-insufficient"
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (!findExercisedEvent(tx, sender)) {
|
|
291
|
+
throw new MppVerificationError(
|
|
292
|
+
`Sender mismatch: ${sender} did not execute the transfer`,
|
|
293
|
+
"verification-failed"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return import_mppx3.Receipt.from({
|
|
297
|
+
method: "canton",
|
|
298
|
+
reference: updateId,
|
|
299
|
+
status: "success",
|
|
300
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/index.ts
|
|
307
|
+
var MPP_CANTON_VERSION = "0.1.0";
|
|
308
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
309
|
+
0 && (module.exports = {
|
|
310
|
+
MPP_CANTON_VERSION,
|
|
311
|
+
MppVerificationError,
|
|
312
|
+
cantonClient,
|
|
313
|
+
cantonMethod,
|
|
314
|
+
cantonServer,
|
|
315
|
+
credentialPayloadSchema,
|
|
316
|
+
receiptSchema,
|
|
317
|
+
requestSchema
|
|
318
|
+
});
|
|
319
|
+
//# sourceMappingURL=index.cjs.map
|