@0xsequence/wallet-primitives 0.0.0-20250520201059

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 (96) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +7 -0
  3. package/LICENSE +202 -0
  4. package/dist/address.d.ts +5 -0
  5. package/dist/address.d.ts.map +1 -0
  6. package/dist/address.js +7 -0
  7. package/dist/address.js.map +1 -0
  8. package/dist/attestation.d.ts +24 -0
  9. package/dist/attestation.d.ts.map +1 -0
  10. package/dist/attestation.js +77 -0
  11. package/dist/attestation.js.map +1 -0
  12. package/dist/config.d.ts +85 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +381 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/constants.d.ts +173 -0
  17. package/dist/constants.d.ts.map +1 -0
  18. package/dist/constants.js +31 -0
  19. package/dist/constants.js.map +1 -0
  20. package/dist/context.d.ts +9 -0
  21. package/dist/context.d.ts.map +1 -0
  22. package/dist/context.js +8 -0
  23. package/dist/context.js.map +1 -0
  24. package/dist/erc-6492.d.ts +19 -0
  25. package/dist/erc-6492.d.ts.map +1 -0
  26. package/dist/erc-6492.js +64 -0
  27. package/dist/erc-6492.js.map +1 -0
  28. package/dist/extensions/index.d.ts +9 -0
  29. package/dist/extensions/index.d.ts.map +1 -0
  30. package/dist/extensions/index.js +7 -0
  31. package/dist/extensions/index.js.map +1 -0
  32. package/dist/extensions/passkeys.d.ts +31 -0
  33. package/dist/extensions/passkeys.d.ts.map +1 -0
  34. package/dist/extensions/passkeys.js +224 -0
  35. package/dist/extensions/passkeys.js.map +1 -0
  36. package/dist/extensions/recovery.d.ts +310 -0
  37. package/dist/extensions/recovery.d.ts.map +1 -0
  38. package/dist/extensions/recovery.js +444 -0
  39. package/dist/extensions/recovery.js.map +1 -0
  40. package/dist/generic-tree.d.ts +14 -0
  41. package/dist/generic-tree.d.ts.map +1 -0
  42. package/dist/generic-tree.js +34 -0
  43. package/dist/generic-tree.js.map +1 -0
  44. package/dist/index.d.ts +16 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +16 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/network.d.ts +15 -0
  49. package/dist/network.d.ts.map +1 -0
  50. package/dist/network.js +24 -0
  51. package/dist/network.js.map +1 -0
  52. package/dist/payload.d.ts +108 -0
  53. package/dist/payload.d.ts.map +1 -0
  54. package/dist/payload.js +627 -0
  55. package/dist/payload.js.map +1 -0
  56. package/dist/permission.d.ts +73 -0
  57. package/dist/permission.d.ts.map +1 -0
  58. package/dist/permission.js +188 -0
  59. package/dist/permission.js.map +1 -0
  60. package/dist/session-config.d.ts +113 -0
  61. package/dist/session-config.d.ts.map +1 -0
  62. package/dist/session-config.js +554 -0
  63. package/dist/session-config.js.map +1 -0
  64. package/dist/session-signature.d.ts +24 -0
  65. package/dist/session-signature.d.ts.map +1 -0
  66. package/dist/session-signature.js +141 -0
  67. package/dist/session-signature.js.map +1 -0
  68. package/dist/signature.d.ts +108 -0
  69. package/dist/signature.d.ts.map +1 -0
  70. package/dist/signature.js +1079 -0
  71. package/dist/signature.js.map +1 -0
  72. package/dist/utils.d.ts +45 -0
  73. package/dist/utils.d.ts.map +1 -0
  74. package/dist/utils.js +100 -0
  75. package/dist/utils.js.map +1 -0
  76. package/eslint.config.mjs +4 -0
  77. package/package.json +27 -0
  78. package/src/address.ts +19 -0
  79. package/src/attestation.ts +114 -0
  80. package/src/config.ts +521 -0
  81. package/src/constants.ts +39 -0
  82. package/src/context.ts +16 -0
  83. package/src/erc-6492.ts +97 -0
  84. package/src/extensions/index.ts +14 -0
  85. package/src/extensions/passkeys.ts +283 -0
  86. package/src/extensions/recovery.ts +542 -0
  87. package/src/generic-tree.ts +55 -0
  88. package/src/index.ts +15 -0
  89. package/src/network.ts +37 -0
  90. package/src/payload.ts +825 -0
  91. package/src/permission.ts +252 -0
  92. package/src/session-config.ts +681 -0
  93. package/src/session-signature.ts +197 -0
  94. package/src/signature.ts +1398 -0
  95. package/src/utils.ts +114 -0
  96. package/tsconfig.json +10 -0
