@agirails/sdk 2.0.0-beta

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 (154) hide show
  1. package/README.md +183 -0
  2. package/dist/ACTPClient.d.ts +52 -0
  3. package/dist/ACTPClient.d.ts.map +1 -0
  4. package/dist/ACTPClient.js +120 -0
  5. package/dist/ACTPClient.js.map +1 -0
  6. package/dist/abi/ACTPKernel.json +1340 -0
  7. package/dist/abi/ERC20.json +38 -0
  8. package/dist/abi/EscrowVault.json +64 -0
  9. package/dist/builders/DeliveryProofBuilder.d.ts +37 -0
  10. package/dist/builders/DeliveryProofBuilder.d.ts.map +1 -0
  11. package/dist/builders/DeliveryProofBuilder.js +165 -0
  12. package/dist/builders/DeliveryProofBuilder.js.map +1 -0
  13. package/dist/builders/QuoteBuilder.d.ts +68 -0
  14. package/dist/builders/QuoteBuilder.d.ts.map +1 -0
  15. package/dist/builders/QuoteBuilder.js +255 -0
  16. package/dist/builders/QuoteBuilder.js.map +1 -0
  17. package/dist/builders/index.d.ts +3 -0
  18. package/dist/builders/index.d.ts.map +1 -0
  19. package/dist/builders/index.js +10 -0
  20. package/dist/builders/index.js.map +1 -0
  21. package/dist/config/networks.d.ts +27 -0
  22. package/dist/config/networks.d.ts.map +1 -0
  23. package/dist/config/networks.js +103 -0
  24. package/dist/config/networks.js.map +1 -0
  25. package/dist/errors/index.d.ts +38 -0
  26. package/dist/errors/index.d.ts.map +1 -0
  27. package/dist/errors/index.js +87 -0
  28. package/dist/errors/index.js.map +1 -0
  29. package/dist/index.d.ts +19 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +68 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/protocol/ACTPKernel.d.ts +30 -0
  34. package/dist/protocol/ACTPKernel.d.ts.map +1 -0
  35. package/dist/protocol/ACTPKernel.js +261 -0
  36. package/dist/protocol/ACTPKernel.js.map +1 -0
  37. package/dist/protocol/EASHelper.d.ts +23 -0
  38. package/dist/protocol/EASHelper.d.ts.map +1 -0
  39. package/dist/protocol/EASHelper.js +106 -0
  40. package/dist/protocol/EASHelper.js.map +1 -0
  41. package/dist/protocol/EscrowVault.d.ts +24 -0
  42. package/dist/protocol/EscrowVault.d.ts.map +1 -0
  43. package/dist/protocol/EscrowVault.js +114 -0
  44. package/dist/protocol/EscrowVault.js.map +1 -0
  45. package/dist/protocol/EventMonitor.d.ts +18 -0
  46. package/dist/protocol/EventMonitor.d.ts.map +1 -0
  47. package/dist/protocol/EventMonitor.js +92 -0
  48. package/dist/protocol/EventMonitor.js.map +1 -0
  49. package/dist/protocol/MessageSigner.d.ts +23 -0
  50. package/dist/protocol/MessageSigner.d.ts.map +1 -0
  51. package/dist/protocol/MessageSigner.js +178 -0
  52. package/dist/protocol/MessageSigner.js.map +1 -0
  53. package/dist/protocol/ProofGenerator.d.ts +22 -0
  54. package/dist/protocol/ProofGenerator.d.ts.map +1 -0
  55. package/dist/protocol/ProofGenerator.js +64 -0
  56. package/dist/protocol/ProofGenerator.js.map +1 -0
  57. package/dist/protocol/QuoteBuilder.d.ts +2 -0
  58. package/dist/protocol/QuoteBuilder.d.ts.map +1 -0
  59. package/dist/protocol/QuoteBuilder.js +7 -0
  60. package/dist/protocol/QuoteBuilder.js.map +1 -0
  61. package/dist/types/eip712.d.ts +106 -0
  62. package/dist/types/eip712.d.ts.map +1 -0
  63. package/dist/types/eip712.js +84 -0
  64. package/dist/types/eip712.js.map +1 -0
  65. package/dist/types/escrow.d.ts +18 -0
  66. package/dist/types/escrow.d.ts.map +1 -0
  67. package/dist/types/escrow.js +3 -0
  68. package/dist/types/escrow.js.map +1 -0
  69. package/dist/types/index.d.ts +6 -0
  70. package/dist/types/index.d.ts.map +1 -0
  71. package/dist/types/index.js +22 -0
  72. package/dist/types/index.js.map +1 -0
  73. package/dist/types/message.d.ts +109 -0
  74. package/dist/types/message.d.ts.map +1 -0
  75. package/dist/types/message.js +3 -0
  76. package/dist/types/message.js.map +1 -0
  77. package/dist/types/state.d.ts +19 -0
  78. package/dist/types/state.d.ts.map +1 -0
  79. package/dist/types/state.js +49 -0
  80. package/dist/types/state.js.map +1 -0
  81. package/dist/types/transaction.d.ts +36 -0
  82. package/dist/types/transaction.d.ts.map +1 -0
  83. package/dist/types/transaction.js +3 -0
  84. package/dist/types/transaction.js.map +1 -0
  85. package/dist/utils/IPFSClient.d.ts +37 -0
  86. package/dist/utils/IPFSClient.d.ts.map +1 -0
  87. package/dist/utils/IPFSClient.js +128 -0
  88. package/dist/utils/IPFSClient.js.map +1 -0
  89. package/dist/utils/NonceManager.d.ts +34 -0
  90. package/dist/utils/NonceManager.d.ts.map +1 -0
  91. package/dist/utils/NonceManager.js +114 -0
  92. package/dist/utils/NonceManager.js.map +1 -0
  93. package/dist/utils/ReceivedNonceTracker.d.ts +35 -0
  94. package/dist/utils/ReceivedNonceTracker.d.ts.map +1 -0
  95. package/dist/utils/ReceivedNonceTracker.js +196 -0
  96. package/dist/utils/ReceivedNonceTracker.js.map +1 -0
  97. package/dist/utils/canonicalJson.d.ts +4 -0
  98. package/dist/utils/canonicalJson.d.ts.map +1 -0
  99. package/dist/utils/canonicalJson.js +21 -0
  100. package/dist/utils/canonicalJson.js.map +1 -0
  101. package/dist/utils/computeTypeHash.d.ts +3 -0
  102. package/dist/utils/computeTypeHash.d.ts.map +1 -0
  103. package/dist/utils/computeTypeHash.js +30 -0
  104. package/dist/utils/computeTypeHash.js.map +1 -0
  105. package/dist/utils/validation.d.ts +6 -0
  106. package/dist/utils/validation.d.ts.map +1 -0
  107. package/dist/utils/validation.js +46 -0
  108. package/dist/utils/validation.js.map +1 -0
  109. package/package.json +73 -0
  110. package/src/ACTPClient.ts +276 -0
  111. package/src/__tests__/ProofGenerator.test.ts +124 -0
  112. package/src/__tests__/QuoteBuilder.test.ts +516 -0
  113. package/src/__tests__/StateMachine.test.ts +82 -0
  114. package/src/__tests__/builders/DeliveryProofBuilder.test.ts +581 -0
  115. package/src/__tests__/integration/ACTPClient.test.ts +263 -0
  116. package/src/__tests__/integration.test.ts +289 -0
  117. package/src/__tests__/protocol/EASHelper.test.ts +472 -0
  118. package/src/__tests__/protocol/EventMonitor.test.ts +382 -0
  119. package/src/__tests__/security/ACTPKernel.security.test.ts +1167 -0
  120. package/src/__tests__/security/EscrowVault.security.test.ts +570 -0
  121. package/src/__tests__/security/MessageSigner.security.test.ts +286 -0
  122. package/src/__tests__/security/NonceReplay.security.test.ts +501 -0
  123. package/src/__tests__/security/validation.security.test.ts +376 -0
  124. package/src/__tests__/utils/IPFSClient.test.ts +262 -0
  125. package/src/__tests__/utils/NonceManager.test.ts +205 -0
  126. package/src/__tests__/utils/canonicalJson.test.ts +153 -0
  127. package/src/abi/ACTPKernel.json +1340 -0
  128. package/src/abi/ERC20.json +40 -0
  129. package/src/abi/EscrowVault.json +66 -0
  130. package/src/builders/DeliveryProofBuilder.ts +326 -0
  131. package/src/builders/QuoteBuilder.ts +483 -0
  132. package/src/builders/index.ts +17 -0
  133. package/src/config/networks.ts +165 -0
  134. package/src/errors/index.ts +130 -0
  135. package/src/index.ts +108 -0
  136. package/src/protocol/ACTPKernel.ts +625 -0
  137. package/src/protocol/EASHelper.ts +197 -0
  138. package/src/protocol/EscrowVault.ts +237 -0
  139. package/src/protocol/EventMonitor.ts +161 -0
  140. package/src/protocol/MessageSigner.ts +336 -0
  141. package/src/protocol/ProofGenerator.ts +119 -0
  142. package/src/protocol/QuoteBuilder.ts +15 -0
  143. package/src/types/eip712.ts +175 -0
  144. package/src/types/escrow.ts +26 -0
  145. package/src/types/index.ts +10 -0
  146. package/src/types/message.ts +145 -0
  147. package/src/types/state.ts +77 -0
  148. package/src/types/transaction.ts +54 -0
  149. package/src/utils/IPFSClient.ts +248 -0
  150. package/src/utils/NonceManager.ts +293 -0
  151. package/src/utils/ReceivedNonceTracker.ts +397 -0
  152. package/src/utils/canonicalJson.ts +38 -0
  153. package/src/utils/computeTypeHash.ts +50 -0
  154. package/src/utils/validation.ts +82 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Nonce Manager Implementation
