@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,26 @@
1
+
2
+ > @caypo/canton-sdk@0.1.0 build /Users/anil/Desktop/caypo/packages/sdk
3
+ > tsup
4
+
5
+ CLI Building entry: src/index.ts
6
+ CLI Using tsconfig: tsconfig.json
7
+ CLI tsup v8.5.1
8
+ CLI Using tsup config: /Users/anil/Desktop/caypo/packages/sdk/tsup.config.ts
9
+ CLI Target: es2022
10
+ CLI Cleaning output folder
11
+ ESM Build start
12
+ CJS Build start
13
+ CJS dist/index.cjs 35.38 KB
14
+ CJS dist/index.cjs.map 72.40 KB
15
+ CJS ⚡️ Build success in 15ms
16
+ ESM dist/chunk-GSDB5FKZ.js 3.22 KB
17
+ ESM dist/amount-L2SDLRZT.js 264.00 B
18
+ ESM dist/index.js 29.26 KB
19
+ ESM dist/chunk-GSDB5FKZ.js.map 7.90 KB
20
+ ESM dist/amount-L2SDLRZT.js.map 71.00 B
21
+ ESM dist/index.js.map 64.49 KB
22
+ ESM ⚡️ Build success in 15ms
23
+ DTS Build start
24
+ DTS ⚡️ Build success in 519ms
25
+ DTS dist/index.d.ts 20.56 KB
26
+ DTS dist/index.d.cts 20.56 KB
@@ -0,0 +1,23 @@
1
+
2
+ > @caypo/canton-sdk@0.1.0 test /Users/anil/Desktop/caypo/packages/sdk
3
+ > vitest run
4
+
5
+
6
+ RUN v3.2.4 /Users/anil/Desktop/caypo/packages/sdk
7
+
8
+ ↓ src/__tests__/e2e/canton-client.e2e.test.ts (13 tests | 13 skipped)
9
+ ✓ src/__tests__/amount.test.ts (107 tests) 5ms
10
+ ↓ src/__tests__/e2e/mpp-flow.e2e.test.ts (11 tests | 11 skipped)
11
+ ✓ src/__tests__/client.test.ts (24 tests) 18ms
12
+ ✓ src/__tests__/usdcx.test.ts (21 tests) 11ms
13
+ ✓ src/__tests__/pay-client.test.ts (15 tests) 13ms
14
+ ✓ src/__tests__/safeguards.test.ts (24 tests) 24ms
15
+ ✓ src/__tests__/agent.test.ts (13 tests) 6ms
16
+ ↓ src/__tests__/e2e/usdcx.e2e.test.ts (5 tests | 5 skipped)
17
+ ✓ src/__tests__/keystore.test.ts (11 tests) 374ms
18
+
19
+ Test Files 7 passed | 3 skipped (10)
20
+ Tests 215 passed | 29 skipped (244)
21
+ Start at 19:11:46
22
+ Duration 636ms (transform 376ms, setup 0ms, collect 632ms, tests 452ms, environment 1ms, prepare 629ms)
23
+
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @caypo/canton-sdk
2
+
3
+ **Core SDK for AI agent banking on [Canton Network](https://canton.network)**
4
+
5
+ USDCx transfers, encrypted wallets, safeguards, traffic management, and MPP auto-pay — everything an AI agent needs to operate a bank account on Canton.
6
+
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0%20%2F%20MIT-blue)](../../LICENSE-APACHE)
8
+ [![Tests](https://img.shields.io/badge/tests-215%20passing-brightgreen)](../../)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @caypo/canton-sdk
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```typescript
19
+ import { CantonAgent } from "@caypo/canton-sdk";
20
+
21
+ const agent = await CantonAgent.create({
22
+ ledgerUrl: "http://localhost:7575",
23
+ token: "your-jwt",
24
+ partyId: "Agent::1220...",
25
+ network: "testnet",
26
+ });
27
+
28
+ // Balance
29
+ const { available, holdingCount } = await agent.checking.balance();
30
+ console.log(`${available} USDCx across ${holdingCount} holdings`);
31
+
32
+ // Send USDCx (safeguards checked automatically)
33
+ const tx = await agent.checking.send("Bob::1220...", "10.00");
34
+ console.log(`Sent! Update ID: ${tx.updateId}`);
35
+
36
+ // Pay for API call (handles 402 automatically)
37
+ const result = await agent.mpp.pay("https://mpp.cayvox.io/openai/v1/chat/completions", {
38
+ method: "POST",
39
+ body: JSON.stringify({ model: "gpt-4o", messages: [{ role: "user", content: "Hello" }] }),
40
+ maxPrice: "0.05",
41
+ });
42
+ ```
43
+
44
+ ## Features
45
+
46
+ ### CantonClient — JSON Ledger API v2
47
+
48
+ ```typescript
49
+ import { CantonClient } from "@caypo/canton-sdk";
50
+
51
+ const client = new CantonClient({ ledgerUrl, token, userId });
52
+ await client.submitAndWait({ commands, commandId, actAs });
53
+ await client.queryActiveContracts({ filtersByParty, activeAtOffset });
54
+ await client.getLedgerEnd();
55
+ await client.allocateParty("Alice");
56
+ await client.isHealthy();
57
+ ```
58
+
59
+ ### USDCxService — Token Operations
60
+
61
+ ```typescript
62
+ import { USDCxService } from "@caypo/canton-sdk";
63
+
64
+ const usdcx = new USDCxService(client, partyId);
65
+ const holdings = await usdcx.getHoldings(); // USDCx UTXO holdings
66
+ const balance = await usdcx.getBalance(); // Sum as string
67
+ await usdcx.transfer({ recipient, amount }); // TransferFactory_Transfer
68
+ await usdcx.mergeHoldings(holdingCids); // Consolidate UTXOs
69
+ ```
70
+
71
+ ### SafeguardManager — Spending Controls
72
+
73
+ ```typescript
74
+ import { SafeguardManager } from "@caypo/canton-sdk";
75
+
76
+ const safeguards = await SafeguardManager.load();
77
+ safeguards.setTxLimit("100"); // Per-transaction limit
78
+ safeguards.setDailyLimit("1000"); // Daily spending limit
79
+ safeguards.lock("1234"); // Lock wallet
80
+ const { allowed, reason } = safeguards.check("50"); // Pre-tx check
81
+ ```
82
+
83
+ ### Keystore — Encrypted Wallet
84
+
85
+ ```typescript
86
+ import { Keystore } from "@caypo/canton-sdk";
87
+
88
+ const ks = await Keystore.create("1234", { partyId, jwt, userId });
89
+ const loaded = await Keystore.load("1234");
90
+ const { partyId, jwt } = loaded.getCredentials();
91
+ await loaded.changePin("1234", "5678");
92
+ ```
93
+
94
+ ### Amount Utilities — No Floating Point
95
+
96
+ ```typescript
97
+ import { addAmounts, compareAmounts, subtractAmounts } from "@caypo/canton-sdk";
98
+
99
+ addAmounts("1.5", "2.5"); // "4"
100
+ compareAmounts("10", "9.99"); // 1
101
+ subtractAmounts("100", "0.01"); // "99.99"
102
+ // All amounts are strings — Canton uses Numeric 10 (10 decimal places)
103
+ ```
104
+
105
+ ## API Reference
106
+
107
+ | Class | Purpose |
108
+ |-------|---------|
109
+ | `CantonAgent` | High-level entry point — wires all services together |
110
+ | `CheckingAccount` | Balance, send, receive, history |
111
+ | `CantonClient` | Low-level JSON Ledger API v2 client |
112
+ | `USDCxService` | Holdings, balance, transfer, merge |
113
+ | `SafeguardManager` | Tx/daily limits, lock/unlock |
114
+ | `TrafficManager` | Validator traffic balance |
115
+ | `MppPayClient` | HTTP 402 auto-pay flow |
116
+ | `Keystore` | AES-256-GCM encrypted wallet |
117
+
118
+ ## License
119
+
120
+ Apache-2.0 OR MIT
package/SPEC.md ADDED
@@ -0,0 +1,223 @@
1
+ # @cayvox/canton-sdk — Core SDK Specification (Production)
2
+
3
+ ## Canton Client (Low-Level)
4
+
5
+ ```typescript
6
+ export class CantonClient {
7
+ constructor(config: {
8
+ ledgerUrl: string; // e.g., "http://localhost:7575"
9
+ token: string; // JWT bearer token
10
+ userId: string; // Ledger API user ID
11
+ })
12
+
13
+ // Command submission (verified v2 API)
14
+ async submitAndWait(params: {
15
+ commands: Command[];
16
+ commandId: string;
17
+ actAs: string[];
18
+ readAs?: string[];
19
+ }): Promise<{ updateId: string; completionOffset: number }>
20
+
21
+ async submitAndWaitForTransaction(params: {
22
+ commands: Command[];
23
+ commandId: string;
24
+ actAs: string[];
25
+ readAs?: string[];
26
+ }): Promise<TransactionResponse>
27
+
28
+ // Active contract queries
29
+ async queryActiveContracts(params: {
30
+ filtersByParty?: Record<string, PartyFilter>;
31
+ filtersForAnyParty?: AnyPartyFilter;
32
+ activeAtOffset: number;
33
+ }): Promise<ActiveContract[]>
34
+
35
+ // Transaction lookup
36
+ async getTransactionById(updateId: string): Promise<TransactionTree>
37
+
38
+ // Ledger state
39
+ async getLedgerEnd(): Promise<number>
40
+
41
+ // Party management
42
+ async allocateParty(hint: string): Promise<PartyDetails>
43
+ async listParties(): Promise<PartyDetails[]>
44
+
45
+ // Health check
46
+ async isHealthy(): Promise<boolean>
47
+ }
48
+
49
+ // Command types (verified from Canton OpenAPI)
50
+ type Command =
51
+ | { CreateCommand: { createArguments: any; templateId: string } }
52
+ | { ExerciseCommand: { templateId: string; contractId: string; choice: string; choiceArgument: any } };
53
+
54
+ interface PartyDetails {
55
+ party: string; // e.g., "Alice::1220abcd..."
56
+ isLocal: boolean;
57
+ localMetadata: { resourceVersion: string; annotations: Record<string, string> };
58
+ identityProviderId: string;
59
+ }
60
+ ```
61
+
62
+ ## USDCx Operations
63
+
64
+ ```typescript
65
+ export class USDCxService {
66
+ constructor(private client: CantonClient, private partyId: string)
67
+
68
+ /** Query all USDCx Holding contracts for this party */
69
+ async getHoldings(): Promise<USDCxHolding[]>
70
+
71
+ /** Calculate total balance from all holdings */
72
+ async getBalance(): Promise<string>
73
+
74
+ /** Transfer USDCx via TransferFactory (requires recipient TransferPreapproval) */
75
+ async transfer(params: {
76
+ recipient: string; // Recipient party ID
77
+ amount: string; // Decimal amount
78
+ commandId?: string; // Optional custom idempotency key
79
+ }): Promise<TransferResult>
80
+
81
+ /** Create a TransferInstruction (2-step, for when no preapproval exists) */
82
+ async createTransferInstruction(params: {
83
+ recipient: string;
84
+ amount: string;
85
+ }): Promise<TransferInstructionResult>
86
+
87
+ /** Merge multiple holdings into fewer UTXOs */
88
+ async mergeHoldings(holdingCids: string[]): Promise<string>
89
+ }
90
+
91
+ interface USDCxHolding {
92
+ contractId: string;
93
+ owner: string; // Party ID
94
+ amount: string; // Decimal amount (up to 10 decimals)
95
+ templateId: string;
96
+ }
97
+
98
+ interface TransferResult {
99
+ updateId: string;
100
+ completionOffset: number;
101
+ commandId: string;
102
+ }
103
+ ```
104
+
105
+ ## CantonAgent (High-Level)
106
+
107
+ ```typescript
108
+ export class CantonAgent {
109
+ static async create(config?: Partial<AgentConfig>): Promise<CantonAgent>
110
+
111
+ // Sub-services
112
+ readonly checking: CheckingAccount;
113
+ readonly savings: SavingsAccount; // Phase 3
114
+ readonly credit: CreditAccount; // Phase 3
115
+ readonly exchange: ExchangeAccount; // Phase 3
116
+ readonly invest: InvestmentAccount; // Phase 4
117
+
118
+ readonly safeguards: SafeguardManager;
119
+ readonly traffic: TrafficManager; // NOT "gas" — Canton uses traffic
120
+ readonly mpp: MppPayClient;
121
+ readonly wallet: WalletInfo;
122
+ }
123
+ ```
124
+
125
+ ## CheckingAccount
126
+
127
+ ```typescript
128
+ export class CheckingAccount {
129
+ /** Get USDCx balance */
130
+ async balance(): Promise<{ available: string; holdingCount: number }>
131
+
132
+ /** Send USDCx — uses TransferFactory_Transfer if preapproval exists,
133
+ falls back to TransferInstruction (2-step) otherwise */
134
+ async send(recipient: string, amount: string, opts?: {
135
+ memo?: string;
136
+ commandId?: string;
137
+ }): Promise<TransferResult>
138
+
139
+ /** Party ID for receiving payments */
140
+ address(): string
141
+
142
+ /** Transaction history via /v2/updates/flats */
143
+ async history(opts?: { limit?: number }): Promise<TransactionRecord[]>
144
+ }
145
+ ```
146
+
147
+ ## TrafficManager (replaces "GasManager")
148
+
149
+ ```typescript
150
+ /** Canton uses traffic budgets per validator, NOT per-transaction gas fees */
151
+ export class TrafficManager {
152
+ /** Check validator's traffic balance */
153
+ async trafficBalance(): Promise<{
154
+ totalPurchased: number;
155
+ consumed: number;
156
+ remaining: number;
157
+ }>
158
+
159
+ /** Purchase additional traffic by burning CC */
160
+ async purchaseTraffic(ccAmount: string): Promise<{ txId: string }>
161
+
162
+ /** Check if sufficient traffic for an operation */
163
+ async hasSufficientTraffic(): Promise<boolean>
164
+
165
+ /** Auto-purchase configuration */
166
+ setAutoPurchase(config: { enabled: boolean; minBalance: number; purchaseAmount: string }): void
167
+ }
168
+ ```
169
+
170
+ ## SafeguardManager
171
+
172
+ ```typescript
173
+ export class SafeguardManager {
174
+ settings(): SafeguardConfig
175
+ setTxLimit(amount: string): void
176
+ setDailyLimit(amount: string): void
177
+ lock(): void
178
+ unlock(pin: string): void
179
+ check(amount: string): { allowed: boolean; reason?: string }
180
+ recordSpend(amount: string): void
181
+ }
182
+
183
+ // Storage: ~/.canton-agent/safeguards.json
184
+ ```
185
+
186
+ ## Wallet Keystore
187
+
188
+ ```typescript
189
+ export class Keystore {
190
+ static async create(pin: string, path?: string): Promise<Keystore>
191
+ static async load(pin: string, path?: string): Promise<Keystore>
192
+ getCredentials(): { partyId: string; jwt: string }
193
+ readonly address: string; // Party ID
194
+ }
195
+
196
+ // Storage: ~/.canton-agent/wallet.key (AES-256-GCM encrypted)
197
+ ```
198
+
199
+ ## Configuration
200
+
201
+ ```json
202
+ {
203
+ "version": 2,
204
+ "network": "mainnet",
205
+ "ledgerUrl": "https://canton-node.example.com:7575",
206
+ "partyId": "Agent::1220abcdef...",
207
+ "userId": "ledger-api-user",
208
+ "keystorePath": "~/.canton-agent/wallet.key",
209
+ "traffic": {
210
+ "autoPurchase": true,
211
+ "minBalance": 1000,
212
+ "purchaseAmountCC": "5.0"
213
+ },
214
+ "safeguards": {
215
+ "txLimit": "100",
216
+ "dailyLimit": "1000"
217
+ },
218
+ "mpp": {
219
+ "gatewayUrl": "https://mpp.cayvox.io",
220
+ "maxAutoPayPrice": "1.00"
221
+ }
222
+ }
223
+ ```
@@ -0,0 +1,15 @@
1
+ import {
2
+ addAmounts,
3
+ compareAmounts,
4
+ isValidAmount,
5
+ subtractAmounts,
6
+ toCantonAmount
7
+ } from "./chunk-GSDB5FKZ.js";
8
+ export {
9
+ addAmounts,
10
+ compareAmounts,
11
+ isValidAmount,
12
+ subtractAmounts,
13
+ toCantonAmount
14
+ };
15
+ //# sourceMappingURL=amount-L2SDLRZT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,110 @@
1
+ // src/canton/amount.ts
2
+ var AMOUNT_RE = /^\d+(\.\d+)?$/;
3
+ function isValidAmount(s) {
4
+ return AMOUNT_RE.test(s);
5
+ }
6
+ function parse(s) {
7
+ const dot = s.indexOf(".");
8
+ const intPart = dot === -1 ? s : s.slice(0, dot);
9
+ const fracPart = dot === -1 ? "" : s.slice(dot + 1);
10
+ return [
11
+ intPart.split("").map(Number),
12
+ fracPart.split("").map(Number)
13
+ ];
14
+ }
15
+ function align(a, b) {
16
+ const [aInt, aFrac] = parse(a);
17
+ const [bInt, bFrac] = parse(b);
18
+ const fracLen = Math.max(aFrac.length, bFrac.length);
19
+ while (aFrac.length < fracLen) aFrac.push(0);
20
+ while (bFrac.length < fracLen) bFrac.push(0);
21
+ const intLen = Math.max(aInt.length, bInt.length);
22
+ while (aInt.length < intLen) aInt.unshift(0);
23
+ while (bInt.length < intLen) bInt.unshift(0);
24
+ return [aInt, aFrac, bInt, bFrac, fracLen];
25
+ }
26
+ function formatResult(intDigits, fracDigits) {
27
+ while (fracDigits.length > 0 && fracDigits[fracDigits.length - 1] === 0) {
28
+ fracDigits.pop();
29
+ }
30
+ while (intDigits.length > 1 && intDigits[0] === 0) {
31
+ intDigits.shift();
32
+ }
33
+ const intStr = intDigits.join("");
34
+ return fracDigits.length > 0 ? `${intStr}.${fracDigits.join("")}` : intStr;
35
+ }
36
+ function compareAmounts(a, b) {
37
+ const [aInt, aFrac, bInt, bFrac] = align(a, b);
38
+ for (let i = 0; i < aInt.length; i++) {
39
+ if (aInt[i] < bInt[i]) return -1;
40
+ if (aInt[i] > bInt[i]) return 1;
41
+ }
42
+ for (let i = 0; i < aFrac.length; i++) {
43
+ if (aFrac[i] < bFrac[i]) return -1;
44
+ if (aFrac[i] > bFrac[i]) return 1;
45
+ }
46
+ return 0;
47
+ }
48
+ function addAmounts(a, b) {
49
+ const [aInt, aFrac, bInt, bFrac, fracLen] = align(a, b);
50
+ const aAll = [...aInt, ...aFrac];
51
+ const bAll = [...bInt, ...bFrac];
52
+ const result = new Array(aAll.length).fill(0);
53
+ let carry = 0;
54
+ for (let i = aAll.length - 1; i >= 0; i--) {
55
+ const sum = aAll[i] + bAll[i] + carry;
56
+ result[i] = sum % 10;
57
+ carry = Math.floor(sum / 10);
58
+ }
59
+ if (carry > 0) {
60
+ result.unshift(carry);
61
+ }
62
+ const intDigits = result.slice(0, result.length - fracLen);
63
+ const fracDigits = result.slice(result.length - fracLen);
64
+ return formatResult(
65
+ intDigits.length === 0 ? [0] : intDigits,
66
+ fracDigits
67
+ );
68
+ }
69
+ function subtractAmounts(a, b) {
70
+ if (compareAmounts(a, b) < 0) {
71
+ throw new Error(`Cannot subtract: ${a} < ${b}`);
72
+ }
73
+ const [aInt, aFrac, bInt, bFrac, fracLen] = align(a, b);
74
+ const aAll = [...aInt, ...aFrac];
75
+ const bAll = [...bInt, ...bFrac];
76
+ const result = new Array(aAll.length).fill(0);
77
+ let borrow = 0;
78
+ for (let i = aAll.length - 1; i >= 0; i--) {
79
+ let diff = aAll[i] - bAll[i] - borrow;
80
+ if (diff < 0) {
81
+ diff += 10;
82
+ borrow = 1;
83
+ } else {
84
+ borrow = 0;
85
+ }
86
+ result[i] = diff;
87
+ }
88
+ const intDigits = result.slice(0, result.length - fracLen);
89
+ const fracDigits = result.slice(result.length - fracLen);
90
+ return formatResult(
91
+ intDigits.length === 0 ? [0] : intDigits,
92
+ fracDigits
93
+ );
94
+ }
95
+ function toCantonAmount(s, decimals = 10) {
96
+ const dot = s.indexOf(".");
97
+ const intPart = dot === -1 ? s : s.slice(0, dot);
98
+ const fracPart = dot === -1 ? "" : s.slice(dot + 1);
99
+ const padded = fracPart.padEnd(decimals, "0").slice(0, decimals);
100
+ return `${intPart}.${padded}`;
101
+ }
102
+
103
+ export {
104
+ isValidAmount,
105
+ compareAmounts,
106
+ addAmounts,
107
+ subtractAmounts,
108
+ toCantonAmount
109
+ };
110
+ //# sourceMappingURL=chunk-GSDB5FKZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canton/amount.ts"],"sourcesContent":["/**\n * String-based decimal arithmetic for Canton amounts.\n *\n * Canton uses Numeric 10 (up to 10 decimal places). USDCx has 6 meaningful decimals.\n * ALL arithmetic is pure string manipulation — NO floating point.\n */\n\nconst AMOUNT_RE = /^\\d+(\\.\\d+)?$/;\n\n/**\n * Validate that a string is a valid non-negative decimal amount.\n */\nexport function isValidAmount(s: string): boolean {\n return AMOUNT_RE.test(s);\n}\n\n/**\n * Parse an amount string into integer and fractional digit arrays.\n * Returns [intDigits, fracDigits] where each is a number[].\n */\nfunction parse(s: string): [number[], number[]] {\n const dot = s.indexOf(\".\");\n const intPart = dot === -1 ? s : s.slice(0, dot);\n const fracPart = dot === -1 ? \"\" : s.slice(dot + 1);\n return [\n intPart.split(\"\").map(Number),\n fracPart.split(\"\").map(Number),\n ];\n}\n\n/**\n * Normalise two amount strings so their fractional parts have equal length.\n * Returns [aParts, bParts, fracLen].\n */\nfunction align(a: string, b: string): [number[], number[], number[], number[], number] {\n const [aInt, aFrac] = parse(a);\n const [bInt, bFrac] = parse(b);\n const fracLen = Math.max(aFrac.length, bFrac.length);\n while (aFrac.length < fracLen) aFrac.push(0);\n while (bFrac.length < fracLen) bFrac.push(0);\n // Also align integer parts to equal length by prepending zeros\n const intLen = Math.max(aInt.length, bInt.length);\n while (aInt.length < intLen) aInt.unshift(0);\n while (bInt.length < intLen) bInt.unshift(0);\n return [aInt, aFrac, bInt, bFrac, fracLen];\n}\n\n/**\n * Strip trailing zeros from fractional part and leading zeros from integer part.\n */\nfunction formatResult(intDigits: number[], fracDigits: number[]): string {\n // Remove trailing zeros from fraction\n while (fracDigits.length > 0 && fracDigits[fracDigits.length - 1] === 0) {\n fracDigits.pop();\n }\n // Remove leading zeros from integer (keep at least one)\n while (intDigits.length > 1 && intDigits[0] === 0) {\n intDigits.shift();\n }\n const intStr = intDigits.join(\"\");\n return fracDigits.length > 0 ? `${intStr}.${fracDigits.join(\"\")}` : intStr;\n}\n\n/**\n * Compare two decimal amount strings.\n * Returns -1 if a < b, 0 if a === b, 1 if a > b.\n */\nexport function compareAmounts(a: string, b: string): -1 | 0 | 1 {\n const [aInt, aFrac, bInt, bFrac] = align(a, b);\n\n // Compare integer parts digit by digit\n for (let i = 0; i < aInt.length; i++) {\n if (aInt[i] < bInt[i]) return -1;\n if (aInt[i] > bInt[i]) return 1;\n }\n\n // Compare fractional parts digit by digit\n for (let i = 0; i < aFrac.length; i++) {\n if (aFrac[i] < bFrac[i]) return -1;\n if (aFrac[i] > bFrac[i]) return 1;\n }\n\n return 0;\n}\n\n/**\n * Add two decimal amount strings. Returns the sum as a string.\n */\nexport function addAmounts(a: string, b: string): string {\n const [aInt, aFrac, bInt, bFrac, fracLen] = align(a, b);\n\n // Combine into single digit arrays (integer + fractional)\n const aAll = [...aInt, ...aFrac];\n const bAll = [...bInt, ...bFrac];\n\n const result: number[] = new Array(aAll.length).fill(0);\n let carry = 0;\n\n for (let i = aAll.length - 1; i >= 0; i--) {\n const sum = aAll[i] + bAll[i] + carry;\n result[i] = sum % 10;\n carry = Math.floor(sum / 10);\n }\n\n if (carry > 0) {\n result.unshift(carry);\n }\n\n // Split back into integer and fractional parts\n const intDigits = result.slice(0, result.length - fracLen);\n const fracDigits = result.slice(result.length - fracLen);\n\n return formatResult(\n intDigits.length === 0 ? [0] : intDigits,\n fracDigits,\n );\n}\n\n/**\n * Subtract b from a. Both must be valid amounts and a >= b.\n * Throws if a < b.\n */\nexport function subtractAmounts(a: string, b: string): string {\n if (compareAmounts(a, b) < 0) {\n throw new Error(`Cannot subtract: ${a} < ${b}`);\n }\n\n const [aInt, aFrac, bInt, bFrac, fracLen] = align(a, b);\n\n const aAll = [...aInt, ...aFrac];\n const bAll = [...bInt, ...bFrac];\n\n const result: number[] = new Array(aAll.length).fill(0);\n let borrow = 0;\n\n for (let i = aAll.length - 1; i >= 0; i--) {\n let diff = aAll[i] - bAll[i] - borrow;\n if (diff < 0) {\n diff += 10;\n borrow = 1;\n } else {\n borrow = 0;\n }\n result[i] = diff;\n }\n\n const intDigits = result.slice(0, result.length - fracLen);\n const fracDigits = result.slice(result.length - fracLen);\n\n return formatResult(\n intDigits.length === 0 ? [0] : intDigits,\n fracDigits,\n );\n}\n\n/**\n * Pad (or truncate) a decimal string to exactly N decimal places.\n * Default: 10 (Canton Numeric 10).\n */\nexport function toCantonAmount(s: string, decimals = 10): string {\n const dot = s.indexOf(\".\");\n const intPart = dot === -1 ? s : s.slice(0, dot);\n const fracPart = dot === -1 ? \"\" : s.slice(dot + 1);\n\n const padded = fracPart.padEnd(decimals, \"0\").slice(0, decimals);\n return `${intPart}.${padded}`;\n}\n"],"mappings":"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","names":[]}