@@ -0,0 +1,681 @@
1
+ import { Address, Bytes, Hash, Hex } from 'ox'
2
+ import * as GenericTree from './generic-tree.js'
3
+ import {
4
+ decodeSessionPermissions,
5
+ encodeSessionPermissions,
6
+ encodeSessionPermissionsForJson,
7
+ SessionPermissions,
8
+ sessionPermissionsFromParsed,
9
+ } from './permission.js'
10
+ import { minBytesFor } from './utils.js'
11
+
12
+ //FIXME Reorder by expected usage
13
+ export const SESSIONS_FLAG_PERMISSIONS = 0
14
+ export const SESSIONS_FLAG_NODE = 1
15
+ export const SESSIONS_FLAG_BRANCH = 2
16
+ export const SESSIONS_FLAG_BLACKLIST = 3
17
+ export const SESSIONS_FLAG_IDENTITY_SIGNER = 4
18
+
19
+ export type ImplicitBlacklistLeaf = {
20
+ type: 'implicit-blacklist'
21
+ blacklist: Address.Address[]
22
+ }
23
+
24
+ export type IdentitySignerLeaf = {
25
+ type: 'identity-signer'
26
+ identitySigner: Address.Address
27
+ }
28
+
29
+ export type SessionPermissionsLeaf = SessionPermissions & {
30
+ type: 'session-permissions'
31
+ }
32
+
33
+ export type SessionNode = Hex.Hex // Hashed leaf
34
+ export type SessionLeaf = SessionPermissionsLeaf | ImplicitBlacklistLeaf | IdentitySignerLeaf
35
+ export type SessionBranch = [SessionsTopology, SessionsTopology, ...SessionsTopology[]]
36
+ export type SessionsTopology = SessionBranch | SessionLeaf | SessionNode
37
+
38
+ function isSessionsNode(topology: any): topology is SessionNode {
39
+ return Hex.validate(topology) && Hex.size(topology) === 32
40
+ }
41
+
42
+ function isImplicitBlacklist(topology: any): topology is ImplicitBlacklistLeaf {
43
+ return typeof topology === 'object' && topology !== null && 'blacklist' in topology
44
+ }
45
+
46
+ function isIdentitySignerLeaf(topology: any): topology is IdentitySignerLeaf {
47
+ return typeof topology === 'object' && topology !== null && 'identitySigner' in topology
48
+ }
49
+
50
+ function isSessionPermissions(topology: any): topology is SessionPermissionsLeaf {
51
+ return typeof topology === 'object' && topology !== null && 'signer' in topology
52
+ }
53
+
54
+ function isSessionsLeaf(topology: any): topology is SessionLeaf {
55
+ return isImplicitBlacklist(topology) || isIdentitySignerLeaf(topology) || isSessionPermissions(topology)
56
+ }
57
+
58
+ function isSessionsBranch(topology: any): topology is SessionBranch {
59
+ return Array.isArray(topology) && topology.length >= 2 && topology.every((child) => isSessionsTopology(child))
60
+ }
61
+
62
+ export function isSessionsTopology(topology: any): topology is SessionsTopology {
63
+ return isSessionsBranch(topology) || isSessionsLeaf(topology) || isSessionsNode(topology)
64
+ }
65
+
66
+ /**
67
+ * Checks if the topology is complete.
68
+ * A complete topology has exactly one identity signer and one blacklist.
69
+ * @param topology The topology to check
70
+ * @returns True if the topology is complete
71
+ */
72
+ export function isCompleteSessionsTopology(topology: any): topology is SessionsTopology {
73
+ // Ensure the object is a sessions topology
74
+ if (!isSessionsTopology(topology)) {
75
+ return false
76
+ }
77
+ // Check the topology contains exactly one identity signer and one blacklist
78
+ const { identitySignerCount, blacklistCount } = checkIsCompleteSessionsBranch(topology)
79
+ return identitySignerCount === 1 && blacklistCount === 1
80
+ }
81
+
82
+ function checkIsCompleteSessionsBranch(topology: SessionsTopology): {
83
+ identitySignerCount: number
84
+ blacklistCount: number
85
+ } {
86
+ let thisHasIdentitySigner = 0
87
+ let thisHasBlacklist = 0
88
+ if (isSessionsBranch(topology)) {
89
+ for (const child of topology) {
90
+ const { identitySignerCount, blacklistCount } = checkIsCompleteSessionsBranch(child)
91
+ thisHasIdentitySigner += identitySignerCount
92
+ thisHasBlacklist += blacklistCount
93
+ }
94
+ }
95
+ if (isIdentitySignerLeaf(topology)) {
96
+ thisHasIdentitySigner++
97
+ }
98
+ if (isImplicitBlacklist(topology)) {
99
+ thisHasBlacklist++
100
+ }
101
+ return { identitySignerCount: thisHasIdentitySigner, blacklistCount: thisHasBlacklist }
102
+ }
103
+
104
+ /**
105
+ * Gets the identity signer from the topology.
106
+ * @param topology The topology to get the identity signer from
107
+ * @returns The identity signer or null if it's not present
108
+ */
109
+ export function getIdentitySigner(topology: SessionsTopology): Address.Address | null {
110
+ if (isIdentitySignerLeaf(topology)) {
111
+ // Got it
112
+ return topology.identitySigner
113
+ }
114
+
115
+ if (isSessionsBranch(topology)) {
116
+ // Check branches
117
+ const results = topology.map(getIdentitySigner).filter((t) => t !== null)
118
+ if (results.length > 1) {
119
+ throw new Error('Multiple identity signers')
120
+ }
121
+ if (results.length === 1) {
122
+ return results[0]!
123
+ }
124
+ }
125
+
126
+ return null
127
+ }
128
+
129
+ /**
130
+ * Gets the implicit blacklist from the topology.
131
+ * @param topology The topology to get the implicit blacklist from
132
+ * @returns The implicit blacklist or null if it's not present
133
+ */
134
+ export function getImplicitBlacklist(topology: SessionsTopology): Address.Address[] | null {
135
+ const blacklistNode = getImplicitBlacklistLeaf(topology)
136
+ if (!blacklistNode) {
137
+ return null
138
+ }
139
+ return blacklistNode.blacklist
140
+ }
141
+
142
+ /**
143
+ * Gets the implicit blacklist leaf from the topology.
144
+ * @param topology The topology to get the implicit blacklist leaf from
145
+ * @returns The implicit blacklist leaf or null if it's not present
146
+ */
147
+ export function getImplicitBlacklistLeaf(topology: SessionsTopology): ImplicitBlacklistLeaf | null {
148
+ if (isImplicitBlacklist(topology)) {
149
+ // Got it
150
+ return topology
151
+ }
152
+
153
+ if (isSessionsBranch(topology)) {
154
+ // Check branches
155
+ const results = topology.map(getImplicitBlacklistLeaf).filter((t) => t !== null)
156
+ if (results.length > 1) {
157
+ throw new Error('Multiple blacklists')
158
+ }
159
+ if (results.length === 1) {
160
+ return results[0]!
161
+ }
162
+ }
163
+
164
+ return null
165
+ }
166
+
167
+ export function getSessionPermissions(topology: SessionsTopology, address: Address.Address): SessionPermissions | null {
168
+ if (isSessionPermissions(topology)) {
169
+ if (Address.isEqual(topology.signer, address)) {
170
+ return topology
171
+ }
172
+ }
173
+ if (isSessionsBranch(topology)) {
174
+ for (const child of topology) {
175
+ const result = getSessionPermissions(child, address)
176
+ if (result) {
177
+ return result
178
+ }
179
+ }
180
+ }
181
+ return null
182
+ }
183
+
184
+ export function getExplicitSigners(topology: SessionsTopology): Address.Address[] {
185
+ return getExplicitSignersFromBranch(topology, [])
186
+ }
187
+
188
+ function getExplicitSignersFromBranch(topology: SessionsTopology, current: Address.Address[]): Address.Address[] {
189
+ if (isSessionPermissions(topology)) {
190
+ return [...current, topology.signer]
191
+ }
192
+ if (isSessionsBranch(topology)) {
193
+ const result: Address.Address[] = [...current]
194
+ for (const child of topology) {
195
+ result.push(...getExplicitSignersFromBranch(child, current))
196
+ }
197
+ return result
198
+ }
199
+ return current
200
+ }
201
+
202
+ // Encode / decode to configuration tree
203
+
204
+ /**
205
+ * Encodes a leaf to bytes.
206
+ * This can be Hash.keccak256'd to convert to a node..
207
+ * @param leaf The leaf to encode
208
+ * @returns The encoded leaf
209
+ */
210
+ export function encodeLeafToGeneric(leaf: SessionLeaf): GenericTree.Leaf {
211
+ if (isSessionPermissions(leaf)) {
212
+ return {
213
+ type: 'leaf',
214
+ value: Bytes.concat(Bytes.fromNumber(SESSIONS_FLAG_PERMISSIONS), encodeSessionPermissions(leaf)),
215
+ }
216
+ }
217
+ if (isImplicitBlacklist(leaf)) {
218
+ return {
219
+ type: 'leaf',
220
+ value: Bytes.concat(
221
+ Bytes.fromNumber(SESSIONS_FLAG_BLACKLIST),
222
+ Bytes.concat(...leaf.blacklist.map((b) => Bytes.padLeft(Bytes.fromHex(b), 20))),
223
+ ),
224
+ }
225
+ }
226
+ if (isIdentitySignerLeaf(leaf)) {
227
+ return {
228
+ type: 'leaf',
229
+ value: Bytes.concat(
230
+ Bytes.fromNumber(SESSIONS_FLAG_IDENTITY_SIGNER),
231
+ Bytes.padLeft(Bytes.fromHex(leaf.identitySigner), 20),
232
+ ),
233
+ }
234
+ }
235
+ // Unreachable
236
+ throw new Error('Invalid leaf')
237
+ }
238
+
239
+ export function decodeLeafFromBytes(bytes: Bytes.Bytes): SessionLeaf {
240
+ const flag = bytes[0]!
241
+ if (flag === SESSIONS_FLAG_BLACKLIST) {
242
+ const blacklist: `0x${string}`[] = []
243
+ for (let i = 1; i < bytes.length; i += 20) {
244
+ blacklist.push(Bytes.toHex(bytes.slice(i, i + 20)))
245
+ }
246
+ return { type: 'implicit-blacklist', blacklist }
247
+ }
248
+ if (flag === SESSIONS_FLAG_IDENTITY_SIGNER) {
249
+ return { type: 'identity-signer', identitySigner: Bytes.toHex(bytes.slice(1, 21)) }
250
+ }
251
+ if (flag === SESSIONS_FLAG_PERMISSIONS) {
252
+ return { type: 'session-permissions', ...decodeSessionPermissions(bytes.slice(1)) }
253
+ }
254
+ throw new Error('Invalid leaf')
255
+ }
256
+
257
+ export function sessionsTopologyToConfigurationTree(topology: SessionsTopology): GenericTree.Tree {
258
+ if (isSessionsBranch(topology)) {
259
+ return topology.map(sessionsTopologyToConfigurationTree) as GenericTree.Branch
260
+ }
261
+ if (isImplicitBlacklist(topology) || isIdentitySignerLeaf(topology) || isSessionPermissions(topology)) {
262
+ return encodeLeafToGeneric(topology)
263
+ }
264
+ if (isSessionsNode(topology)) {
265
+ // A node is already encoded and hashed
266
+ return topology
267
+ }
268
+ throw new Error('Invalid topology')
269
+ }
270
+
271
+ export function configurationTreeToSessionsTopology(tree: GenericTree.Tree): SessionsTopology {
272
+ if (GenericTree.isBranch(tree)) {
273
+ return tree.map(configurationTreeToSessionsTopology) as SessionBranch
274
+ }
275
+
276
+ if (GenericTree.isNode(tree)) {
277
+ throw new Error('Unknown in configuration tree')
278
+ }
279
+
280
+ return decodeLeafFromBytes(tree.value)
281
+ }
282
+
283
+ // Encoding for contract validation
284
+
285
+ /**
286
+ * Encodes a topology into bytes for contract validation.
287
+ * @param topology The topology to encode
288
+ * @returns The encoded topology
289
+ */
290
+ export function encodeSessionsTopology(topology: SessionsTopology): Bytes.Bytes {
291
+ if (isSessionsBranch(topology)) {
292
+ const encodedBranches = []
293
+ for (const node of topology) {
294
+ encodedBranches.push(encodeSessionsTopology(node))
295
+ }
296
+ const encoded = Bytes.concat(...encodedBranches)
297
+ const encodedSize = minBytesFor(BigInt(encoded.length))
298
+ if (encodedSize > 15) {
299
+ throw new Error('Branch too large')
300
+ }
301
+ const flagByte = (SESSIONS_FLAG_BRANCH << 4) | encodedSize
302
+ return Bytes.concat(
303
+ Bytes.fromNumber(flagByte),
304
+ Bytes.padLeft(Bytes.fromNumber(encoded.length), encodedSize),
305
+ encoded,
306
+ )
307
+ }
308
+
309
+ if (isSessionPermissions(topology)) {
310
+ const flagByte = SESSIONS_FLAG_PERMISSIONS << 4
311
+ const encodedLeaf = encodeSessionPermissions(topology)
312
+ return Bytes.concat(Bytes.fromNumber(flagByte), encodedLeaf)
313
+ }
314
+
315
+ if (isSessionsNode(topology)) {
316
+ const flagByte = SESSIONS_FLAG_NODE << 4
317
+ return Bytes.concat(Bytes.fromNumber(flagByte), Hex.toBytes(topology))
318
+ }
319
+
320
+ if (isImplicitBlacklist(topology)) {
321
+ const encoded = Bytes.concat(...topology.blacklist.map((b) => Bytes.fromHex(b)))
322
+ if (topology.blacklist.length >= 0x0f) {
323
+ // If the blacklist is too large, we can't encode the length into the flag byte.
324
+ // Instead we encode 0x0f and the length in the next 2 bytes.
325
+ if (topology.blacklist.length > 0xffff) {
326
+ throw new Error('Blacklist too large')
327
+ }
328
+ return Bytes.concat(
329
+ Bytes.fromNumber((SESSIONS_FLAG_BLACKLIST << 4) | 0x0f),
330
+ Bytes.fromNumber(topology.blacklist.length, { size: 2 }),
331
+ encoded,
332
+ )
333
+ }
334
+ // Encode the size into the flag byte
335
+ const flagByte = (SESSIONS_FLAG_BLACKLIST << 4) | topology.blacklist.length
336
+ return Bytes.concat(Bytes.fromNumber(flagByte), encoded)
337
+ }
338
+
339
+ if (isIdentitySignerLeaf(topology)) {
340
+ const flagByte = SESSIONS_FLAG_IDENTITY_SIGNER << 4
341
+ return Bytes.concat(Bytes.fromNumber(flagByte), Bytes.padLeft(Bytes.fromHex(topology.identitySigner), 20))
342
+ }
343
+
344
+ throw new Error('Invalid topology')
345
+ }
346
+
347
+ // JSON
348
+
349
+ export function sessionsTopologyToJson(topology: SessionsTopology): string {
350
+ return JSON.stringify(encodeSessionsTopologyForJson(topology))
351
+ }
352
+
353
+ function encodeSessionsTopologyForJson(topology: SessionsTopology): any {
354
+ if (isSessionsNode(topology)) {
355
+ return topology
356
+ }
357
+
358
+ if (isSessionPermissions(topology)) {
359
+ return encodeSessionPermissionsForJson(topology)
360
+ }
361
+
362
+ if (isImplicitBlacklist(topology) || isIdentitySignerLeaf(topology)) {
363
+ return topology // No encoding necessary
364
+ }
365
+
366
+ if (isSessionsBranch(topology)) {
367
+ return topology.map((node) => encodeSessionsTopologyForJson(node))
368
+ }
369
+
370
+ throw new Error('Invalid topology')
371
+ }
372
+
373
+ export function sessionsTopologyFromJson(json: string): SessionsTopology {
374
+ const parsed = JSON.parse(json)
375
+ return sessionsTopologyFromParsed(parsed)
376
+ }
377
+
378
+ function sessionsTopologyFromParsed(parsed: any): SessionsTopology {
379
+ // Parse branch
380
+ if (Array.isArray(parsed)) {
381
+ const branches = parsed.map((node: any) => sessionsTopologyFromParsed(node))
382
+ return branches as SessionBranch
383
+ }
384
+
385
+ // Parse node
386
+ if (typeof parsed === 'string' && Hex.validate(parsed) && Hex.size(parsed) === 32) {
387
+ return parsed
388
+ }
389
+
390
+ // Parse permissions
391
+ if (
392
+ typeof parsed === 'object' &&
393
+ parsed !== null &&
394
+ 'signer' in parsed &&
395
+ 'valueLimit' in parsed &&
396
+ 'deadline' in parsed &&
397
+ 'permissions' in parsed
398
+ ) {
399
+ return { type: 'session-permissions', ...sessionPermissionsFromParsed(parsed) }
400
+ }
401
+
402
+ // Parse identity signer
403
+ if (typeof parsed === 'object' && parsed !== null && 'identitySigner' in parsed) {
404
+ const identitySigner = parsed.identitySigner as `0x${string}`
405
+ return { type: 'identity-signer', identitySigner }
406
+ }
407
+
408
+ // Parse blacklist
409
+ if (typeof parsed === 'object' && parsed !== null && 'blacklist' in parsed) {
410
+ const blacklist = parsed.blacklist.map((address: any) => Address.from(address))
411
+ return { type: 'implicit-blacklist', blacklist }
412
+ }
413
+
414
+ throw new Error('Invalid topology')
415
+ }
416
+
417
+ // Operations
418
+
419
+ /**
420
+ * Removes all explicit sessions (permissions leaf nodes) that match the given signer from the topology.
421
+ * Returns the updated topology or null if it becomes empty (for nesting).
422
+ * If the signer is not found, the topology is returned unchanged.
423
+ */
424
+ export function removeExplicitSession(
425
+ topology: SessionsTopology,
426
+ signerAddress: `0x${string}`,
427
+ ): SessionsTopology | null {
428
+ if (isSessionPermissions(topology)) {
429
+ if (Address.isEqual(topology.signer, signerAddress)) {
430
+ return null
431
+ }
432
+ // Return the leaf unchanged
433
+ return topology
434
+ }
435
+
436
+ // If it's a branch, recurse on each child:
437
+ if (isSessionsBranch(topology)) {
438
+ const newChildren: SessionsTopology[] = []
439
+ for (const child of topology) {
440
+ const updatedChild = removeExplicitSession(child, signerAddress)
441
+ if (updatedChild != null) {
442
+ newChildren.push(updatedChild)
443
+ }
444
+ }
445
+
446
+ // If no children remain, return null to remove entire branch
447
+ if (newChildren.length === 0) {
448
+ return null
449
+ }
450
+
451
+ // If exactly one child remains, collapse upward
452
+ if (newChildren.length === 1) {
453
+ return newChildren[0]!
454
+ }
455
+
456
+ // Otherwise, return the updated branch
457
+ return newChildren as SessionBranch
458
+ }
459
+
460
+ // Other leaf, return unchanged
461
+ return topology
462
+ }
463
+
464
+ export function addExplicitSession(
465
+ topology: SessionsTopology,
466
+ sessionPermissions: SessionPermissions,
467
+ ): SessionsTopology {
468
+ // Find the session in the topology
469
+ if (getSessionPermissions(topology, sessionPermissions.signer)) {
470
+ throw new Error('Session already exists')
471
+ }
472
+ // Merge and balance
473
+ const merged = mergeSessionsTopologies(topology, { type: 'session-permissions', ...sessionPermissions })
474
+ return balanceSessionsTopology(merged)
475
+ }
476
+
477
+ /**
478
+ * Merges two topologies into a new branch of [a, b].
479
+ */
480
+ export function mergeSessionsTopologies(a: SessionsTopology, b: SessionsTopology): SessionsTopology {
481
+ return [a, b]
482
+ }
483
+
484
+ /**
485
+ * Helper to flatten a topology into an array of leaves and nodes only.
486
+ * We ignore branches by recursing into them.
487
+ */
488
+ function flattenSessionsTopology(topology: SessionsTopology): (SessionLeaf | SessionNode)[] {
489
+ if (isSessionsLeaf(topology) || isSessionsNode(topology)) {
490
+ return [topology]
491
+ }
492
+ // If it's a branch, flatten all children
493
+ const result: (SessionLeaf | SessionNode)[] = []
494
+ for (const child of topology) {
495
+ result.push(...flattenSessionsTopology(child))
496
+ }
497
+ return result
498
+ }
499
+
500
+ /**
501
+ * Helper to build a balanced binary tree from an array of leaves/nodes.
502
+ * This function returns:
503
+ * - A single leaf/node if there's only 1 item
504
+ * - A branch of two subtrees otherwise
505
+ */
506
+ function buildBalancedSessionsTopology(items: (SessionLeaf | SessionNode)[]): SessionsTopology {
507
+ if (items.length === 1) {
508
+ return items[0]!
509
+ }
510
+ if (items.length === 0) {
511
+ throw new Error('Cannot build a topology from an empty list')
512
+ }
513
+ const mid = Math.floor(items.length / 2)
514
+ const left = items.slice(0, mid)
515
+ const right = items.slice(mid)
516
+ // Recursively build subtrees
517
+ const leftTopo = buildBalancedSessionsTopology(left)
518
+ const rightTopo = buildBalancedSessionsTopology(right)
519
+ return [leftTopo, rightTopo]
520
+ }
521
+
522
+ /**
523
+ * Balances the topology by flattening and rebuilding as a balanced binary tree.
524
+ * This does not make a binary tree as the blacklist and identity signer are included at the top level.
525
+ */
526
+ export function balanceSessionsTopology(topology: SessionsTopology): SessionsTopology {
527
+ const flattened = flattenSessionsTopology(topology)
528
+ const blacklist = flattened.find((l) => isImplicitBlacklist(l))
529
+ const identitySigner = flattened.find((l) => isIdentitySignerLeaf(l))
530
+ const leaves = flattened.filter((l) => isSessionPermissions(l))
531
+ if (!blacklist || !identitySigner) {
532
+ throw new Error('No blacklist or identity signer')
533
+ }
534
+ return buildBalancedSessionsTopology([blacklist, identitySigner, ...leaves])
535
+ }
536
+
537
+ /**
538
+ * Cleans a topology by removing leaves (SessionPermissions) whose deadline has expired.
539
+ * - currentTime is compared against `session.deadline`.
540
+ * - If a branch ends up with zero valid leaves, return `null`.
541
+ * - If it has one child, collapse that child upward.
542
+ */
543
+ export function cleanSessionsTopology(
544
+ topology: SessionsTopology,
545
+ currentTime: bigint = BigInt(Math.floor(Date.now() / 1000)),
546
+ ): SessionsTopology | null {
547
+ // If it's a node, just return it as is.
548
+ if (isSessionsNode(topology)) {
549
+ return topology
550
+ }
551
+
552
+ // If it's a leaf, check the deadline
553
+ if (isSessionPermissions(topology)) {
554
+ if (topology.deadline < currentTime) {
555
+ // Expired => remove
556
+ return null
557
+ }
558
+ // Valid => keep
559
+ return topology
560
+ }
561
+
562
+ if (isIdentitySignerLeaf(topology) || isImplicitBlacklist(topology)) {
563
+ return topology
564
+ }
565
+
566
+ // If it's a branch, clean all children
567
+ const newChildren: SessionsTopology[] = []
568
+ for (const child of topology) {
569
+ const cleanedChild = cleanSessionsTopology(child, currentTime)
570
+ if (cleanedChild !== null) {
571
+ newChildren.push(cleanedChild)
572
+ }
573
+ }
574
+
575
+ // If no children remain, return null
576
+ if (newChildren.length === 0) {
577
+ return null
578
+ }
579
+
580
+ // If exactly one child remains, collapse upward:
581
+ if (newChildren.length === 1) {
582
+ return newChildren[0]!
583
+ }
584
+
585
+ // Otherwise, return a new branch with the cleaned children
586
+ return newChildren as SessionBranch
587
+ }
588
+
589
+ /**
590
+ * Minimise the topology by rolling unused signers into nodes.
591
+ * @param topology The topology to minimise
592
+ * @param signers The list of signers to consider
593
+ * @returns The minimised topology
594
+ */
595
+ export function minimiseSessionsTopology(
596
+ topology: SessionsTopology,
597
+ explicitSigners: Address.Address[] = [],
598
+ implicitSigners: Address.Address[] = [],
599
+ ): SessionsTopology {
600
+ if (isSessionsBranch(topology)) {
601
+ const branches = topology.map((b) => minimiseSessionsTopology(b, explicitSigners, implicitSigners))
602
+ // If all branches are nodes, the branch can be a node too
603
+ if (branches.every((b) => isSessionsNode(b))) {
604
+ return Hash.keccak256(Bytes.concat(...branches.map((b) => Hex.toBytes(b))), { as: 'Hex' })
605
+ }
606
+ return branches as SessionBranch
607
+ }
608
+ if (isSessionPermissions(topology)) {
609
+ if (explicitSigners.includes(topology.signer)) {
610
+ // Don't role it up as signer permissions must be visible
611
+ return topology
612
+ }
613
+ return GenericTree.hash(encodeLeafToGeneric(topology))
614
+ }
615
+ if (isImplicitBlacklist(topology)) {
616
+ if (implicitSigners.length === 0) {
617
+ // No implicit signers, so we can roll up the blacklist
618
+ return GenericTree.hash(encodeLeafToGeneric(topology))
619
+ }
620
+ // If there are implicit signers, we can't roll up the blacklist
621
+ return topology
622
+ }
623
+ if (isIdentitySignerLeaf(topology)) {
624
+ // Never roll up the identity signer
625
+ return topology
626
+ }
627
+ if (isSessionsNode(topology)) {
628
+ // Node is already encoded and hashed
629
+ return topology
630
+ }
631
+ // Unreachable
632
+ throw new Error('Invalid topology')
633
+ }
634
+
635
+ /**
636
+ * Adds an address to the implicit session's blacklist.
637
+ * If the address is not already in the blacklist, it is added and the list is sorted.
638
+ */
639
+ export function addToImplicitBlacklist(topology: SessionsTopology, address: Address.Address): SessionsTopology {
640
+ const blacklistNode = getImplicitBlacklistLeaf(topology)
641
+ if (!blacklistNode) {
642
+ throw new Error('No blacklist found')
643
+ }
644
+ const { blacklist } = blacklistNode
645
+ if (blacklist.some((addr) => Address.isEqual(addr, address))) {
646
+ return topology
647
+ }
648
+ blacklist.push(address)
649
+ blacklist.sort() // keep sorted so on-chain binary search works as expected
650
+ return topology
651
+ }
652
+
653
+ /**
654
+ * Removes an address from the implicit session's blacklist.
655
+ */
656
+ export function removeFromImplicitBlacklist(topology: SessionsTopology, address: Address.Address): SessionsTopology {
657
+ const blacklistNode = getImplicitBlacklistLeaf(topology)
658
+ if (!blacklistNode) {
659
+ throw new Error('No blacklist found')
660
+ }
661
+ const { blacklist } = blacklistNode
662
+ const newBlacklist = blacklist.filter((a) => a !== address)
663
+ blacklistNode.blacklist = newBlacklist
664
+ return topology
665
+ }
666
+
667
+ /**
668
+ * Generate an empty sessions topology with the given identity signer. No session permission and an empty blacklist
669
+ */
670
+ export function emptySessionsTopology(identitySigner: Address.Address): SessionsTopology {
671
+ return [
672
+ {
673
+ type: 'implicit-blacklist',
674
+ blacklist: [],
675
+ },
676
+ {
677
+ type: 'identity-signer',
678
+ identitySigner,
679
+ },
680
+ ]
681
+ }