@caypo/canton-sdk 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 +26 -0
- package/.turbo/turbo-test.log +23 -0
- package/README.md +120 -0
- package/SPEC.md +223 -0
- package/dist/amount-L2SDLRZT.js +15 -0
- package/dist/amount-L2SDLRZT.js.map +1 -0
- package/dist/chunk-GSDB5FKZ.js +110 -0
- package/dist/chunk-GSDB5FKZ.js.map +1 -0
- package/dist/index.cjs +1158 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +673 -0
- package/dist/index.d.ts +673 -0
- package/dist/index.js +986 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/__tests__/agent.test.ts +217 -0
- package/src/__tests__/amount.test.ts +202 -0
- package/src/__tests__/client.test.ts +516 -0
- package/src/__tests__/e2e/canton-client.e2e.test.ts +190 -0
- package/src/__tests__/e2e/mpp-flow.e2e.test.ts +346 -0
- package/src/__tests__/e2e/setup.ts +112 -0
- package/src/__tests__/e2e/usdcx.e2e.test.ts +114 -0
- package/src/__tests__/keystore.test.ts +197 -0
- package/src/__tests__/pay-client.test.ts +257 -0
- package/src/__tests__/safeguards.test.ts +333 -0
- package/src/__tests__/usdcx.test.ts +374 -0
- package/src/accounts/checking.ts +118 -0
- package/src/agent.ts +132 -0
- package/src/canton/amount.ts +167 -0
- package/src/canton/client.ts +218 -0
- package/src/canton/errors.ts +45 -0
- package/src/canton/holdings.ts +90 -0
- package/src/canton/index.ts +51 -0
- package/src/canton/types.ts +214 -0
- package/src/canton/usdcx.ts +166 -0
- package/src/index.ts +97 -0
- package/src/mpp/pay-client.ts +170 -0
- package/src/safeguards/manager.ts +183 -0
- package/src/traffic/manager.ts +95 -0
- package/src/wallet/config.ts +88 -0
- package/src/wallet/keystore.ts +164 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +7 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1158 @@
|
|
|
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 __esm = (fn, res) => function __init() {
|
|
7
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
8
|
+
};
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
|
+
|
|
23
|
+
// src/canton/amount.ts
|
|
24
|
+
var amount_exports = {};
|
|
25
|
+
__export(amount_exports, {
|
|
26
|
+
addAmounts: () => addAmounts,
|
|
27
|
+
compareAmounts: () => compareAmounts,
|
|
28
|
+
isValidAmount: () => isValidAmount,
|
|
29
|
+
subtractAmounts: () => subtractAmounts,
|
|
30
|
+
toCantonAmount: () => toCantonAmount
|
|
31
|
+
});
|
|
32
|
+
function isValidAmount(s) {
|
|
33
|
+
return AMOUNT_RE.test(s);
|
|
34
|
+
}
|
|
35
|
+
function parse(s) {
|
|
36
|
+
const dot = s.indexOf(".");
|
|
37
|
+
const intPart = dot === -1 ? s : s.slice(0, dot);
|
|
38
|
+
const fracPart = dot === -1 ? "" : s.slice(dot + 1);
|
|
39
|
+
return [
|
|
40
|
+
intPart.split("").map(Number),
|
|
41
|
+
fracPart.split("").map(Number)
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function align(a, b) {
|
|
45
|
+
const [aInt, aFrac] = parse(a);
|
|
46
|
+
const [bInt, bFrac] = parse(b);
|
|
47
|
+
const fracLen = Math.max(aFrac.length, bFrac.length);
|
|
48
|
+
while (aFrac.length < fracLen) aFrac.push(0);
|
|
49
|
+
while (bFrac.length < fracLen) bFrac.push(0);
|
|
50
|
+
const intLen = Math.max(aInt.length, bInt.length);
|
|
51
|
+
while (aInt.length < intLen) aInt.unshift(0);
|
|
52
|
+
while (bInt.length < intLen) bInt.unshift(0);
|
|
53
|
+
return [aInt, aFrac, bInt, bFrac, fracLen];
|
|
54
|
+
}
|
|
55
|
+
function formatResult(intDigits, fracDigits) {
|
|
56
|
+
while (fracDigits.length > 0 && fracDigits[fracDigits.length - 1] === 0) {
|
|
57
|
+
fracDigits.pop();
|
|
58
|
+
}
|
|
59
|
+
while (intDigits.length > 1 && intDigits[0] === 0) {
|
|
60
|
+
intDigits.shift();
|
|
61
|
+
}
|
|
62
|
+
const intStr = intDigits.join("");
|
|
63
|
+
return fracDigits.length > 0 ? `${intStr}.${fracDigits.join("")}` : intStr;
|
|
64
|
+
}
|
|
65
|
+
function compareAmounts(a, b) {
|
|
66
|
+
const [aInt, aFrac, bInt, bFrac] = align(a, b);
|
|
67
|
+
for (let i = 0; i < aInt.length; i++) {
|
|
68
|
+
if (aInt[i] < bInt[i]) return -1;
|
|
69
|
+
if (aInt[i] > bInt[i]) return 1;
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i < aFrac.length; i++) {
|
|
72
|
+
if (aFrac[i] < bFrac[i]) return -1;
|
|
73
|
+
if (aFrac[i] > bFrac[i]) return 1;
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
function addAmounts(a, b) {
|
|
78
|
+
const [aInt, aFrac, bInt, bFrac, fracLen] = align(a, b);
|
|
79
|
+
const aAll = [...aInt, ...aFrac];
|
|
80
|
+
const bAll = [...bInt, ...bFrac];
|
|
81
|
+
const result = new Array(aAll.length).fill(0);
|
|
82
|
+
let carry = 0;
|
|
83
|
+
for (let i = aAll.length - 1; i >= 0; i--) {
|
|
84
|
+
const sum = aAll[i] + bAll[i] + carry;
|
|
85
|
+
result[i] = sum % 10;
|
|
86
|
+
carry = Math.floor(sum / 10);
|
|
87
|
+
}
|
|
88
|
+
if (carry > 0) {
|
|
89
|
+
result.unshift(carry);
|
|
90
|
+
}
|
|
91
|
+
const intDigits = result.slice(0, result.length - fracLen);
|
|
92
|
+
const fracDigits = result.slice(result.length - fracLen);
|
|
93
|
+
return formatResult(
|
|
94
|
+
intDigits.length === 0 ? [0] : intDigits,
|
|
95
|
+
fracDigits
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
function subtractAmounts(a, b) {
|
|
99
|
+
if (compareAmounts(a, b) < 0) {
|
|
100
|
+
throw new Error(`Cannot subtract: ${a} < ${b}`);
|
|
101
|
+
}
|
|
102
|
+
const [aInt, aFrac, bInt, bFrac, fracLen] = align(a, b);
|
|
103
|
+
const aAll = [...aInt, ...aFrac];
|
|
104
|
+
const bAll = [...bInt, ...bFrac];
|
|
105
|
+
const result = new Array(aAll.length).fill(0);
|
|
106
|
+
let borrow = 0;
|
|
107
|
+
for (let i = aAll.length - 1; i >= 0; i--) {
|
|
108
|
+
let diff = aAll[i] - bAll[i] - borrow;
|
|
109
|
+
if (diff < 0) {
|
|
110
|
+
diff += 10;
|
|
111
|
+
borrow = 1;
|
|
112
|
+
} else {
|
|
113
|
+
borrow = 0;
|
|
114
|
+
}
|
|
115
|
+
result[i] = diff;
|
|
116
|
+
}
|
|
117
|
+
const intDigits = result.slice(0, result.length - fracLen);
|
|
118
|
+
const fracDigits = result.slice(result.length - fracLen);
|
|
119
|
+
return formatResult(
|
|
120
|
+
intDigits.length === 0 ? [0] : intDigits,
|
|
121
|
+
fracDigits
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
function toCantonAmount(s, decimals = 10) {
|
|
125
|
+
const dot = s.indexOf(".");
|
|
126
|
+
const intPart = dot === -1 ? s : s.slice(0, dot);
|
|
127
|
+
const fracPart = dot === -1 ? "" : s.slice(dot + 1);
|
|
128
|
+
const padded = fracPart.padEnd(decimals, "0").slice(0, decimals);
|
|
129
|
+
return `${intPart}.${padded}`;
|
|
130
|
+
}
|
|
131
|
+
var AMOUNT_RE;
|
|
132
|
+
var init_amount = __esm({
|
|
133
|
+
"src/canton/amount.ts"() {
|
|
134
|
+
"use strict";
|
|
135
|
+
AMOUNT_RE = /^\d+(\.\d+)?$/;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// src/index.ts
|
|
140
|
+
var index_exports = {};
|
|
141
|
+
__export(index_exports, {
|
|
142
|
+
CANTON_SDK_VERSION: () => CANTON_SDK_VERSION,
|
|
143
|
+
CantonAgent: () => CantonAgent,
|
|
144
|
+
CantonApiError: () => CantonApiError,
|
|
145
|
+
CantonAuthError: () => CantonAuthError,
|
|
146
|
+
CantonClient: () => CantonClient,
|
|
147
|
+
CantonTimeoutError: () => CantonTimeoutError,
|
|
148
|
+
CheckingAccount: () => CheckingAccount,
|
|
149
|
+
DEFAULT_CONFIG: () => DEFAULT_CONFIG,
|
|
150
|
+
DEFAULT_LEDGER_PORT: () => DEFAULT_LEDGER_PORT,
|
|
151
|
+
InsufficientBalanceError: () => InsufficientBalanceError,
|
|
152
|
+
Keystore: () => Keystore,
|
|
153
|
+
MPP_CANTON_VERSION: () => import_mpp_canton.MPP_CANTON_VERSION,
|
|
154
|
+
MppPayClient: () => MppPayClient,
|
|
155
|
+
SafeguardManager: () => SafeguardManager,
|
|
156
|
+
TRANSFER_FACTORY_TEMPLATE_ID: () => TRANSFER_FACTORY_TEMPLATE_ID,
|
|
157
|
+
TrafficManager: () => TrafficManager,
|
|
158
|
+
USDCX_HOLDING_TEMPLATE_ID: () => USDCX_HOLDING_TEMPLATE_ID,
|
|
159
|
+
USDCX_INSTRUMENT_ID: () => USDCX_INSTRUMENT_ID,
|
|
160
|
+
USDCxService: () => USDCxService,
|
|
161
|
+
addAmounts: () => addAmounts,
|
|
162
|
+
compareAmounts: () => compareAmounts,
|
|
163
|
+
isValidAmount: () => isValidAmount,
|
|
164
|
+
loadConfig: () => loadConfig,
|
|
165
|
+
parseWwwAuthenticate: () => parseWwwAuthenticate,
|
|
166
|
+
saveConfig: () => saveConfig,
|
|
167
|
+
selectHoldings: () => selectHoldings,
|
|
168
|
+
subtractAmounts: () => subtractAmounts,
|
|
169
|
+
toCantonAmount: () => toCantonAmount
|
|
170
|
+
});
|
|
171
|
+
module.exports = __toCommonJS(index_exports);
|
|
172
|
+
var import_mpp_canton = require("@caypo/mpp-canton");
|
|
173
|
+
|
|
174
|
+
// src/canton/errors.ts
|
|
175
|
+
var CantonApiError = class extends Error {
|
|
176
|
+
code;
|
|
177
|
+
ledgerCause;
|
|
178
|
+
grpcCodeValue;
|
|
179
|
+
errorCategory;
|
|
180
|
+
context;
|
|
181
|
+
constructor(ledgerError) {
|
|
182
|
+
super(`Canton API error [${ledgerError.code}]: ${ledgerError.cause}`);
|
|
183
|
+
this.name = "CantonApiError";
|
|
184
|
+
this.code = ledgerError.code;
|
|
185
|
+
this.ledgerCause = ledgerError.cause;
|
|
186
|
+
this.grpcCodeValue = ledgerError.grpcCodeValue;
|
|
187
|
+
this.errorCategory = ledgerError.errorCategory;
|
|
188
|
+
this.context = ledgerError.context;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
var CantonTimeoutError = class extends Error {
|
|
192
|
+
timeoutMs;
|
|
193
|
+
path;
|
|
194
|
+
constructor(path, timeoutMs) {
|
|
195
|
+
super(`Canton request to ${path} timed out after ${timeoutMs}ms`);
|
|
196
|
+
this.name = "CantonTimeoutError";
|
|
197
|
+
this.timeoutMs = timeoutMs;
|
|
198
|
+
this.path = path;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
var CantonAuthError = class extends Error {
|
|
202
|
+
statusCode;
|
|
203
|
+
constructor(statusCode, message) {
|
|
204
|
+
super(message ?? `Canton authentication failed (HTTP ${statusCode})`);
|
|
205
|
+
this.name = "CantonAuthError";
|
|
206
|
+
this.statusCode = statusCode;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/canton/client.ts
|
|
211
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
212
|
+
var CantonClient = class {
|
|
213
|
+
ledgerUrl;
|
|
214
|
+
token;
|
|
215
|
+
userId;
|
|
216
|
+
timeout;
|
|
217
|
+
constructor(config) {
|
|
218
|
+
this.ledgerUrl = config.ledgerUrl.replace(/\/+$/, "");
|
|
219
|
+
this.token = config.token;
|
|
220
|
+
this.userId = config.userId;
|
|
221
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
222
|
+
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Command Submission
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
async submitAndWait(params) {
|
|
227
|
+
return this.request("POST", "/v2/commands/submit-and-wait", {
|
|
228
|
+
commands: params.commands,
|
|
229
|
+
userId: this.userId,
|
|
230
|
+
commandId: params.commandId,
|
|
231
|
+
actAs: params.actAs,
|
|
232
|
+
readAs: params.readAs
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async submitAndWaitForTransaction(params) {
|
|
236
|
+
return this.request(
|
|
237
|
+
"POST",
|
|
238
|
+
"/v2/commands/submit-and-wait-for-transaction",
|
|
239
|
+
{
|
|
240
|
+
commands: params.commands,
|
|
241
|
+
userId: this.userId,
|
|
242
|
+
commandId: params.commandId,
|
|
243
|
+
actAs: params.actAs,
|
|
244
|
+
readAs: params.readAs
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Active Contract Queries
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
async queryActiveContracts(params) {
|
|
252
|
+
const body = {
|
|
253
|
+
eventFormat: {
|
|
254
|
+
filtersByParty: params.filtersByParty,
|
|
255
|
+
filtersForAnyParty: params.filtersForAnyParty,
|
|
256
|
+
verbose: true
|
|
257
|
+
},
|
|
258
|
+
activeAtOffset: params.activeAtOffset
|
|
259
|
+
};
|
|
260
|
+
const response = await this.request("POST", "/v2/state/active-contracts", body);
|
|
261
|
+
if (!response.contractEntry) {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
return response.contractEntry.filter((entry) => entry.createdEvent != null).map((entry) => {
|
|
265
|
+
const evt = entry.createdEvent;
|
|
266
|
+
return {
|
|
267
|
+
contractId: evt.contractId,
|
|
268
|
+
templateId: evt.templateId,
|
|
269
|
+
createArgument: evt.createArgument,
|
|
270
|
+
createdAt: "",
|
|
271
|
+
signatories: evt.signatories,
|
|
272
|
+
observers: evt.observers
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Transaction Lookup
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
async getTransactionById(updateId) {
|
|
280
|
+
try {
|
|
281
|
+
return await this.request(
|
|
282
|
+
"GET",
|
|
283
|
+
`/v2/updates/transaction-by-id/${encodeURIComponent(updateId)}`
|
|
284
|
+
);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err instanceof CantonApiError && err.code === "NOT_FOUND") {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// Ledger State
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
async getLedgerEnd() {
|
|
296
|
+
const response = await this.request("GET", "/v2/state/ledger-end");
|
|
297
|
+
return response.offset;
|
|
298
|
+
}
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Party Management
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
async allocateParty(hint) {
|
|
303
|
+
const response = await this.request("POST", "/v2/parties", {
|
|
304
|
+
partyIdHint: hint,
|
|
305
|
+
identityProviderId: ""
|
|
306
|
+
});
|
|
307
|
+
return response.partyDetails;
|
|
308
|
+
}
|
|
309
|
+
async listParties() {
|
|
310
|
+
const response = await this.request("GET", "/v2/parties");
|
|
311
|
+
return response.partyDetails;
|
|
312
|
+
}
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Health Check
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
async isHealthy() {
|
|
317
|
+
try {
|
|
318
|
+
const response = await fetch(`${this.ledgerUrl}/livez`, {
|
|
319
|
+
method: "GET",
|
|
320
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
321
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
322
|
+
});
|
|
323
|
+
return response.ok;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
// Internal
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
async request(method, path, body) {
|
|
332
|
+
const url = `${this.ledgerUrl}${path}`;
|
|
333
|
+
const headers = {
|
|
334
|
+
Authorization: `Bearer ${this.token}`
|
|
335
|
+
};
|
|
336
|
+
if (body !== void 0) {
|
|
337
|
+
headers["Content-Type"] = "application/json";
|
|
338
|
+
}
|
|
339
|
+
let response;
|
|
340
|
+
try {
|
|
341
|
+
response = await fetch(url, {
|
|
342
|
+
method,
|
|
343
|
+
headers,
|
|
344
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
345
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
346
|
+
});
|
|
347
|
+
} catch (err) {
|
|
348
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
349
|
+
throw new CantonTimeoutError(path, this.timeout);
|
|
350
|
+
}
|
|
351
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
352
|
+
throw new CantonTimeoutError(path, this.timeout);
|
|
353
|
+
}
|
|
354
|
+
throw err;
|
|
355
|
+
}
|
|
356
|
+
if (response.status === 401 || response.status === 403) {
|
|
357
|
+
const text2 = await response.text().catch(() => "");
|
|
358
|
+
throw new CantonAuthError(response.status, text2 || void 0);
|
|
359
|
+
}
|
|
360
|
+
if (!response.ok) {
|
|
361
|
+
const errorBody = await response.json().catch(() => null);
|
|
362
|
+
if (errorBody && typeof errorBody === "object" && "code" in errorBody) {
|
|
363
|
+
throw new CantonApiError(errorBody);
|
|
364
|
+
}
|
|
365
|
+
throw new Error(`Canton API error: HTTP ${response.status} on ${method} ${path}`);
|
|
366
|
+
}
|
|
367
|
+
const text = await response.text();
|
|
368
|
+
if (!text) {
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
return JSON.parse(text);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/canton/usdcx.ts
|
|
376
|
+
init_amount();
|
|
377
|
+
|
|
378
|
+
// src/canton/holdings.ts
|
|
379
|
+
init_amount();
|
|
380
|
+
var InsufficientBalanceError = class extends Error {
|
|
381
|
+
available;
|
|
382
|
+
required;
|
|
383
|
+
constructor(available, required) {
|
|
384
|
+
super(`Insufficient balance: have ${available}, need ${required}`);
|
|
385
|
+
this.name = "InsufficientBalanceError";
|
|
386
|
+
this.available = available;
|
|
387
|
+
this.required = required;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
function selectHoldings(holdings, requiredAmount) {
|
|
391
|
+
if (holdings.length === 0) {
|
|
392
|
+
throw new InsufficientBalanceError("0", requiredAmount);
|
|
393
|
+
}
|
|
394
|
+
const sorted = [...holdings].sort((a, b) => compareAmounts(b.amount, a.amount));
|
|
395
|
+
let bestSingle = null;
|
|
396
|
+
for (const h of sorted) {
|
|
397
|
+
if (compareAmounts(h.amount, requiredAmount) >= 0) {
|
|
398
|
+
bestSingle = h;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (bestSingle) {
|
|
402
|
+
return {
|
|
403
|
+
type: "single",
|
|
404
|
+
contractIds: [bestSingle.contractId]
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
let accumulated = "0";
|
|
408
|
+
const selected = [];
|
|
409
|
+
for (const h of sorted) {
|
|
410
|
+
selected.push(h.contractId);
|
|
411
|
+
accumulated = addAmounts(accumulated, h.amount);
|
|
412
|
+
if (compareAmounts(accumulated, requiredAmount) >= 0) {
|
|
413
|
+
return {
|
|
414
|
+
type: "merge-then-transfer",
|
|
415
|
+
contractIds: selected
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
throw new InsufficientBalanceError(accumulated, requiredAmount);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/canton/usdcx.ts
|
|
423
|
+
var USDCX_HOLDING_TEMPLATE_ID = "Splice.Api.Token.HoldingV1:Holding";
|
|
424
|
+
var TRANSFER_FACTORY_TEMPLATE_ID = "Splice.Api.Token.TransferFactoryV1:TransferFactory";
|
|
425
|
+
var USDCX_INSTRUMENT_ID = "USDCx";
|
|
426
|
+
var USDCxService = class {
|
|
427
|
+
constructor(client, partyId) {
|
|
428
|
+
this.client = client;
|
|
429
|
+
this.partyId = partyId;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Query all USDCx Holding contracts for this party.
|
|
433
|
+
*/
|
|
434
|
+
async getHoldings() {
|
|
435
|
+
const offset = await this.client.getLedgerEnd();
|
|
436
|
+
const contracts = await this.client.queryActiveContracts({
|
|
437
|
+
filtersByParty: {
|
|
438
|
+
[this.partyId]: {
|
|
439
|
+
cumulative: [
|
|
440
|
+
{
|
|
441
|
+
identifierFilter: {
|
|
442
|
+
TemplateFilter: {
|
|
443
|
+
value: { templateId: USDCX_HOLDING_TEMPLATE_ID }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
activeAtOffset: offset
|
|
451
|
+
});
|
|
452
|
+
return contracts.map((c) => ({
|
|
453
|
+
contractId: c.contractId,
|
|
454
|
+
owner: c.createArgument.owner ?? this.partyId,
|
|
455
|
+
amount: c.createArgument.amount,
|
|
456
|
+
templateId: c.templateId
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Calculate total USDCx balance by summing all holding amounts.
|
|
461
|
+
* Returns a string with up to 10 decimal places.
|
|
462
|
+
*/
|
|
463
|
+
async getBalance() {
|
|
464
|
+
const holdings = await this.getHoldings();
|
|
465
|
+
if (holdings.length === 0) {
|
|
466
|
+
return "0";
|
|
467
|
+
}
|
|
468
|
+
let total = "0";
|
|
469
|
+
for (const h of holdings) {
|
|
470
|
+
total = addAmounts(total, h.amount);
|
|
471
|
+
}
|
|
472
|
+
return total;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Transfer USDCx using TransferFactory_Transfer (1-step).
|
|
476
|
+
* Requires the recipient to have an active TransferPreapproval.
|
|
477
|
+
*/
|
|
478
|
+
async transfer(params) {
|
|
479
|
+
const commandId = params.commandId ?? crypto.randomUUID();
|
|
480
|
+
const amount = toCantonAmount(params.amount);
|
|
481
|
+
const holdings = await this.getHoldings();
|
|
482
|
+
const selection = selectHoldings(holdings, params.amount);
|
|
483
|
+
const result = await this.client.submitAndWait({
|
|
484
|
+
commands: [
|
|
485
|
+
{
|
|
486
|
+
ExerciseCommand: {
|
|
487
|
+
templateId: TRANSFER_FACTORY_TEMPLATE_ID,
|
|
488
|
+
contractId: selection.contractIds[0],
|
|
489
|
+
choice: "TransferFactory_Transfer",
|
|
490
|
+
choiceArgument: {
|
|
491
|
+
sender: this.partyId,
|
|
492
|
+
receiver: params.recipient,
|
|
493
|
+
amount,
|
|
494
|
+
instrumentId: USDCX_INSTRUMENT_ID,
|
|
495
|
+
inputHoldingCids: selection.contractIds,
|
|
496
|
+
meta: {}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
],
|
|
501
|
+
commandId,
|
|
502
|
+
actAs: [this.partyId],
|
|
503
|
+
readAs: [this.partyId]
|
|
504
|
+
});
|
|
505
|
+
return {
|
|
506
|
+
updateId: result.updateId,
|
|
507
|
+
completionOffset: result.completionOffset,
|
|
508
|
+
commandId
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Merge multiple holdings into fewer UTXOs.
|
|
513
|
+
* Returns the commandId of the merge transaction.
|
|
514
|
+
*/
|
|
515
|
+
async mergeHoldings(holdingCids) {
|
|
516
|
+
if (holdingCids.length < 2) {
|
|
517
|
+
throw new Error("Need at least 2 holdings to merge");
|
|
518
|
+
}
|
|
519
|
+
const commandId = crypto.randomUUID();
|
|
520
|
+
await this.client.submitAndWait({
|
|
521
|
+
commands: [
|
|
522
|
+
{
|
|
523
|
+
ExerciseCommand: {
|
|
524
|
+
templateId: USDCX_HOLDING_TEMPLATE_ID,
|
|
525
|
+
contractId: holdingCids[0],
|
|
526
|
+
choice: "Merge",
|
|
527
|
+
choiceArgument: {
|
|
528
|
+
holdingCids: holdingCids.slice(1)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
commandId,
|
|
534
|
+
actAs: [this.partyId]
|
|
535
|
+
});
|
|
536
|
+
return commandId;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/index.ts
|
|
541
|
+
init_amount();
|
|
542
|
+
|
|
543
|
+
// src/wallet/keystore.ts
|
|
544
|
+
var import_node_crypto = require("crypto");
|
|
545
|
+
var import_promises = require("fs/promises");
|
|
546
|
+
var import_node_path = require("path");
|
|
547
|
+
var import_node_os = require("os");
|
|
548
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
549
|
+
var PBKDF2_KEYLEN = 32;
|
|
550
|
+
var PBKDF2_DIGEST = "sha256";
|
|
551
|
+
var SALT_LEN = 32;
|
|
552
|
+
var IV_LEN = 16;
|
|
553
|
+
var ALGORITHM = "aes-256-gcm";
|
|
554
|
+
var DEFAULT_WALLET_PATH = (0, import_node_path.join)((0, import_node_os.homedir)(), ".caypo", "wallet.key");
|
|
555
|
+
function deriveKey(pin, salt) {
|
|
556
|
+
return (0, import_node_crypto.pbkdf2Sync)(pin, salt, PBKDF2_ITERATIONS, PBKDF2_KEYLEN, PBKDF2_DIGEST);
|
|
557
|
+
}
|
|
558
|
+
function encrypt(data, pin) {
|
|
559
|
+
const salt = (0, import_node_crypto.randomBytes)(SALT_LEN);
|
|
560
|
+
const key = deriveKey(pin, salt);
|
|
561
|
+
const iv = (0, import_node_crypto.randomBytes)(IV_LEN);
|
|
562
|
+
const cipher = (0, import_node_crypto.createCipheriv)(ALGORITHM, key, iv);
|
|
563
|
+
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
564
|
+
const tag = cipher.getAuthTag();
|
|
565
|
+
return {
|
|
566
|
+
iv: iv.toString("base64"),
|
|
567
|
+
salt: salt.toString("base64"),
|
|
568
|
+
encrypted: encrypted.toString("base64"),
|
|
569
|
+
tag: tag.toString("base64")
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function decrypt(file, pin) {
|
|
573
|
+
const salt = Buffer.from(file.salt, "base64");
|
|
574
|
+
const iv = Buffer.from(file.iv, "base64");
|
|
575
|
+
const encrypted = Buffer.from(file.encrypted, "base64");
|
|
576
|
+
const tag = Buffer.from(file.tag, "base64");
|
|
577
|
+
const key = deriveKey(pin, salt);
|
|
578
|
+
const decipher = (0, import_node_crypto.createDecipheriv)(ALGORITHM, key, iv);
|
|
579
|
+
decipher.setAuthTag(tag);
|
|
580
|
+
try {
|
|
581
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
582
|
+
return decrypted.toString("utf8");
|
|
583
|
+
} catch {
|
|
584
|
+
throw new Error("Invalid PIN or corrupted wallet file");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
var Keystore = class _Keystore {
|
|
588
|
+
data;
|
|
589
|
+
filePath;
|
|
590
|
+
constructor(data, filePath) {
|
|
591
|
+
this.data = data;
|
|
592
|
+
this.filePath = filePath;
|
|
593
|
+
}
|
|
594
|
+
/** Party ID of this wallet. */
|
|
595
|
+
get address() {
|
|
596
|
+
return this.data.partyId;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Create a new encrypted wallet.
|
|
600
|
+
* Generates a random 32-byte private key and saves encrypted to disk.
|
|
601
|
+
*/
|
|
602
|
+
static async create(pin, params, path) {
|
|
603
|
+
const filePath = path ?? DEFAULT_WALLET_PATH;
|
|
604
|
+
const walletData = {
|
|
605
|
+
partyId: params.partyId,
|
|
606
|
+
jwt: params.jwt,
|
|
607
|
+
userId: params.userId,
|
|
608
|
+
privateKey: (0, import_node_crypto.randomBytes)(32).toString("hex")
|
|
609
|
+
};
|
|
610
|
+
const encryptedFile = encrypt(JSON.stringify(walletData), pin);
|
|
611
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(filePath), { recursive: true });
|
|
612
|
+
await (0, import_promises.writeFile)(filePath, JSON.stringify(encryptedFile), "utf8");
|
|
613
|
+
return new _Keystore(walletData, filePath);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Load and decrypt an existing wallet.
|
|
617
|
+
* Throws if the PIN is wrong or the file is corrupted.
|
|
618
|
+
*/
|
|
619
|
+
static async load(pin, path) {
|
|
620
|
+
const filePath = path ?? DEFAULT_WALLET_PATH;
|
|
621
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf8");
|
|
622
|
+
const encryptedFile = JSON.parse(raw);
|
|
623
|
+
const decrypted = decrypt(encryptedFile, pin);
|
|
624
|
+
const walletData = JSON.parse(decrypted);
|
|
625
|
+
return new _Keystore(walletData, filePath);
|
|
626
|
+
}
|
|
627
|
+
/** Get credentials for Canton Ledger API access. */
|
|
628
|
+
getCredentials() {
|
|
629
|
+
return {
|
|
630
|
+
partyId: this.data.partyId,
|
|
631
|
+
jwt: this.data.jwt,
|
|
632
|
+
userId: this.data.userId
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/** Change the encryption PIN. Re-encrypts wallet data with new PIN. */
|
|
636
|
+
async changePin(oldPin, newPin) {
|
|
637
|
+
const raw = await (0, import_promises.readFile)(this.filePath, "utf8");
|
|
638
|
+
const encryptedFile = JSON.parse(raw);
|
|
639
|
+
decrypt(encryptedFile, oldPin);
|
|
640
|
+
const newEncrypted = encrypt(JSON.stringify(this.data), newPin);
|
|
641
|
+
await (0, import_promises.writeFile)(this.filePath, JSON.stringify(newEncrypted), "utf8");
|
|
642
|
+
}
|
|
643
|
+
/** Export the raw private key. Dangerous — only call with explicit user consent. */
|
|
644
|
+
exportKey(pin) {
|
|
645
|
+
void pin;
|
|
646
|
+
return this.data.privateKey;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/wallet/config.ts
|
|
651
|
+
var import_promises2 = require("fs/promises");
|
|
652
|
+
var import_node_path2 = require("path");
|
|
653
|
+
var import_node_os2 = require("os");
|
|
654
|
+
var DEFAULT_CONFIG_PATH = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".caypo", "config.json");
|
|
655
|
+
var DEFAULT_CONFIG = {
|
|
656
|
+
version: 2,
|
|
657
|
+
network: "testnet",
|
|
658
|
+
ledgerUrl: "http://localhost:7575",
|
|
659
|
+
partyId: "",
|
|
660
|
+
userId: "ledger-api-user",
|
|
661
|
+
keystorePath: (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".caypo", "wallet.key"),
|
|
662
|
+
traffic: {
|
|
663
|
+
autoPurchase: true,
|
|
664
|
+
minBalance: 1e3,
|
|
665
|
+
purchaseAmountCC: "5.0"
|
|
666
|
+
},
|
|
667
|
+
safeguards: {
|
|
668
|
+
txLimit: "100",
|
|
669
|
+
dailyLimit: "1000"
|
|
670
|
+
},
|
|
671
|
+
mpp: {
|
|
672
|
+
gatewayUrl: "https://mpp.cayvox.io",
|
|
673
|
+
maxAutoPayPrice: "1.00"
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
async function loadConfig(path) {
|
|
677
|
+
const filePath = path ?? DEFAULT_CONFIG_PATH;
|
|
678
|
+
try {
|
|
679
|
+
const raw = await (0, import_promises2.readFile)(filePath, "utf8");
|
|
680
|
+
const parsed = JSON.parse(raw);
|
|
681
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
682
|
+
} catch (err) {
|
|
683
|
+
if (err.code === "ENOENT") {
|
|
684
|
+
return { ...DEFAULT_CONFIG };
|
|
685
|
+
}
|
|
686
|
+
throw err;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
async function saveConfig(config, path) {
|
|
690
|
+
const filePath = path ?? DEFAULT_CONFIG_PATH;
|
|
691
|
+
await (0, import_promises2.mkdir)((0, import_node_path2.dirname)(filePath), { recursive: true });
|
|
692
|
+
await (0, import_promises2.writeFile)(filePath, JSON.stringify(config, null, 2), "utf8");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/safeguards/manager.ts
|
|
696
|
+
var import_promises3 = require("fs/promises");
|
|
697
|
+
var import_node_path3 = require("path");
|
|
698
|
+
var import_node_os3 = require("os");
|
|
699
|
+
init_amount();
|
|
700
|
+
var DEFAULT_SAFEGUARDS_PATH = (0, import_node_path3.join)((0, import_node_os3.homedir)(), ".caypo", "safeguards.json");
|
|
701
|
+
var DEFAULT_SAFEGUARD_CONFIG = {
|
|
702
|
+
txLimit: "100",
|
|
703
|
+
dailyLimit: "1000",
|
|
704
|
+
locked: false,
|
|
705
|
+
lockedPinHash: "",
|
|
706
|
+
dailySpent: "0",
|
|
707
|
+
lastResetDate: today()
|
|
708
|
+
};
|
|
709
|
+
function today() {
|
|
710
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
711
|
+
}
|
|
712
|
+
function hashPin(pin) {
|
|
713
|
+
let hash = 0;
|
|
714
|
+
for (let i = 0; i < pin.length; i++) {
|
|
715
|
+
const ch = pin.charCodeAt(i);
|
|
716
|
+
hash = (hash << 5) - hash + ch | 0;
|
|
717
|
+
}
|
|
718
|
+
return String(hash);
|
|
719
|
+
}
|
|
720
|
+
var SafeguardManager = class _SafeguardManager {
|
|
721
|
+
config;
|
|
722
|
+
filePath;
|
|
723
|
+
constructor(config, filePath) {
|
|
724
|
+
this.config = config ? { ...config } : { ...DEFAULT_SAFEGUARD_CONFIG };
|
|
725
|
+
this.filePath = filePath ?? DEFAULT_SAFEGUARDS_PATH;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Load safeguards from disk. Returns a new SafeguardManager.
|
|
729
|
+
* If file doesn't exist, uses defaults.
|
|
730
|
+
*/
|
|
731
|
+
static async load(path) {
|
|
732
|
+
const filePath = path ?? DEFAULT_SAFEGUARDS_PATH;
|
|
733
|
+
try {
|
|
734
|
+
const raw = await (0, import_promises3.readFile)(filePath, "utf8");
|
|
735
|
+
const parsed = JSON.parse(raw);
|
|
736
|
+
const config = { ...DEFAULT_SAFEGUARD_CONFIG, ...parsed };
|
|
737
|
+
return new _SafeguardManager(config, filePath);
|
|
738
|
+
} catch (err) {
|
|
739
|
+
if (err.code === "ENOENT") {
|
|
740
|
+
return new _SafeguardManager(void 0, filePath);
|
|
741
|
+
}
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/** Get current safeguard settings. */
|
|
746
|
+
settings() {
|
|
747
|
+
return { ...this.config };
|
|
748
|
+
}
|
|
749
|
+
/** Set per-transaction limit. */
|
|
750
|
+
setTxLimit(amount) {
|
|
751
|
+
this.config.txLimit = amount;
|
|
752
|
+
void this.save();
|
|
753
|
+
}
|
|
754
|
+
/** Set daily spending limit. */
|
|
755
|
+
setDailyLimit(amount) {
|
|
756
|
+
this.config.dailyLimit = amount;
|
|
757
|
+
void this.save();
|
|
758
|
+
}
|
|
759
|
+
/** Lock the wallet. All transactions will be rejected until unlocked. */
|
|
760
|
+
lock(pin) {
|
|
761
|
+
this.config.locked = true;
|
|
762
|
+
if (pin) {
|
|
763
|
+
this.config.lockedPinHash = hashPin(pin);
|
|
764
|
+
}
|
|
765
|
+
void this.save();
|
|
766
|
+
}
|
|
767
|
+
/** Unlock the wallet. Requires PIN if one was set during lock. */
|
|
768
|
+
unlock(pin) {
|
|
769
|
+
if (this.config.lockedPinHash && hashPin(pin) !== this.config.lockedPinHash) {
|
|
770
|
+
throw new Error("Invalid PIN");
|
|
771
|
+
}
|
|
772
|
+
this.config.locked = false;
|
|
773
|
+
this.config.lockedPinHash = "";
|
|
774
|
+
void this.save();
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Check if a transaction for the given amount is allowed.
|
|
778
|
+
* Auto-resets daily counter if the date has changed.
|
|
779
|
+
*/
|
|
780
|
+
check(amount) {
|
|
781
|
+
this.autoResetDaily();
|
|
782
|
+
const dailyRemaining = subtractAmounts(this.config.dailyLimit, this.config.dailySpent);
|
|
783
|
+
if (this.config.locked) {
|
|
784
|
+
return { allowed: false, reason: "Wallet is locked", dailyRemaining };
|
|
785
|
+
}
|
|
786
|
+
if (compareAmounts(amount, this.config.txLimit) > 0) {
|
|
787
|
+
return {
|
|
788
|
+
allowed: false,
|
|
789
|
+
reason: `Amount ${amount} exceeds per-transaction limit of ${this.config.txLimit}`,
|
|
790
|
+
dailyRemaining
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
const projectedDaily = addAmounts(this.config.dailySpent, amount);
|
|
794
|
+
if (compareAmounts(projectedDaily, this.config.dailyLimit) > 0) {
|
|
795
|
+
return {
|
|
796
|
+
allowed: false,
|
|
797
|
+
reason: `Amount ${amount} would exceed daily limit of ${this.config.dailyLimit} (spent: ${this.config.dailySpent})`,
|
|
798
|
+
dailyRemaining
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
return { allowed: true, dailyRemaining };
|
|
802
|
+
}
|
|
803
|
+
/** Record a completed spend. Call after successful transaction. */
|
|
804
|
+
recordSpend(amount) {
|
|
805
|
+
this.autoResetDaily();
|
|
806
|
+
this.config.dailySpent = addAmounts(this.config.dailySpent, amount);
|
|
807
|
+
void this.save();
|
|
808
|
+
}
|
|
809
|
+
/** Manually reset the daily counter. */
|
|
810
|
+
resetDaily() {
|
|
811
|
+
this.config.dailySpent = "0";
|
|
812
|
+
this.config.lastResetDate = today();
|
|
813
|
+
void this.save();
|
|
814
|
+
}
|
|
815
|
+
autoResetDaily() {
|
|
816
|
+
const currentDate = today();
|
|
817
|
+
if (this.config.lastResetDate !== currentDate) {
|
|
818
|
+
this.config.dailySpent = "0";
|
|
819
|
+
this.config.lastResetDate = currentDate;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
async save() {
|
|
823
|
+
try {
|
|
824
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(this.filePath), { recursive: true });
|
|
825
|
+
await (0, import_promises3.writeFile)(this.filePath, JSON.stringify(this.config, null, 2), "utf8");
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// src/accounts/checking.ts
|
|
832
|
+
var CheckingAccount = class {
|
|
833
|
+
constructor(usdcx, safeguards, client, partyId) {
|
|
834
|
+
this.usdcx = usdcx;
|
|
835
|
+
this.safeguards = safeguards;
|
|
836
|
+
this.client = client;
|
|
837
|
+
this.partyId = partyId;
|
|
838
|
+
}
|
|
839
|
+
/** Get USDCx balance: total available and number of UTXO holdings. */
|
|
840
|
+
async balance() {
|
|
841
|
+
const holdings = await this.usdcx.getHoldings();
|
|
842
|
+
let available = "0";
|
|
843
|
+
const { addAmounts: addAmounts2 } = await Promise.resolve().then(() => (init_amount(), amount_exports));
|
|
844
|
+
for (const h of holdings) {
|
|
845
|
+
available = addAmounts2(available, h.amount);
|
|
846
|
+
}
|
|
847
|
+
return { available, holdingCount: holdings.length };
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Send USDCx to a recipient.
|
|
851
|
+
* Checks safeguards before executing the transfer.
|
|
852
|
+
*/
|
|
853
|
+
async send(recipient, amount, opts) {
|
|
854
|
+
const check = this.safeguards.check(amount);
|
|
855
|
+
if (!check.allowed) {
|
|
856
|
+
throw new Error(`Safeguard rejected: ${check.reason}`);
|
|
857
|
+
}
|
|
858
|
+
const result = await this.usdcx.transfer({
|
|
859
|
+
recipient,
|
|
860
|
+
amount,
|
|
861
|
+
commandId: opts?.commandId
|
|
862
|
+
});
|
|
863
|
+
this.safeguards.recordSpend(amount);
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
/** Party ID for receiving payments. */
|
|
867
|
+
address() {
|
|
868
|
+
return this.partyId;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Query transaction history via /v2/updates/flats.
|
|
872
|
+
* Returns recent flat transactions involving this party.
|
|
873
|
+
*/
|
|
874
|
+
async history(opts) {
|
|
875
|
+
const limit = opts?.limit ?? 20;
|
|
876
|
+
const offset = await this.client.getLedgerEnd();
|
|
877
|
+
const contracts = await this.client.queryActiveContracts({
|
|
878
|
+
filtersByParty: {
|
|
879
|
+
[this.partyId]: {
|
|
880
|
+
cumulative: [
|
|
881
|
+
{
|
|
882
|
+
identifierFilter: {
|
|
883
|
+
WildcardFilter: { value: { includeCreatedEventBlob: false } }
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
]
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
activeAtOffset: offset
|
|
890
|
+
});
|
|
891
|
+
return contracts.slice(0, limit).map((c) => ({
|
|
892
|
+
updateId: "",
|
|
893
|
+
commandId: "",
|
|
894
|
+
effectiveAt: c.createdAt,
|
|
895
|
+
offset: 0,
|
|
896
|
+
type: "unknown",
|
|
897
|
+
amount: typeof c.createArgument.amount === "string" ? c.createArgument.amount : void 0
|
|
898
|
+
}));
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/mpp/pay-client.ts
|
|
903
|
+
init_amount();
|
|
904
|
+
function parseWwwAuthenticate(header) {
|
|
905
|
+
if (!header.startsWith("Payment")) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
const params = {};
|
|
909
|
+
const re = /(\w+)="([^"]*)"/g;
|
|
910
|
+
let match;
|
|
911
|
+
while ((match = re.exec(header)) !== null) {
|
|
912
|
+
params[match[1]] = match[2];
|
|
913
|
+
}
|
|
914
|
+
if (params.method !== "canton" || !params.amount || !params.recipient || !params.network) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
amount: params.amount,
|
|
919
|
+
currency: params.currency ?? "USDCx",
|
|
920
|
+
recipient: params.recipient,
|
|
921
|
+
network: params.network,
|
|
922
|
+
description: params.description
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
var MppPayClient = class {
|
|
926
|
+
constructor(usdcx, safeguards, partyId, network) {
|
|
927
|
+
this.usdcx = usdcx;
|
|
928
|
+
this.safeguards = safeguards;
|
|
929
|
+
this.partyId = partyId;
|
|
930
|
+
this.network = network;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Pay for an API call via MPP 402 flow.
|
|
934
|
+
* If the response is not 402, returns it as-is with paid=false.
|
|
935
|
+
*/
|
|
936
|
+
async pay(url, opts) {
|
|
937
|
+
const requestInit = {
|
|
938
|
+
method: opts?.method ?? "GET",
|
|
939
|
+
headers: opts?.headers,
|
|
940
|
+
body: opts?.body
|
|
941
|
+
};
|
|
942
|
+
const firstResponse = await fetch(url, requestInit);
|
|
943
|
+
if (firstResponse.status !== 402) {
|
|
944
|
+
return { response: firstResponse, paid: false };
|
|
945
|
+
}
|
|
946
|
+
const authHeader = firstResponse.headers.get("WWW-Authenticate") ?? "";
|
|
947
|
+
const challenge = parseWwwAuthenticate(authHeader);
|
|
948
|
+
if (!challenge) {
|
|
949
|
+
throw new Error("402 received but no valid Canton payment challenge in WWW-Authenticate");
|
|
950
|
+
}
|
|
951
|
+
if (challenge.network !== this.network) {
|
|
952
|
+
throw new Error(
|
|
953
|
+
`Network mismatch: challenge requires ${challenge.network}, agent on ${this.network}`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (opts?.maxPrice && compareAmounts(challenge.amount, opts.maxPrice) > 0) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`Price ${challenge.amount} exceeds maxPrice ${opts.maxPrice}`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
const check = this.safeguards.check(challenge.amount);
|
|
962
|
+
if (!check.allowed) {
|
|
963
|
+
throw new Error(`Safeguard rejected: ${check.reason}`);
|
|
964
|
+
}
|
|
965
|
+
const transferResult = await this.usdcx.transfer({
|
|
966
|
+
recipient: challenge.recipient,
|
|
967
|
+
amount: challenge.amount
|
|
968
|
+
});
|
|
969
|
+
this.safeguards.recordSpend(challenge.amount);
|
|
970
|
+
const credential = Buffer.from(
|
|
971
|
+
JSON.stringify({
|
|
972
|
+
updateId: transferResult.updateId,
|
|
973
|
+
completionOffset: transferResult.completionOffset,
|
|
974
|
+
sender: this.partyId,
|
|
975
|
+
commandId: transferResult.commandId
|
|
976
|
+
})
|
|
977
|
+
).toString("base64");
|
|
978
|
+
const retryResponse = await fetch(url, {
|
|
979
|
+
...requestInit,
|
|
980
|
+
headers: {
|
|
981
|
+
...opts?.headers,
|
|
982
|
+
Authorization: `Payment ${credential}`
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
return {
|
|
986
|
+
response: retryResponse,
|
|
987
|
+
paid: true,
|
|
988
|
+
receipt: {
|
|
989
|
+
updateId: transferResult.updateId,
|
|
990
|
+
completionOffset: transferResult.completionOffset,
|
|
991
|
+
commandId: transferResult.commandId,
|
|
992
|
+
amount: challenge.amount
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
// src/traffic/manager.ts
|
|
999
|
+
var TrafficManager = class {
|
|
1000
|
+
constructor(client, partyId) {
|
|
1001
|
+
this.client = client;
|
|
1002
|
+
this.partyId = partyId;
|
|
1003
|
+
}
|
|
1004
|
+
autoPurchaseConfig = {
|
|
1005
|
+
enabled: false,
|
|
1006
|
+
minBalance: 1e3,
|
|
1007
|
+
purchaseAmount: "5.0"
|
|
1008
|
+
};
|
|
1009
|
+
/**
|
|
1010
|
+
* Check validator's traffic balance.
|
|
1011
|
+
*
|
|
1012
|
+
* TODO: Implement using actual validator admin API.
|
|
1013
|
+
* The traffic balance is a validator-level concept, not per-party.
|
|
1014
|
+
* For now, returns a stub indicating sufficient traffic.
|
|
1015
|
+
*/
|
|
1016
|
+
async trafficBalance() {
|
|
1017
|
+
const healthy = await this.client.isHealthy();
|
|
1018
|
+
if (!healthy) {
|
|
1019
|
+
return { totalPurchased: 0, consumed: 0, remaining: 0 };
|
|
1020
|
+
}
|
|
1021
|
+
return {
|
|
1022
|
+
totalPurchased: 1e7,
|
|
1023
|
+
consumed: 0,
|
|
1024
|
+
remaining: 1e7
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Purchase additional traffic by burning Canton Coin (CC).
|
|
1029
|
+
*
|
|
1030
|
+
* TODO: Implement using actual CC burn mechanism.
|
|
1031
|
+
*/
|
|
1032
|
+
async purchaseTraffic(ccAmount) {
|
|
1033
|
+
void ccAmount;
|
|
1034
|
+
throw new Error("Traffic purchase not yet implemented \u2014 requires validator admin API");
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Check if there's sufficient traffic for a standard operation.
|
|
1038
|
+
* Returns true if remaining traffic > minimum threshold.
|
|
1039
|
+
*/
|
|
1040
|
+
async hasSufficientTraffic() {
|
|
1041
|
+
const balance = await this.trafficBalance();
|
|
1042
|
+
return balance.remaining > this.autoPurchaseConfig.minBalance;
|
|
1043
|
+
}
|
|
1044
|
+
/** Configure auto-purchase settings. */
|
|
1045
|
+
setAutoPurchase(config) {
|
|
1046
|
+
this.autoPurchaseConfig = { ...config };
|
|
1047
|
+
}
|
|
1048
|
+
/** Get current auto-purchase configuration. */
|
|
1049
|
+
getAutoPurchaseConfig() {
|
|
1050
|
+
return { ...this.autoPurchaseConfig };
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
// src/agent.ts
|
|
1055
|
+
var CantonAgent = class _CantonAgent {
|
|
1056
|
+
checking;
|
|
1057
|
+
safeguards;
|
|
1058
|
+
traffic;
|
|
1059
|
+
mpp;
|
|
1060
|
+
wallet;
|
|
1061
|
+
client;
|
|
1062
|
+
usdcx;
|
|
1063
|
+
constructor(client, usdcx, checking, safeguards, traffic, mpp, wallet) {
|
|
1064
|
+
this.client = client;
|
|
1065
|
+
this.usdcx = usdcx;
|
|
1066
|
+
this.checking = checking;
|
|
1067
|
+
this.safeguards = safeguards;
|
|
1068
|
+
this.traffic = traffic;
|
|
1069
|
+
this.mpp = mpp;
|
|
1070
|
+
this.wallet = wallet;
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Create a new CantonAgent from config.
|
|
1074
|
+
*
|
|
1075
|
+
* Loads configuration from ~/.caypo/config.json (or overrides),
|
|
1076
|
+
* initializes all sub-services, and wires them together.
|
|
1077
|
+
*/
|
|
1078
|
+
static async create(config) {
|
|
1079
|
+
const fileConfig = await loadConfig(config?.configPath);
|
|
1080
|
+
const merged = {
|
|
1081
|
+
...fileConfig,
|
|
1082
|
+
...config?.ledgerUrl && { ledgerUrl: config.ledgerUrl },
|
|
1083
|
+
...config?.partyId && { partyId: config.partyId },
|
|
1084
|
+
...config?.userId && { userId: config.userId },
|
|
1085
|
+
...config?.network && { network: config.network }
|
|
1086
|
+
};
|
|
1087
|
+
const token = config?.token ?? "";
|
|
1088
|
+
const client = new CantonClient({
|
|
1089
|
+
ledgerUrl: merged.ledgerUrl,
|
|
1090
|
+
token,
|
|
1091
|
+
userId: merged.userId
|
|
1092
|
+
});
|
|
1093
|
+
const usdcx = new USDCxService(client, merged.partyId);
|
|
1094
|
+
const safeguards = await SafeguardManager.load(config?.safeguardsPath);
|
|
1095
|
+
const traffic = new TrafficManager(client, merged.partyId);
|
|
1096
|
+
const checking = new CheckingAccount(usdcx, safeguards, client, merged.partyId);
|
|
1097
|
+
const mpp = new MppPayClient(usdcx, safeguards, merged.partyId, merged.network);
|
|
1098
|
+
const wallet = {
|
|
1099
|
+
address: merged.partyId,
|
|
1100
|
+
partyId: merged.partyId,
|
|
1101
|
+
network: merged.network
|
|
1102
|
+
};
|
|
1103
|
+
return new _CantonAgent(client, usdcx, checking, safeguards, traffic, mpp, wallet);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Create a CantonAgent from explicit parameters (no file I/O).
|
|
1107
|
+
* Useful for testing and programmatic setup.
|
|
1108
|
+
*/
|
|
1109
|
+
static fromParams(params) {
|
|
1110
|
+
const safeguards = params.safeguards ?? new SafeguardManager();
|
|
1111
|
+
const usdcx = new USDCxService(params.client, params.partyId);
|
|
1112
|
+
const traffic = new TrafficManager(params.client, params.partyId);
|
|
1113
|
+
const checking = new CheckingAccount(usdcx, safeguards, params.client, params.partyId);
|
|
1114
|
+
const mpp = new MppPayClient(usdcx, safeguards, params.partyId, params.network);
|
|
1115
|
+
const wallet = {
|
|
1116
|
+
address: params.partyId,
|
|
1117
|
+
partyId: params.partyId,
|
|
1118
|
+
network: params.network
|
|
1119
|
+
};
|
|
1120
|
+
return new _CantonAgent(params.client, usdcx, checking, safeguards, traffic, mpp, wallet);
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
// src/index.ts
|
|
1125
|
+
var CANTON_SDK_VERSION = "0.1.0";
|
|
1126
|
+
var DEFAULT_LEDGER_PORT = 7575;
|
|
1127
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1128
|
+
0 && (module.exports = {
|
|
1129
|
+
CANTON_SDK_VERSION,
|
|
1130
|
+
CantonAgent,
|
|
1131
|
+
CantonApiError,
|
|
1132
|
+
CantonAuthError,
|
|
1133
|
+
CantonClient,
|
|
1134
|
+
CantonTimeoutError,
|
|
1135
|
+
CheckingAccount,
|
|
1136
|
+
DEFAULT_CONFIG,
|
|
1137
|
+
DEFAULT_LEDGER_PORT,
|
|
1138
|
+
InsufficientBalanceError,
|
|
1139
|
+
Keystore,
|
|
1140
|
+
MPP_CANTON_VERSION,
|
|
1141
|
+
MppPayClient,
|
|
1142
|
+
SafeguardManager,
|
|
1143
|
+
TRANSFER_FACTORY_TEMPLATE_ID,
|
|
1144
|
+
TrafficManager,
|
|
1145
|
+
USDCX_HOLDING_TEMPLATE_ID,
|
|
1146
|
+
USDCX_INSTRUMENT_ID,
|
|
1147
|
+
USDCxService,
|
|
1148
|
+
addAmounts,
|
|
1149
|
+
compareAmounts,
|
|
1150
|
+
isValidAmount,
|
|
1151
|
+
loadConfig,
|
|
1152
|
+
parseWwwAuthenticate,
|
|
1153
|
+
saveConfig,
|
|
1154
|
+
selectHoldings,
|
|
1155
|
+
subtractAmounts,
|
|
1156
|
+
toCantonAmount
|
|
1157
|
+
});
|
|
1158
|
+
//# sourceMappingURL=index.cjs.map
|