@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
package/src/server.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canton MPP server — used by gateways to verify payments.
|
|
3
|
+
*
|
|
4
|
+
* Verification flow:
|
|
5
|
+
* 1. Check network matches server config
|
|
6
|
+
* 2. Check recipient matches server's party
|
|
7
|
+
* 3. Fetch transaction from ledger by updateId
|
|
8
|
+
* 4. Find Created Holding event for recipient, verify amount >= required
|
|
9
|
+
* 5. Find Exercised event proving the sender executed the transfer
|
|
10
|
+
* 6. Return receipt
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Method, Receipt, type CredentialData } from "mppx";
|
|
14
|
+
import { cantonMethod } from "./method.js";
|
|
15
|
+
import type { CantonCredentialPayload, CantonRequest } from "./schemas.js";
|
|
16
|
+
|
|
17
|
+
export type CantonNetwork = "mainnet" | "testnet" | "devnet";
|
|
18
|
+
|
|
19
|
+
export interface CantonMppServerConfig {
|
|
20
|
+
ledgerUrl: string;
|
|
21
|
+
token: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
recipientPartyId: string;
|
|
24
|
+
network: CantonNetwork;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MppVerificationError extends Error {
|
|
28
|
+
readonly problemCode: string;
|
|
29
|
+
|
|
30
|
+
constructor(message: string, problemCode: string) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "MppVerificationError";
|
|
33
|
+
this.problemCode = problemCode;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface TransactionEvent {
|
|
38
|
+
createdEvent?: {
|
|
39
|
+
contractId: string;
|
|
40
|
+
templateId: string;
|
|
41
|
+
createArgument: Record<string, unknown>;
|
|
42
|
+
witnessParties: string[];
|
|
43
|
+
signatories: string[];
|
|
44
|
+
};
|
|
45
|
+
exercisedEvent?: {
|
|
46
|
+
contractId: string;
|
|
47
|
+
choice: string;
|
|
48
|
+
actingParties: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface TransactionData {
|
|
53
|
+
updateId: string;
|
|
54
|
+
eventsById?: Record<string, TransactionEvent>;
|
|
55
|
+
rootEventIds?: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findCreatedHolding(
|
|
59
|
+
tx: TransactionData,
|
|
60
|
+
recipientParty: string,
|
|
61
|
+
): { amount: string } | null {
|
|
62
|
+
const events = tx.eventsById ?? {};
|
|
63
|
+
|
|
64
|
+
for (const evt of Object.values(events)) {
|
|
65
|
+
if (evt.createdEvent) {
|
|
66
|
+
const signatories = evt.createdEvent.signatories ?? [];
|
|
67
|
+
const witnesses = evt.createdEvent.witnessParties ?? [];
|
|
68
|
+
const allParties = [...signatories, ...witnesses];
|
|
69
|
+
|
|
70
|
+
if (allParties.includes(recipientParty)) {
|
|
71
|
+
const amount = evt.createdEvent.createArgument?.amount;
|
|
72
|
+
if (typeof amount === "string") {
|
|
73
|
+
return { amount };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findExercisedEvent(
|
|
83
|
+
tx: TransactionData,
|
|
84
|
+
senderParty: string,
|
|
85
|
+
): boolean {
|
|
86
|
+
const events = tx.eventsById ?? {};
|
|
87
|
+
|
|
88
|
+
for (const evt of Object.values(events)) {
|
|
89
|
+
if (evt.exercisedEvent) {
|
|
90
|
+
const acting = evt.exercisedEvent.actingParties ?? [];
|
|
91
|
+
if (acting.includes(senderParty)) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function cantonServer(config: CantonMppServerConfig) {
|
|
101
|
+
return Method.toServer(cantonMethod, {
|
|
102
|
+
async verify({
|
|
103
|
+
credential,
|
|
104
|
+
}: {
|
|
105
|
+
credential: CredentialData<CantonCredentialPayload, CantonRequest>;
|
|
106
|
+
}) {
|
|
107
|
+
const { updateId, sender } = credential.payload;
|
|
108
|
+
const { amount, recipient, network } = credential.challenge.request;
|
|
109
|
+
|
|
110
|
+
// 1. Network check
|
|
111
|
+
if (network !== config.network) {
|
|
112
|
+
throw new MppVerificationError(
|
|
113
|
+
`Network mismatch: credential is for ${network}, server is on ${config.network}`,
|
|
114
|
+
"verification-failed",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 2. Recipient check
|
|
119
|
+
if (recipient !== config.recipientPartyId) {
|
|
120
|
+
throw new MppVerificationError(
|
|
121
|
+
`Recipient mismatch: credential targets ${recipient}, server is ${config.recipientPartyId}`,
|
|
122
|
+
"verification-failed",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 3. Fetch transaction by updateId
|
|
127
|
+
const txResponse = await fetch(
|
|
128
|
+
`${config.ledgerUrl}/v2/updates/transaction-by-id/${encodeURIComponent(updateId)}`,
|
|
129
|
+
{
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${config.token}`,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!txResponse.ok) {
|
|
137
|
+
throw new MppVerificationError(
|
|
138
|
+
"Transaction not found on Canton ledger",
|
|
139
|
+
"verification-failed",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const tx = (await txResponse.json()) as TransactionData;
|
|
144
|
+
|
|
145
|
+
// 4. Find created Holding for recipient and verify amount
|
|
146
|
+
const recipientHolding = findCreatedHolding(tx, config.recipientPartyId);
|
|
147
|
+
if (!recipientHolding) {
|
|
148
|
+
throw new MppVerificationError(
|
|
149
|
+
"No holding created for recipient in transaction",
|
|
150
|
+
"verification-failed",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (parseFloat(recipientHolding.amount) < parseFloat(amount)) {
|
|
155
|
+
throw new MppVerificationError(
|
|
156
|
+
`Payment insufficient: received ${recipientHolding.amount}, required ${amount}`,
|
|
157
|
+
"payment-insufficient",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 5. Verify sender
|
|
162
|
+
if (!findExercisedEvent(tx, sender)) {
|
|
163
|
+
throw new MppVerificationError(
|
|
164
|
+
`Sender mismatch: ${sender} did not execute the transfer`,
|
|
165
|
+
"verification-failed",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 6. Return receipt
|
|
170
|
+
return Receipt.from({
|
|
171
|
+
method: "canton",
|
|
172
|
+
reference: updateId,
|
|
173
|
+
status: "success",
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED