@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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +26 -0
  2. package/.turbo/turbo-test.log +23 -0
  3. package/README.md +120 -0
  4. package/SPEC.md +223 -0
  5. package/dist/amount-L2SDLRZT.js +15 -0
  6. package/dist/amount-L2SDLRZT.js.map +1 -0
  7. package/dist/chunk-GSDB5FKZ.js +110 -0
  8. package/dist/chunk-GSDB5FKZ.js.map +1 -0
  9. package/dist/index.cjs +1158 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.d.cts +673 -0
  12. package/dist/index.d.ts +673 -0
  13. package/dist/index.js +986 -0
  14. package/dist/index.js.map +1 -0
  15. package/package.json +50 -0
  16. package/src/__tests__/agent.test.ts +217 -0
  17. package/src/__tests__/amount.test.ts +202 -0
  18. package/src/__tests__/client.test.ts +516 -0
  19. package/src/__tests__/e2e/canton-client.e2e.test.ts +190 -0
  20. package/src/__tests__/e2e/mpp-flow.e2e.test.ts +346 -0
  21. package/src/__tests__/e2e/setup.ts +112 -0
  22. package/src/__tests__/e2e/usdcx.e2e.test.ts +114 -0
  23. package/src/__tests__/keystore.test.ts +197 -0
  24. package/src/__tests__/pay-client.test.ts +257 -0
  25. package/src/__tests__/safeguards.test.ts +333 -0
  26. package/src/__tests__/usdcx.test.ts +374 -0
  27. package/src/accounts/checking.ts +118 -0
  28. package/src/agent.ts +132 -0
  29. package/src/canton/amount.ts +167 -0
  30. package/src/canton/client.ts +218 -0
  31. package/src/canton/errors.ts +45 -0
  32. package/src/canton/holdings.ts +90 -0
  33. package/src/canton/index.ts +51 -0
  34. package/src/canton/types.ts +214 -0
  35. package/src/canton/usdcx.ts +166 -0
  36. package/src/index.ts +97 -0
  37. package/src/mpp/pay-client.ts +170 -0
  38. package/src/safeguards/manager.ts +183 -0
  39. package/src/traffic/manager.ts +95 -0
  40. package/src/wallet/config.ts +88 -0
  41. package/src/wallet/keystore.ts +164 -0
  42. package/tsconfig.json +8 -0
  43. package/tsup.config.ts +9 -0
  44. package/vitest.config.ts +7 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Holding selection algorithm for USDCx transfers.