3
+ * Tracks nonces per DID + message type for AIP-4 delivery proofs
4
+ * Reference: AIP-4 §3.2 (nonce field requirement)
5
+ */
6
+
7
+ /**
8
+ * Nonce Manager Interface (from DeliveryProofBuilder)
9
+ */
10
+ export interface NonceManager {
11
+ /**
12
+ * Get next nonce for message type
13
+ * @param messageType - Message type identifier (e.g., "agirails.delivery.v1")
14
+ * @returns Monotonically increasing nonce
15
+ */
16
+ getNextNonce(messageType: string): number;
17
+
18
+ /**
19
+ * Record nonce usage
20
+ * @param messageType - Message type identifier
21
+ * @param nonce - Nonce used
22
+ */
23
+ recordNonce(messageType: string, nonce: number): void;
24
+
25
+ /**
26
+ * Get current nonce (last used)
27
+ * @param messageType - Message type identifier
28
+ * @returns Current nonce or 0 if none used
29
+ */
30
+ getCurrentNonce(messageType: string): number;
31
+
32
+ /**
33
+ * Reset nonce for message type
34
+ * @param messageType - Message type identifier
35
+ */
36
+ resetNonce(messageType: string): void;
37
+ }
38
+
39
+ /**
40
+ * In-Memory Nonce Manager
41
+ * Simple implementation using Map for per-message-type nonce tracking
42
+ *
43
+ * ⚠️ WARNING: Nonces are lost on process restart. For production:
44
+ * - Use persistent storage (Redis, PostgreSQL, etc.)
45
+ * - Implement nonce recovery from blockchain events
46
+ * - Add DID-scoped nonce tracking
47
+ */
48
+ export class InMemoryNonceManager implements NonceManager {
49
+ private nonces: Map<string, number> = new Map();
50
+
51
+ /**
52
+ * Create in-memory nonce manager
53
+ * @param initialNonces - Optional initial nonce values (for recovery)
54
+ */
55
+ constructor(initialNonces?: Record<string, number>) {
56
+ if (initialNonces) {
57
+ Object.entries(initialNonces).forEach(([messageType, nonce]) => {
58
+ this.nonces.set(messageType, nonce);
59
+ });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get next nonce for message type
65
+ * @param messageType - Message type identifier
66
+ * @returns Monotonically increasing nonce
67
+ */
68
+ getNextNonce(messageType: string): number {
69
+ const current = this.nonces.get(messageType) || 0;
70
+ return current + 1;
71
+ }
72
+
73
+ /**
74
+ * Record nonce usage
75
+ * @param messageType - Message type identifier
76
+ * @param nonce - Nonce used
77
+ */
78
+ recordNonce(messageType: string, nonce: number): void {
79
+ const current = this.nonces.get(messageType) || 0;
80
+
81
+ // Ensure monotonic increase
82
+ if (nonce <= current) {
83
+ throw new Error(
84
+ `Nonce must be strictly increasing: attempted ${nonce}, current is ${current}`
85
+ );
86
+ }
87
+
88
+ this.nonces.set(messageType, nonce);
89
+ }
90
+
91
+ /**
92
+ * Get current nonce (last used)
93
+ * @param messageType - Message type identifier
94
+ * @returns Current nonce or 0 if none used
95
+ */
96
+ getCurrentNonce(messageType: string): number {
97
+ return this.nonces.get(messageType) || 0;
98
+ }
99
+
100
+ /**
101
+ * Reset nonce for message type
102
+ * @param messageType - Message type identifier
103
+ */
104
+ resetNonce(messageType: string): void {
105
+ this.nonces.delete(messageType);
106
+ }
107
+
108
+ /**
109
+ * Get all nonces (for persistence)
110
+ * @returns Record of all message type nonces
111
+ */
112
+ getAllNonces(): Record<string, number> {
113
+ return Object.fromEntries(this.nonces.entries());
114
+ }
115
+
116
+ /**
117
+ * Clear all nonces
118
+ */
119
+ clearAll(): void {
120
+ this.nonces.clear();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * DID-Scoped Nonce Manager
126
+ * Tracks nonces per DID + message type combination
127
+ * Recommended for multi-agent scenarios
128
+ */
129
+ export class DIDScopedNonceManager implements NonceManager {
130
+ private nonces: Map<string, Map<string, number>> = new Map();
131
+ private currentDID: string;
132
+
133
+ /**
134
+ * Create DID-scoped nonce manager
135
+ * @param did - DID to track nonces for
136
+ * @param initialNonces - Optional initial nonce values
137
+ */
138
+ constructor(did: string, initialNonces?: Record<string, number>) {
139
+ this.currentDID = did;
140
+
141
+ if (initialNonces) {
142
+ const didNonces = new Map<string, number>();
143
+ Object.entries(initialNonces).forEach(([messageType, nonce]) => {
144
+ didNonces.set(messageType, nonce);
145
+ });
146
+ this.nonces.set(did, didNonces);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Get next nonce for message type (current DID)
152
+ * @param messageType - Message type identifier
153
+ * @returns Monotonically increasing nonce
154
+ */
155
+ getNextNonce(messageType: string): number {
156
+ return this.getNextNonceForDID(this.currentDID, messageType);
157
+ }
158
+
159
+ /**
160
+ * Record nonce usage (current DID)
161
+ * @param messageType - Message type identifier
162
+ * @param nonce - Nonce used
163
+ */
164
+ recordNonce(messageType: string, nonce: number): void {
165
+ this.recordNonceForDID(this.currentDID, messageType, nonce);
166
+ }
167
+
168
+ /**
169
+ * Get current nonce (current DID)
170
+ * @param messageType - Message type identifier
171
+ * @returns Current nonce or 0 if none used
172
+ */
173
+ getCurrentNonce(messageType: string): number {
174
+ return this.getCurrentNonceForDID(this.currentDID, messageType);
175
+ }
176
+
177
+ /**
178
+ * Reset nonce for message type (current DID)
179
+ * @param messageType - Message type identifier
180
+ */
181
+ resetNonce(messageType: string): void {
182
+ this.resetNonceForDID(this.currentDID, messageType);
183
+ }
184
+
185
+ /**
186
+ * Get next nonce for specific DID + message type
187
+ * @param did - DID identifier
188
+ * @param messageType - Message type identifier
189
+ * @returns Monotonically increasing nonce
190
+ */
191
+ getNextNonceForDID(did: string, messageType: string): number {
192
+ const didNonces = this.nonces.get(did);
193
+ if (!didNonces) {
194
+ return 1;
195
+ }
196
+ const current = didNonces.get(messageType) || 0;
197
+ return current + 1;
198
+ }
199
+
200
+ /**
201
+ * Record nonce usage for specific DID + message type
202
+ * @param did - DID identifier
203
+ * @param messageType - Message type identifier
204
+ * @param nonce - Nonce used
205
+ */
206
+ recordNonceForDID(did: string, messageType: string, nonce: number): void {
207
+ let didNonces = this.nonces.get(did);
208
+
209
+ if (!didNonces) {
210
+ didNonces = new Map<string, number>();
211
+ this.nonces.set(did, didNonces);
212
+ }
213
+
214
+ const current = didNonces.get(messageType) || 0;
215
+
216
+ // Ensure monotonic increase
217
+ if (nonce <= current) {
218
+ throw new Error(
219
+ `Nonce must be strictly increasing for ${did}: attempted ${nonce}, current is ${current}`
220
+ );
221
+ }
222
+
223
+ didNonces.set(messageType, nonce);
224
+ }
225
+
226
+ /**
227
+ * Get current nonce for specific DID + message type
228
+ * @param did - DID identifier
229
+ * @param messageType - Message type identifier
230
+ * @returns Current nonce or 0 if none used
231
+ */
232
+ getCurrentNonceForDID(did: string, messageType: string): number {
233
+ const didNonces = this.nonces.get(did);
234
+ return didNonces?.get(messageType) || 0;
235
+ }
236
+
237
+ /**
238
+ * Reset nonce for specific DID + message type
239
+ * @param did - DID identifier
240
+ * @param messageType - Message type identifier
241
+ */
242
+ resetNonceForDID(did: string, messageType: string): void {
243
+ const didNonces = this.nonces.get(did);
244
+ if (didNonces) {
245
+ didNonces.delete(messageType);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Switch current DID context
251
+ * @param did - New DID to track
252
+ */
253
+ switchDID(did: string): void {
254
+ this.currentDID = did;
255
+ }
256
+
257
+ /**
258
+ * Get all nonces for all DIDs (for persistence)
259
+ * @returns Nested record of DID → message type → nonce
260
+ */
261
+ getAllNonces(): Record<string, Record<string, number>> {
262
+ const result: Record<string, Record<string, number>> = {};
263
+
264
+ this.nonces.forEach((didNonces, did) => {
265
+ result[did] = Object.fromEntries(didNonces.entries());
266
+ });
267
+
268
+ return result;
269
+ }
270
+
271
+ /**
272
+ * Clear all nonces for all DIDs
273
+ */
274
+ clearAll(): void {
275
+ this.nonces.clear();
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Create nonce manager based on environment
281
+ * @param did - Optional DID for scoped tracking
282
+ * @param initialNonces - Optional initial nonces
283
+ * @returns NonceManager instance
284
+ */
285
+ export function createNonceManager(
286
+ did?: string,
287
+ initialNonces?: Record<string, number>
288
+ ): NonceManager {
289
+ if (did) {
290
+ return new DIDScopedNonceManager(did, initialNonces);
291
+ }
292
+ return new InMemoryNonceManager(initialNonces);
293
+ }
@@ -0,0 +1,397 @@
1
+ /**
2
+ * ReceivedNonceTracker - Replay Attack Prevention for Message Receivers
3
+ *
4
+ * This utility tracks nonces of received messages to prevent replay attacks.
5
+ * It works in conjunction with NonceManager (for senders) but serves the receiver side.
6
+ *
7
+ * Reference: V4 Security Vulnerability (EIP-712 Replay Attack)
8
+ *
9
+ * Usage Pattern:
10
+ * - Sender: Uses NonceManager to generate monotonically increasing nonces
11
+ * - Receiver: Uses ReceivedNonceTracker to validate and track received nonces
12
+ *
13
+ * Security Properties:
14
+ * 1. Nonces must be monotonically increasing per sender + message type
15
+ * 2. Duplicate nonces are rejected (replay attack prevention)
16
+ * 3. Nonces that are lower than the highest seen are rejected (old replay prevention)
17
+ *
18
+ * ⚠️ WARNING: In-memory tracking only. For production:
19
+ * - Use persistent storage (Redis, PostgreSQL, etc.)
20
+ * - Implement nonce recovery from transaction history
21
+ * - Consider nonce expiry for long-running processes
22
+ */
23
+
24
+ /**
25
+ * Nonce validation result
26
+ */
27
+ export interface NonceValidationResult {
28
+ valid: boolean;
29
+ reason?: string;
30
+ expectedMinimum?: string; // bytes32 format
31
+ receivedNonce?: string; // bytes32 format
32
+ }
33
+
34
+ /**
35
+ * Interface for tracking received nonces
36
+ */
37
+ export interface IReceivedNonceTracker {
38
+ /**
39
+ * Validate and record a received nonce
40
+ * @param sender - Sender DID (e.g., "did:ethr:0x...")
41
+ * @param messageType - Message type (e.g., "agirails.delivery.v1")
42
+ * @param nonce - Nonce value (bytes32 format: "0x...")
43
+ * @returns Validation result
44
+ */
45
+ validateAndRecord(sender: string, messageType: string, nonce: string): NonceValidationResult;
46
+
47
+ /**
48
+ * Check if a nonce has been used (without recording)
49
+ * @param sender - Sender DID
50
+ * @param messageType - Message type
51
+ * @param nonce - Nonce value (bytes32 format)
52
+ * @returns true if nonce was already used
53
+ */
54
+ hasBeenUsed(sender: string, messageType: string, nonce: string): boolean;
55
+
56
+ /**
57
+ * Get highest nonce seen for sender + message type
58
+ * @param sender - Sender DID
59
+ * @param messageType - Message type
60
+ * @returns Highest nonce (bytes32 format) or null if none seen
61
+ */
62
+ getHighestNonce(sender: string, messageType: string): string | null;
63
+
64
+ /**
65
+ * Reset tracking for a specific sender + message type
66
+ * @param sender - Sender DID
67
+ * @param messageType - Message type
68
+ */
69
+ reset(sender: string, messageType: string): void;
70
+
71
+ /**
72
+ * Clear all tracked nonces
73
+ */
74
+ clearAll(): void;
75
+ }
76
+
77
+ /**
78
+ * In-Memory Received Nonce Tracker
79
+ *
80
+ * Strategy: Track highest nonce seen per sender + message type
81
+ * - Accept nonces that are strictly greater than the highest seen
82
+ * - Reject nonces that are <= highest seen (replay attack)
83
+ *
84
+ * Trade-off:
85
+ * - Memory efficient (one value per sender + type)
86
+ * - Requires ordered nonce sequences
87
+ * - Cannot skip nonces (nonce gaps are rejected)
88
+ */
89
+ export class InMemoryReceivedNonceTracker implements IReceivedNonceTracker {
90
+ // Map: sender -> messageType -> highest nonce (as BigInt for comparison)
91
+ private highestNonces: Map<string, Map<string, bigint>> = new Map();
92
+
93
+ /**
94
+ * Validate and record a received nonce
95
+ */
96
+ validateAndRecord(sender: string, messageType: string, nonce: string): NonceValidationResult {
97
+ // Validate nonce format (must be bytes32: 0x + 64 hex chars)
98
+ if (!/^0x[a-fA-F0-9]{64}$/.test(nonce)) {
99
+ return {
100
+ valid: false,
101
+ reason: 'Invalid nonce format (must be bytes32)',
102
+ receivedNonce: nonce
103
+ };
104
+ }
105
+
106
+ // Convert nonce to BigInt for comparison
107
+ const nonceValue = BigInt(nonce);
108
+
109
+ // Get sender's nonce map
110
+ let senderNonces = this.highestNonces.get(sender);
111
+ if (!senderNonces) {
112
+ senderNonces = new Map<string, bigint>();
113
+ this.highestNonces.set(sender, senderNonces);
114
+ }
115
+
116
+ // Get highest nonce for this message type
117
+ const highestNonce = senderNonces.get(messageType);
118
+
119
+ if (highestNonce === undefined) {
120
+ // First message from this sender for this type
121
+ senderNonces.set(messageType, nonceValue);
122
+ return { valid: true };
123
+ }
124
+
125
+ // Nonce must be strictly greater than highest seen
126
+ if (nonceValue <= highestNonce) {
127
+ const expectedMinimum = '0x' + (highestNonce + BigInt(1)).toString(16).padStart(64, '0');
128
+ return {
129
+ valid: false,
130
+ reason: `Nonce replay detected: nonce must be > ${this.bigintToBytes32(highestNonce)}`,
131
+ expectedMinimum,
132
+ receivedNonce: nonce
133
+ };
134
+ }
135
+
136
+ // Valid nonce - record it
137
+ senderNonces.set(messageType, nonceValue);
138
+ return { valid: true };
139
+ }
140
+
141
+ /**
142
+ * Check if a nonce has been used (non-mutating)
143
+ */
144
+ hasBeenUsed(sender: string, messageType: string, nonce: string): boolean {
145
+ const nonceValue = BigInt(nonce);
146
+ const senderNonces = this.highestNonces.get(sender);
147
+
148
+ if (!senderNonces) {
149
+ return false; // No nonces seen from this sender
150
+ }
151
+
152
+ const highestNonce = senderNonces.get(messageType);
153
+ if (highestNonce === undefined) {
154
+ return false; // No nonces seen for this message type
155
+ }
156
+
157
+ // If the provided nonce is <= highest seen, it's been "used"
158
+ return nonceValue <= highestNonce;
159
+ }
160
+
161
+ /**
162
+ * Get highest nonce seen
163
+ */
164
+ getHighestNonce(sender: string, messageType: string): string | null {
165
+ const senderNonces = this.highestNonces.get(sender);
166
+ if (!senderNonces) {
167
+ return null;
168
+ }
169
+
170
+ const highestNonce = senderNonces.get(messageType);
171
+ if (highestNonce === undefined) {
172
+ return null;
173
+ }
174
+
175
+ return this.bigintToBytes32(highestNonce);
176
+ }
177
+
178
+ /**
179
+ * Reset tracking for sender + message type
180
+ */
181
+ reset(sender: string, messageType: string): void {
182
+ const senderNonces = this.highestNonces.get(sender);
183
+ if (senderNonces) {
184
+ senderNonces.delete(messageType);
185
+ // Clean up sender map if empty
186
+ if (senderNonces.size === 0) {
187
+ this.highestNonces.delete(sender);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Clear all tracked nonces
194
+ */
195
+ clearAll(): void {
196
+ this.highestNonces.clear();
197
+ }
198
+
199
+ /**
200
+ * Convert BigInt to bytes32 hex string
201
+ */
202
+ private bigintToBytes32(value: bigint): string {
203
+ return '0x' + value.toString(16).padStart(64, '0');
204
+ }
205
+
206
+ /**
207
+ * Get all nonces (for debugging/persistence)
208
+ */
209
+ getAllNonces(): Record<string, Record<string, string>> {
210
+ const result: Record<string, Record<string, string>> = {};
211
+
212
+ this.highestNonces.forEach((senderNonces, sender) => {
213
+ result[sender] = {};
214
+ senderNonces.forEach((nonce, messageType) => {
215
+ result[sender][messageType] = this.bigintToBytes32(nonce);
216
+ });
217
+ });
218
+
219
+ return result;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Set-Based Received Nonce Tracker
225
+ *
226
+ * Strategy: Track exact set of used nonces per sender + message type
227
+ * - Accept nonces that haven't been seen before
228
+ * - Reject duplicate nonces (replay attack)
229
+ * - Allows non-sequential nonces (nonce gaps are OK)
230
+ *
231
+ * Trade-off:
232
+ * - Higher memory usage (stores every nonce)
233
+ * - More flexible (allows out-of-order delivery)
234
+ * - Requires periodic cleanup to prevent unbounded growth
235
+ */
236
+ export class SetBasedReceivedNonceTracker implements IReceivedNonceTracker {
237
+ // Map: sender -> messageType -> Set of used nonces
238
+ private usedNonces: Map<string, Map<string, Set<string>>> = new Map();
239
+
240
+ /**
241
+ * Validate and record a received nonce
242
+ */
243
+ validateAndRecord(sender: string, messageType: string, nonce: string): NonceValidationResult {
244
+ // Validate nonce format
245
+ if (!/^0x[a-fA-F0-9]{64}$/.test(nonce)) {
246
+ return {
247
+ valid: false,
248
+ reason: 'Invalid nonce format (must be bytes32)',
249
+ receivedNonce: nonce
250
+ };
251
+ }
252
+
253
+ // Get sender's nonce map
254
+ let senderNonces = this.usedNonces.get(sender);
255
+ if (!senderNonces) {
256
+ senderNonces = new Map<string, Set<string>>();
257
+ this.usedNonces.set(sender, senderNonces);
258
+ }
259
+
260
+ // Get set of used nonces for this message type
261
+ let usedSet = senderNonces.get(messageType);
262
+ if (!usedSet) {
263
+ usedSet = new Set<string>();
264
+ senderNonces.set(messageType, usedSet);
265
+ }
266
+
267
+ // Check if nonce was already used
268
+ if (usedSet.has(nonce)) {
269
+ return {
270
+ valid: false,
271
+ reason: 'Nonce replay detected: this nonce has already been used',
272
+ receivedNonce: nonce
273
+ };
274
+ }
275
+
276
+ // Valid nonce - record it
277
+ usedSet.add(nonce);
278
+ return { valid: true };
279
+ }
280
+
281
+ /**
282
+ * Check if a nonce has been used
283
+ */
284
+ hasBeenUsed(sender: string, messageType: string, nonce: string): boolean {
285
+ const senderNonces = this.usedNonces.get(sender);
286
+ if (!senderNonces) {
287
+ return false;
288
+ }
289
+
290
+ const usedSet = senderNonces.get(messageType);
291
+ if (!usedSet) {
292
+ return false;
293
+ }
294
+
295
+ return usedSet.has(nonce);
296
+ }
297
+
298
+ /**
299
+ * Get highest nonce seen (for this strategy, compute from set)
300
+ */
301
+ getHighestNonce(sender: string, messageType: string): string | null {
302
+ const senderNonces = this.usedNonces.get(sender);
303
+ if (!senderNonces) {
304
+ return null;
305
+ }
306
+
307
+ const usedSet = senderNonces.get(messageType);
308
+ if (!usedSet || usedSet.size === 0) {
309
+ return null;
310
+ }
311
+
312
+ // Find maximum nonce in set
313
+ let maxNonce = BigInt(0);
314
+ usedSet.forEach(nonce => {
315
+ const value = BigInt(nonce);
316
+ if (value > maxNonce) {
317
+ maxNonce = value;
318
+ }
319
+ });
320
+
321
+ return '0x' + maxNonce.toString(16).padStart(64, '0');
322
+ }
323
+
324
+ /**
325
+ * Reset tracking for sender + message type
326
+ */
327
+ reset(sender: string, messageType: string): void {
328
+ const senderNonces = this.usedNonces.get(sender);
329
+ if (senderNonces) {
330
+ senderNonces.delete(messageType);
331
+ if (senderNonces.size === 0) {
332
+ this.usedNonces.delete(sender);
333
+ }
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Clear all tracked nonces
339
+ */
340
+ clearAll(): void {
341
+ this.usedNonces.clear();
342
+ }
343
+
344
+ /**
345
+ * Get nonce count for sender + message type (for monitoring)
346
+ */
347
+ getNonceCount(sender: string, messageType: string): number {
348
+ const senderNonces = this.usedNonces.get(sender);
349
+ if (!senderNonces) {
350
+ return 0;
351
+ }
352
+
353
+ const usedSet = senderNonces.get(messageType);
354
+ return usedSet ? usedSet.size : 0;
355
+ }
356
+
357
+ /**
358
+ * Cleanup old nonces (keep only last N)
359
+ * This prevents unbounded memory growth
360
+ */
361
+ cleanup(sender: string, messageType: string, keepLast: number = 1000): void {
362
+ const senderNonces = this.usedNonces.get(sender);
363
+ if (!senderNonces) {
364
+ return;
365
+ }
366
+
367
+ const usedSet = senderNonces.get(messageType);
368
+ if (!usedSet || usedSet.size <= keepLast) {
369
+ return;
370
+ }
371
+
372
+ // Convert to array and sort by nonce value
373
+ const sortedNonces = Array.from(usedSet).sort((a, b) => {
374
+ const aVal = BigInt(a);
375
+ const bVal = BigInt(b);
376
+ return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
377
+ });
378
+
379
+ // Keep only the last N nonces
380
+ const toKeep = new Set(sortedNonces.slice(-keepLast));
381
+ senderNonces.set(messageType, toKeep);
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Factory function to create a nonce tracker
387
+ * @param strategy - 'memory-efficient' (highest nonce) or 'set-based' (all nonces)
388
+ * @returns IReceivedNonceTracker instance
389
+ */
390
+ export function createReceivedNonceTracker(
391
+ strategy: 'memory-efficient' | 'set-based' = 'memory-efficient'
392
+ ): IReceivedNonceTracker {
393
+ if (strategy === 'set-based') {
394
+ return new SetBasedReceivedNonceTracker();
395
+ }
396
+ return new InMemoryReceivedNonceTracker();
397
+ }