1
1
import { Address , Bytes , Hash , Hex } from 'ox'
2
2
import * as Payload from '../payload'
3
3
import { getSignPayload } from 'ox/TypedData'
4
+ import * as GenericTree from '../generic-tree'
4
5
5
6
export const FLAG_RECOVERY_LEAF = 1
6
7
export const FLAG_NODE = 3
7
8
export const FLAG_BRANCH = 4
8
9
10
+ const RECOVERY_LEAF_PREFIX = Bytes . fromString ( 'Sequence recovery leaf:\n' )
11
+
9
12
/**
10
13
* A leaf in the Recovery tree, storing:
11
14
* - signer who can queue a payload
@@ -19,23 +22,18 @@ export type RecoveryLeaf = {
19
22
minTimestamp : bigint
20
23
}
21
24
22
- /**
23
- * A node is just a 32-byte hash
24
- */
25
- export type NodeLeaf = Hex . Hex
26
-
27
25
/**
28
26
* A branch is a list of subtrees (≥2 in length).
29
27
*/
30
- export type Node = [ Node , Node ]
28
+ export type Branch = [ Tree , Tree ]
31
29
32
30
/**
33
31
* The topology of a recovery tree can be either:
34
32
* - A node (pair of subtrees)
35
33
* - A node leaf (32-byte hash)
36
34
* - A recovery leaf (signer with timing constraints)
37
35
*/
38
- export type Topology = Node | NodeLeaf | RecoveryLeaf
36
+ export type Tree = Branch | GenericTree . Node | RecoveryLeaf
39
37
40
38
/**
41
39
* Type guard to check if a value is a RecoveryLeaf
@@ -44,25 +42,18 @@ export function isRecoveryLeaf(cand: any): cand is RecoveryLeaf {
44
42
return typeof cand === 'object' && cand !== null && cand . type === 'leaf'
45
43
}
46
44
47
- /**
48
- * Type guard to check if a value is a NodeLeaf (32-byte hash)
49
- */
50
- export function isNodeLeaf ( cand : any ) : cand is NodeLeaf {
51
- return typeof cand === 'string' && cand . length === 66 && cand . startsWith ( '0x' )
52
- }
53
-
54
45
/**
55
46
* Type guard to check if a value is a Node (pair of subtrees)
56
47
*/
57
- export function isNode ( cand : any ) : cand is Node {
58
- return Array . isArray ( cand ) && cand . length === 2 && isTopology ( cand [ 0 ] ) && isTopology ( cand [ 1 ] )
48
+ export function isBranch ( cand : any ) : cand is Branch {
49
+ return Array . isArray ( cand ) && cand . length === 2 && isTree ( cand [ 0 ] ) && isTree ( cand [ 1 ] )
59
50
}
60
51
61
52
/**
62
53
* Type guard to check if a value is a Topology
63
54
*/
64
- export function isTopology ( cand : any ) : cand is Topology {
65
- return isRecoveryLeaf ( cand ) || isNodeLeaf ( cand ) || isNode ( cand )
55
+ export function isTree ( cand : any ) : cand is Tree {
56
+ return isRecoveryLeaf ( cand ) || GenericTree . isNode ( cand ) || isBranch ( cand )
66
57
}
67
58
68
59
/**
@@ -79,24 +70,8 @@ export const DOMAIN_VERSION = '1'
79
70
* For node leaves, it returns the hash directly.
80
71
* For nodes, it hashes the concatenation of the hashes of both subtrees.
81
72
*/
82
- export function hashConfiguration ( topology : Topology ) : Hex . Hex {
83
- if ( isRecoveryLeaf ( topology ) ) {
84
- return Hash . keccak256 (
85
- Bytes . concat (
86
- Bytes . fromString ( 'Sequence recovery leaf:\n' ) ,
87
- Bytes . fromHex ( topology . signer , { size : 20 } ) ,
88
- Bytes . padLeft ( Bytes . fromNumber ( topology . requiredDeltaTime ) , 32 ) ,
89
- Bytes . padLeft ( Bytes . fromNumber ( topology . minTimestamp ) , 32 ) ,
90
- ) ,
91
- { as : 'Hex' } ,
92
- )
93
- } else if ( isNodeLeaf ( topology ) ) {
94
- return topology
95
- } else if ( isNode ( topology ) ) {
96
- return Hash . keccak256 ( Hex . concat ( hashConfiguration ( topology [ 0 ] ) , hashConfiguration ( topology [ 1 ] ) ) , { as : 'Hex' } )
97
- } else {
98
- throw new Error ( 'Invalid topology' )
99
- }
73
+ export function hashConfiguration ( topology : Tree ) : Hex . Hex {
74
+ return GenericTree . hash ( toGenericTree ( topology ) )
100
75
}
101
76
102
77
/**
@@ -107,13 +82,13 @@ export function hashConfiguration(topology: Topology): Hex.Hex {
107
82
* - leaves: Array of RecoveryLeaf nodes
108
83
* - isComplete: boolean indicating if all leaves are present (no node references)
109
84
*/
110
- export function getRecoveryLeaves ( topology : Topology ) : { leaves : RecoveryLeaf [ ] ; isComplete : boolean } {
85
+ export function getRecoveryLeaves ( topology : Tree ) : { leaves : RecoveryLeaf [ ] ; isComplete : boolean } {
111
86
const isComplete = true
112
87
if ( isRecoveryLeaf ( topology ) ) {
113
88
return { leaves : [ topology ] , isComplete }
114
- } else if ( isNodeLeaf ( topology ) ) {
89
+ } else if ( GenericTree . isNode ( topology ) ) {
115
90
return { leaves : [ ] , isComplete : false }
116
- } else if ( isNode ( topology ) ) {
91
+ } else if ( isBranch ( topology ) ) {
117
92
const left = getRecoveryLeaves ( topology [ 0 ] )
118
93
const right = getRecoveryLeaves ( topology [ 1 ] )
119
94
return { leaves : [ ...left . leaves , ...right . leaves ] , isComplete : left . isComplete && right . isComplete }
@@ -129,7 +104,7 @@ export function getRecoveryLeaves(topology: Topology): { leaves: RecoveryLeaf[];
129
104
* @returns The decoded Topology object
130
105
* @throws Error if the encoding is invalid
131
106
*/
132
- export function decodeTopology ( encoded : Bytes . Bytes ) : Topology {
107
+ export function decodeTopology ( encoded : Bytes . Bytes ) : Tree {
133
108
const { nodes, leftover } = parseBranch ( encoded )
134
109
if ( leftover . length > 0 ) {
135
110
throw new Error ( 'Leftover bytes in branch' )
@@ -146,12 +121,12 @@ export function decodeTopology(encoded: Bytes.Bytes): Topology {
146
121
* - leftover: Any remaining unparsed bytes
147
122
* @throws Error if the encoding is invalid
148
123
*/
149
- export function parseBranch ( encoded : Bytes . Bytes ) : { nodes : Topology [ ] ; leftover : Bytes . Bytes } {
124
+ export function parseBranch ( encoded : Bytes . Bytes ) : { nodes : Tree [ ] ; leftover : Bytes . Bytes } {
150
125
if ( encoded . length === 0 ) {
151
126
throw new Error ( 'Empty branch' )
152
127
}
153
128
154
- const nodes : Topology [ ] = [ ]
129
+ const nodes : Tree [ ] = [ ]
155
130
let index = 0
156
131
157
132
while ( index < encoded . length ) {
@@ -208,7 +183,7 @@ export function parseBranch(encoded: Bytes.Bytes): { nodes: Topology[]; leftover
208
183
* @param signer - The signer address to keep
209
184
* @returns The trimmed topology
210
185
*/
211
- export function trimTopology ( topology : Topology , signer : Address . Address ) : Topology {
186
+ export function trimTopology ( topology : Tree , signer : Address . Address ) : Tree {
212
187
if ( isRecoveryLeaf ( topology ) ) {
213
188
if ( topology . signer === signer ) {
214
189
return topology
@@ -217,20 +192,20 @@ export function trimTopology(topology: Topology, signer: Address.Address): Topol
217
192
}
218
193
}
219
194
220
- if ( isNodeLeaf ( topology ) ) {
195
+ if ( GenericTree . isNode ( topology ) ) {
221
196
return topology
222
197
}
223
198
224
- if ( isNode ( topology ) ) {
199
+ if ( isBranch ( topology ) ) {
225
200
const left = trimTopology ( topology [ 0 ] , signer )
226
201
const right = trimTopology ( topology [ 1 ] , signer )
227
202
228
203
// If both are hashes, we can just return the hash of the node
229
- if ( isNodeLeaf ( left ) && isNodeLeaf ( right ) ) {
204
+ if ( GenericTree . isNode ( left ) && GenericTree . isNode ( right ) ) {
230
205
return hashConfiguration ( topology )
231
206
}
232
207
233
- return [ left , right ] as Node
208
+ return [ left , right ] as Branch
234
209
}
235
210
236
211
throw new Error ( 'Invalid topology' )
@@ -243,11 +218,11 @@ export function trimTopology(topology: Topology, signer: Address.Address): Topol
243
218
* @returns The binary encoded topology
244
219
* @throws Error if the topology is invalid
245
220
*/
246
- export function encodeTopology ( topology : Topology ) : Bytes . Bytes {
247
- if ( isNode ( topology ) ) {
221
+ export function encodeTopology ( topology : Tree ) : Bytes . Bytes {
222
+ if ( isBranch ( topology ) ) {
248
223
const encoded0 = encodeTopology ( topology [ 0 ] ! )
249
224
const encoded1 = encodeTopology ( topology [ 1 ] ! )
250
- const isBranching = isNode ( topology [ 1 ] ! )
225
+ const isBranching = isBranch ( topology [ 1 ] ! )
251
226
252
227
if ( isBranching ) {
253
228
// max 3 bytes for the size
@@ -263,7 +238,7 @@ export function encodeTopology(topology: Topology): Bytes.Bytes {
263
238
}
264
239
}
265
240
266
- if ( isNodeLeaf ( topology ) ) {
241
+ if ( GenericTree . isNode ( topology ) ) {
267
242
const flag = Bytes . fromNumber ( FLAG_NODE )
268
243
const nodeHash = Bytes . fromHex ( topology , { size : 32 } )
269
244
return Bytes . concat ( flag , nodeHash )
@@ -296,7 +271,7 @@ export function encodeTopology(topology: Topology): Bytes.Bytes {
296
271
* @returns A binary tree structure
297
272
* @throws Error if the nodes array is empty
298
273
*/
299
- function foldNodes ( nodes : Topology [ ] ) : Topology {
274
+ function foldNodes ( nodes : Tree [ ] ) : Tree {
300
275
if ( nodes . length === 0 ) {
301
276
throw new Error ( 'Empty signature tree' )
302
277
}
@@ -305,9 +280,9 @@ function foldNodes(nodes: Topology[]): Topology {
305
280
return nodes [ 0 ] !
306
281
}
307
282
308
- let tree : Topology = nodes [ 0 ] !
283
+ let tree : Tree = nodes [ 0 ] !
309
284
for ( let i = 1 ; i < nodes . length ; i ++ ) {
310
- tree = [ tree , nodes [ i ] ! ] as Topology
285
+ tree = [ tree , nodes [ i ] ! ] as Tree
311
286
}
312
287
return tree
313
288
}
@@ -321,7 +296,7 @@ function foldNodes(nodes: Topology[]): Topology {
321
296
* @returns A topology tree structure
322
297
* @throws Error if the leaves array is empty
323
298
*/
324
- export function fromRecoveryLeaves ( leaves : RecoveryLeaf [ ] ) : Topology {
299
+ export function fromRecoveryLeaves ( leaves : RecoveryLeaf [ ] ) : Tree {
325
300
if ( leaves . length === 0 ) {
326
301
throw new Error ( 'Cannot build a tree with zero leaves' )
327
302
}
@@ -333,7 +308,7 @@ export function fromRecoveryLeaves(leaves: RecoveryLeaf[]): Topology {
333
308
const mid = Math . floor ( leaves . length / 2 )
334
309
const left = fromRecoveryLeaves ( leaves . slice ( 0 , mid ) )
335
310
const right = fromRecoveryLeaves ( leaves . slice ( mid ) )
336
- return [ left , right ] as Node
311
+ return [ left , right ] as Branch
337
312
}
338
313
339
314
/**
@@ -389,3 +364,74 @@ export function hashRecoveryPayload(
389
364
const structHash = Bytes . fromHex ( getSignPayload ( Payload . toTyped ( wallet , noChainId ? 0n : chainId , payload ) ) )
390
365
return Hash . keccak256 ( Bytes . concat ( Bytes . fromString ( '\x19\x01' ) , Hex . toBytes ( ds ) , structHash ) , { as : 'Hex' } )
391
366
}
367
+
368
+ /**
369
+ * Convert a RecoveryTree topology to a generic tree format
370
+ *
371
+ * @param topology - The recovery tree topology to convert
372
+ * @returns A generic tree that produces the same root hash
373
+ */
374
+ export function toGenericTree ( topology : Tree ) : GenericTree . Tree {
375
+ if ( isRecoveryLeaf ( topology ) ) {
376
+ // Convert recovery leaf to generic leaf
377
+ return {
378
+ type : 'leaf' ,
379
+ value : Bytes . concat (
380
+ RECOVERY_LEAF_PREFIX ,
381
+ Bytes . fromHex ( topology . signer , { size : 20 } ) ,
382
+ Bytes . padLeft ( Bytes . fromNumber ( topology . requiredDeltaTime ) , 32 ) ,
383
+ Bytes . padLeft ( Bytes . fromNumber ( topology . minTimestamp ) , 32 ) ,
384
+ ) ,
385
+ }
386
+ } else if ( GenericTree . isNode ( topology ) ) {
387
+ // Node leaves are already in the correct format
388
+ return topology
389
+ } else if ( isBranch ( topology ) ) {
390
+ // Convert node to branch
391
+ return [ toGenericTree ( topology [ 0 ] ) , toGenericTree ( topology [ 1 ] ) ]
392
+ } else {
393
+ throw new Error ( 'Invalid topology' )
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Convert a generic tree back to a RecoveryTree topology
399
+ *
400
+ * @param tree - The generic tree to convert
401
+ * @returns A recovery tree topology that produces the same root hash
402
+ */
403
+ export function fromGenericTree ( tree : GenericTree . Tree ) : Tree {
404
+ if ( GenericTree . isLeaf ( tree ) ) {
405
+ // Convert generic leaf back to recovery leaf
406
+ const bytes = tree . value
407
+ if (
408
+ bytes . length !== RECOVERY_LEAF_PREFIX . length + 84 ||
409
+ ! Bytes . isEqual ( bytes . slice ( 0 , RECOVERY_LEAF_PREFIX . length ) , RECOVERY_LEAF_PREFIX )
410
+ ) {
411
+ throw new Error ( 'Invalid recovery leaf format' )
412
+ }
413
+
414
+ const offset = RECOVERY_LEAF_PREFIX . length
415
+ const signer = Address . from ( Hex . fromBytes ( bytes . slice ( offset , offset + 20 ) ) )
416
+ const requiredDeltaTime = Bytes . toBigInt ( bytes . slice ( offset + 20 , offset + 52 ) )
417
+ const minTimestamp = Bytes . toBigInt ( bytes . slice ( offset + 52 , offset + 84 ) )
418
+
419
+ return {
420
+ type : 'leaf' ,
421
+ signer,
422
+ requiredDeltaTime,
423
+ minTimestamp,
424
+ }
425
+ } else if ( GenericTree . isNode ( tree ) ) {
426
+ // Nodes are already in the correct format
427
+ return tree
428
+ } else if ( GenericTree . isBranch ( tree ) ) {
429
+ // Convert branch back to node
430
+ if ( tree . length !== 2 ) {
431
+ throw new Error ( 'Recovery tree only supports binary branches' )
432
+ }
433
+ return [ fromGenericTree ( tree [ 0 ] ) , fromGenericTree ( tree [ 1 ] ) ] as Branch
434
+ } else {
435
+ throw new Error ( 'Invalid tree format' )
436
+ }
437
+ }
0 commit comments