3
+ *
4
+ * Canton uses a UTXO-like model: a party can have multiple Holding contracts.
5
+ * When transferring, we need to select holdings that cover the required amount.
6
+ */
7
+
8
+ import { addAmounts, compareAmounts } from "./amount.js";
9
+
10
+ export interface USDCxHolding {
11
+ contractId: string;
12
+ owner: string;
13
+ amount: string;
14
+ templateId: string;
15
+ }
16
+
17
+ export interface HoldingSelection {
18
+ type: "single" | "merge-then-transfer";
19
+ contractIds: string[];
20
+ }
21
+
22
+ export class InsufficientBalanceError extends Error {
23
+ readonly available: string;
24
+ readonly required: string;
25
+
26
+ constructor(available: string, required: string) {
27
+ super(`Insufficient balance: have ${available}, need ${required}`);
28
+ this.name = "InsufficientBalanceError";
29
+ this.available = available;
30
+ this.required = required;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Select holdings that cover the required amount.
36
+ *
37
+ * Strategy:
38
+ * 1. Sort holdings by amount descending.
39
+ * 2. Look for a single holding >= required (prefer smallest sufficient one).
40
+ * 3. If none, accumulate multiple holdings until we cover the amount.
41
+ * 4. Throw InsufficientBalanceError if total < required.
42
+ */
43
+ export function selectHoldings(
44
+ holdings: USDCxHolding[],
45
+ requiredAmount: string,
46
+ ): HoldingSelection {
47
+ if (holdings.length === 0) {
48
+ throw new InsufficientBalanceError("0", requiredAmount);
49
+ }
50
+
51
+ // Sort descending by amount
52
+ const sorted = [...holdings].sort((a, b) => compareAmounts(b.amount, a.amount));
53
+
54
+ // 1. Try to find a single holding that covers the amount.
55
+ // Among all sufficient holdings, pick the smallest (best fit).
56
+ let bestSingle: USDCxHolding | null = null;
57
+ for (const h of sorted) {
58
+ if (compareAmounts(h.amount, requiredAmount) >= 0) {
59
+ // This one is sufficient — it might be the best fit
60
+ // since we iterate descending, each subsequent one is smaller but still sufficient
61
+ bestSingle = h;
62
+ }
63
+ }
64
+
65
+ if (bestSingle) {
66
+ return {
67
+ type: "single",
68
+ contractIds: [bestSingle.contractId],
69
+ };
70
+ }
71
+
72
+ // 2. Accumulate multiple holdings (greedy — take largest first)
73
+ let accumulated = "0";
74
+ const selected: string[] = [];
75
+
76
+ for (const h of sorted) {
77
+ selected.push(h.contractId);
78
+ accumulated = addAmounts(accumulated, h.amount);
79
+
80
+ if (compareAmounts(accumulated, requiredAmount) >= 0) {
81
+ return {
82
+ type: "merge-then-transfer",
83
+ contractIds: selected,
84
+ };
85
+ }
86
+ }
87
+
88
+ // 3. Not enough
89
+ throw new InsufficientBalanceError(accumulated, requiredAmount);
90
+ }
@@ -0,0 +1,51 @@
1
+ export { CantonClient } from "./client.js";
2
+ export { CantonApiError, CantonAuthError, CantonTimeoutError } from "./errors.js";
3
+ export type {
4
+ ActiveContract,
5
+ ActiveContractsRequest,
6
+ AnyPartyFilter,
7
+ ArchivedEvent,
8
+ CantonClientConfig,
9
+ CantonErrorCode,
10
+ Command,
11
+ CreateCommand,
12
+ CreatedEvent,
13
+ EventFormat,
14
+ ExerciseCommand,
15
+ ExercisedEvent,
16
+ FlatTransaction,
17
+ IdentifierFilter,
18
+ LedgerEndResponse,
19
+ LedgerError,
20
+ PartyDetails,
21
+ PartyFilter,
22
+ PartyLocalMetadata,
23
+ QueryActiveContractsParams,
24
+ SubmitAndWaitRequest,
25
+ SubmitAndWaitResponse,
26
+ SubmitParams,
27
+ TransactionResponse,
28
+ TransactionTree,
29
+ TransactionTreeEvent,
30
+ } from "./types.js";
31
+ export {
32
+ addAmounts,
33
+ compareAmounts,
34
+ isValidAmount,
35
+ subtractAmounts,
36
+ toCantonAmount,
37
+ } from "./amount.js";
38
+ export {
39
+ InsufficientBalanceError,
40
+ selectHoldings,
41
+ type HoldingSelection,
42
+ type USDCxHolding,
43
+ } from "./holdings.js";
44
+ export {
45
+ USDCxService,
46
+ USDCX_HOLDING_TEMPLATE_ID,
47
+ USDCX_INSTRUMENT_ID,
48
+ TRANSFER_FACTORY_TEMPLATE_ID,
49
+ type TransferParams,
50
+ type TransferResult,
51
+ } from "./usdcx.js";
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Canton JSON Ledger API v2 — Type Definitions
3
+ *
4
+ * Verified against: docs.digitalasset.com/build/3.5, Canton OpenAPI spec v3.3.0
5
+ * Default port: 7575
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Party
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface PartyLocalMetadata {
13
+ resourceVersion: string;
14
+ annotations: Record<string, string>;
15
+ }
16
+
17
+ export interface PartyDetails {
18
+ party: string; // e.g., "Alice::122084768362d0ce21f1ffec870e55e365a292cdf8f54c5c38ad7775b9bdd462e141"
19
+ isLocal: boolean;
20
+ localMetadata: PartyLocalMetadata;
21
+ identityProviderId: string;
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Commands
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export interface CreateCommand {
29
+ CreateCommand: {
30
+ templateId: string; // "#package-name:Module:Template" or "packagehash:Module:Template"
31
+ createArguments: Record<string, unknown>;
32
+ };
33
+ }
34
+
35
+ export interface ExerciseCommand {
36
+ ExerciseCommand: {
37
+ templateId: string;
38
+ contractId: string;
39
+ choice: string;
40
+ choiceArgument: Record<string, unknown>;
41
+ };
42
+ }
43
+
44
+ export type Command = CreateCommand | ExerciseCommand;
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Submit and Wait
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export interface SubmitAndWaitRequest {
51
+ commands: Command[];
52
+ userId: string;
53
+ commandId: string;
54
+ actAs: string[];
55
+ readAs?: string[];
56
+ }
57
+
58
+ export interface SubmitAndWaitResponse {
59
+ updateId: string;
60
+ completionOffset: number;
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Events
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export interface CreatedEvent {
68
+ contractId: string;
69
+ templateId: string;
70
+ createArgument: Record<string, unknown>;
71
+ witnessParties: string[];
72
+ signatories: string[];
73
+ observers: string[];
74
+ }
75
+
76
+ export interface ArchivedEvent {
77
+ contractId: string;
78
+ templateId: string;
79
+ witnessParties: string[];
80
+ }
81
+
82
+ export interface ExercisedEvent {
83
+ contractId: string;
84
+ templateId: string;
85
+ choice: string;
86
+ choiceArgument: Record<string, unknown>;
87
+ exerciseResult: unknown;
88
+ actingParties: string[];
89
+ childEvents: TransactionTreeEvent[];
90
+ }
91
+
92
+ export type TransactionTreeEvent =
93
+ | { createdEvent: CreatedEvent }
94
+ | { exercisedEvent: ExercisedEvent };
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Transactions
98
+ // ---------------------------------------------------------------------------
99
+
100
+ export interface TransactionTree {
101
+ updateId: string;
102
+ commandId: string;
103
+ effectiveAt: string;
104
+ offset: number;
105
+ eventsById: Record<string, TransactionTreeEvent>;
106
+ rootEventIds: string[];
107
+ }
108
+
109
+ export interface FlatTransaction {
110
+ updateId: string;
111
+ commandId: string;
112
+ effectiveAt: string;
113
+ offset: number;
114
+ events: Array<{ createdEvent?: CreatedEvent; archivedEvent?: ArchivedEvent }>;
115
+ }
116
+
117
+ export interface TransactionResponse {
118
+ transaction: FlatTransaction;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Active Contracts
123
+ // ---------------------------------------------------------------------------
124
+
125
+ export interface IdentifierFilter {
126
+ identifierFilter:
127
+ | { TemplateFilter: { value: { templateId: string } } }
128
+ | { WildcardFilter: { value: { includeCreatedEventBlob?: boolean } } };
129
+ }
130
+
131
+ export interface PartyFilter {
132
+ cumulative: IdentifierFilter[];
133
+ }
134
+
135
+ export interface AnyPartyFilter {
136
+ cumulative: IdentifierFilter[];
137
+ }
138
+
139
+ export interface EventFormat {
140
+ filtersByParty?: Record<string, PartyFilter>;
141
+ filtersForAnyParty?: AnyPartyFilter;
142
+ verbose?: boolean;
143
+ }
144
+
145
+ export interface ActiveContractsRequest {
146
+ eventFormat: EventFormat;
147
+ activeAtOffset: number;
148
+ }
149
+
150
+ export interface ActiveContract {
151
+ contractId: string;
152
+ templateId: string;
153
+ createArgument: Record<string, unknown>;
154
+ createdAt: string;
155
+ signatories: string[];
156
+ observers: string[];
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Ledger State
161
+ // ---------------------------------------------------------------------------
162
+
163
+ export interface LedgerEndResponse {
164
+ offset: number;
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Errors
169
+ // ---------------------------------------------------------------------------
170
+
171
+ export type CantonErrorCode =
172
+ | "INVALID_ARGUMENT"
173
+ | "NOT_FOUND"
174
+ | "PERMISSION_DENIED"
175
+ | "ALREADY_EXISTS"
176
+ | "FAILED_PRECONDITION"
177
+ | "UNAVAILABLE"
178
+ | string;
179
+
180
+ export interface LedgerError {
181
+ cause: string;
182
+ code: CantonErrorCode;
183
+ context: Record<string, string>;
184
+ errorCategory: number;
185
+ grpcCodeValue: number;
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Client Config
190
+ // ---------------------------------------------------------------------------
191
+
192
+ export interface CantonClientConfig {
193
+ ledgerUrl: string; // e.g., "http://localhost:7575"
194
+ token: string; // JWT bearer token
195
+ userId: string; // Ledger API user ID
196
+ timeout?: number; // Request timeout in ms (default: 30000)
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Client Method Params (excluding userId, which comes from config)
201
+ // ---------------------------------------------------------------------------
202
+
203
+ export interface SubmitParams {
204
+ commands: Command[];
205
+ commandId: string;
206
+ actAs: string[];
207
+ readAs?: string[];
208
+ }
209
+
210
+ export interface QueryActiveContractsParams {
211
+ filtersByParty?: Record<string, PartyFilter>;
212
+ filtersForAnyParty?: AnyPartyFilter;
213
+ activeAtOffset: number;
214
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * USDCx Operations — Query holdings, transfer, merge.
3
+ *
4
+ * Uses CIP-56 token standard:
5
+ * - Splice.Api.Token.HoldingV1:Holding for balance queries
6
+ * - TransferFactory_Transfer for 1-step transfers (requires TransferPreapproval)
7
+ */
8
+
9
+ import { addAmounts, toCantonAmount } from "./amount.js";
10
+ import type { CantonClient } from "./client.js";
11
+ import { selectHoldings, type HoldingSelection, type USDCxHolding } from "./holdings.js";
12
+ import type { SubmitAndWaitResponse } from "./types.js";
13
+
14
+ /** CIP-56 Holding template ID — used for active contract queries */
15
+ export const USDCX_HOLDING_TEMPLATE_ID = "Splice.Api.Token.HoldingV1:Holding";
16
+
17
+ /** TransferFactory template ID for 1-step transfers */
18
+ export const TRANSFER_FACTORY_TEMPLATE_ID = "Splice.Api.Token.TransferFactoryV1:TransferFactory";
19
+
20
+ /** USDCx instrument identifier */
21
+ export const USDCX_INSTRUMENT_ID = "USDCx";
22
+
23
+ export interface TransferResult {
24
+ updateId: string;
25
+ completionOffset: number;
26
+ commandId: string;
27
+ }
28
+
29
+ export interface TransferParams {
30
+ recipient: string;
31
+ amount: string;
32
+ commandId?: string;
33
+ }
34
+
35
+ export class USDCxService {
36
+ constructor(
37
+ private readonly client: CantonClient,
38
+ private readonly partyId: string,
39
+ ) {}
40
+
41
+ /**
42
+ * Query all USDCx Holding contracts for this party.
43
+ */
44
+ async getHoldings(): Promise<USDCxHolding[]> {
45
+ const offset = await this.client.getLedgerEnd();
46
+
47
+ const contracts = await this.client.queryActiveContracts({
48
+ filtersByParty: {
49
+ [this.partyId]: {
50
+ cumulative: [
51
+ {
52
+ identifierFilter: {
53
+ TemplateFilter: {
54
+ value: { templateId: USDCX_HOLDING_TEMPLATE_ID },
55
+ },
56
+ },
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ activeAtOffset: offset,
62
+ });
63
+
64
+ return contracts.map((c) => ({
65
+ contractId: c.contractId,
66
+ owner: (c.createArgument.owner as string) ?? this.partyId,
67
+ amount: c.createArgument.amount as string,
68
+ templateId: c.templateId,
69
+ }));
70
+ }
71
+
72
+ /**
73
+ * Calculate total USDCx balance by summing all holding amounts.
74
+ * Returns a string with up to 10 decimal places.
75
+ */
76
+ async getBalance(): Promise<string> {
77
+ const holdings = await this.getHoldings();
78
+
79
+ if (holdings.length === 0) {
80
+ return "0";
81
+ }
82
+
83
+ let total = "0";
84
+ for (const h of holdings) {
85
+ total = addAmounts(total, h.amount);
86
+ }
87
+
88
+ return total;
89
+ }
90
+
91
+ /**
92
+ * Transfer USDCx using TransferFactory_Transfer (1-step).
93
+ * Requires the recipient to have an active TransferPreapproval.
94
+ */
95
+ async transfer(params: TransferParams): Promise<TransferResult> {
96
+ const commandId = params.commandId ?? crypto.randomUUID();
97
+ const amount = toCantonAmount(params.amount);
98
+
99
+ // 1. Query holdings
100
+ const holdings = await this.getHoldings();
101
+
102
+ // 2. Select holdings covering the amount (throws InsufficientBalanceError if not enough)
103
+ const selection: HoldingSelection = selectHoldings(holdings, params.amount);
104
+
105
+ // 3. Build and submit ExerciseCommand for TransferFactory_Transfer
106
+ const result: SubmitAndWaitResponse = await this.client.submitAndWait({
107
+ commands: [
108
+ {
109
+ ExerciseCommand: {
110
+ templateId: TRANSFER_FACTORY_TEMPLATE_ID,
111
+ contractId: selection.contractIds[0],
112
+ choice: "TransferFactory_Transfer",
113
+ choiceArgument: {
114
+ sender: this.partyId,
115
+ receiver: params.recipient,
116
+ amount,
117
+ instrumentId: USDCX_INSTRUMENT_ID,
118
+ inputHoldingCids: selection.contractIds,
119
+ meta: {},
120
+ },
121
+ },
122
+ },
123
+ ],
124
+ commandId,
125
+ actAs: [this.partyId],
126
+ readAs: [this.partyId],
127
+ });
128
+
129
+ return {
130
+ updateId: result.updateId,
131
+ completionOffset: result.completionOffset,
132
+ commandId,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Merge multiple holdings into fewer UTXOs.
138
+ * Returns the commandId of the merge transaction.
139
+ */
140
+ async mergeHoldings(holdingCids: string[]): Promise<string> {
141
+ if (holdingCids.length < 2) {
142
+ throw new Error("Need at least 2 holdings to merge");
143
+ }
144
+
145
+ const commandId = crypto.randomUUID();
146
+
147
+ await this.client.submitAndWait({
148
+ commands: [
149
+ {
150
+ ExerciseCommand: {
151
+ templateId: USDCX_HOLDING_TEMPLATE_ID,
152
+ contractId: holdingCids[0],
153
+ choice: "Merge",
154
+ choiceArgument: {
155
+ holdingCids: holdingCids.slice(1),
156
+ },
157
+ },
158
+ },
159
+ ],
160
+ commandId,
161
+ actAs: [this.partyId],
162
+ });
163
+
164
+ return commandId;
165
+ }
166
+ }
package/src/index.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @caypo/canton-sdk — Core SDK for Canton Network.
3
+ * JSON Ledger API v2 client, USDCx operations, agent accounts.
4
+ */
5
+
6
+ export { MPP_CANTON_VERSION } from "@caypo/mpp-canton";
7
+
8
+ export const CANTON_SDK_VERSION = "0.1.0";
9
+
10
+ export const DEFAULT_LEDGER_PORT = 7575;
11
+
12
+ // Canton JSON Ledger API v2 client
13
+ export { CantonClient } from "./canton/client.js";
14
+ export { CantonApiError, CantonAuthError, CantonTimeoutError } from "./canton/errors.js";
15
+ export type {
16
+ ActiveContract,
17
+ ActiveContractsRequest,
18
+ AnyPartyFilter,
19
+ ArchivedEvent,
20
+ CantonClientConfig,
21
+ CantonErrorCode,
22
+ Command,
23
+ CreateCommand,
24
+ CreatedEvent,
25
+ EventFormat,
26
+ ExerciseCommand,
27
+ ExercisedEvent,
28
+ FlatTransaction,
29
+ IdentifierFilter,
30
+ LedgerEndResponse,
31
+ LedgerError,
32
+ PartyDetails,
33
+ PartyFilter,
34
+ PartyLocalMetadata,
35
+ QueryActiveContractsParams,
36
+ SubmitAndWaitRequest,
37
+ SubmitAndWaitResponse,
38
+ SubmitParams,
39
+ TransactionResponse,
40
+ TransactionTree,
41
+ TransactionTreeEvent,
42
+ } from "./canton/types.js";
43
+
44
+ // USDCx operations
45
+ export {
46
+ USDCxService,
47
+ USDCX_HOLDING_TEMPLATE_ID,
48
+ USDCX_INSTRUMENT_ID,
49
+ TRANSFER_FACTORY_TEMPLATE_ID,
50
+ } from "./canton/usdcx.js";
51
+ export type { TransferParams, TransferResult } from "./canton/usdcx.js";
52
+
53
+ // Amount utilities (string-based decimal arithmetic)
54
+ export {
55
+ addAmounts,
56
+ compareAmounts,
57
+ isValidAmount,
58
+ subtractAmounts,
59
+ toCantonAmount,
60
+ } from "./canton/amount.js";
61
+
62
+ // Holding selection
63
+ export { InsufficientBalanceError, selectHoldings } from "./canton/holdings.js";
64
+ export type { HoldingSelection, USDCxHolding } from "./canton/holdings.js";
65
+
66
+ // Wallet keystore
67
+ export { Keystore } from "./wallet/keystore.js";
68
+ export type { WalletData } from "./wallet/keystore.js";
69
+
70
+ // Agent configuration
71
+ export { loadConfig, saveConfig, DEFAULT_CONFIG } from "./wallet/config.js";
72
+ export type {
73
+ AgentConfig,
74
+ TrafficConfig,
75
+ SafeguardsConfig,
76
+ MppConfig,
77
+ } from "./wallet/config.js";
78
+
79
+ // Safeguards
80
+ export { SafeguardManager } from "./safeguards/manager.js";
81
+ export type { SafeguardConfig, CheckResult } from "./safeguards/manager.js";
82
+
83
+ // High-level agent
84
+ export { CantonAgent } from "./agent.js";
85
+ export type { CantonAgentConfig, WalletInfo } from "./agent.js";
86
+
87
+ // Checking account
88
+ export { CheckingAccount } from "./accounts/checking.js";
89
+ export type { SendOptions, TransactionRecord } from "./accounts/checking.js";
90
+
91
+ // Traffic manager
92
+ export { TrafficManager } from "./traffic/manager.js";
93
+ export type { TrafficBalance, AutoPurchaseConfig } from "./traffic/manager.js";
94
+
95
+ // MPP pay client
96
+ export { MppPayClient, parseWwwAuthenticate } from "./mpp/pay-client.js";
97
+ export type { PayOptions, PayResult, PaymentChallenge } from "./mpp/pay-client.js";