@@ -4,10 +4,12 @@ import {
44 matchPrefix ,
55 matchRegex ,
66 OrderedQmdbClient ,
7+ type OrderedOperation ,
78 type OrderedSubscribeProof ,
89 type VerifiedCurrentKeyLookupProof ,
910 type VerifiedCurrentKeyRangeProof ,
1011 type VerifiedCurrentKeyValueProof ,
12+ type VerifiedHistoricalMultiProof ,
1113} from '@qmdb-ts' ;
1214
1315export const QMDB_URL = import . meta. env . VITE_QMDB_URL as string | undefined ;
@@ -56,21 +58,23 @@ function formatProofSize(bytes: number): string {
5658 return `${ ( bytes / 1024 ) . toFixed ( 1 ) } KiB` ;
5759}
5860
59- function parseTip ( value : string ) : bigint {
61+ function parseNonNegativeBigInt ( value : string , label : string ) : bigint {
6062 const trimmed = value . trim ( ) ;
6163 if ( ! trimmed ) {
62- throw new Error ( 'Tip is required' ) ;
64+ throw new Error ( ` ${ label } is required` ) ;
6365 }
64- const tip = BigInt ( trimmed ) ;
65- if ( tip < 0n ) {
66- throw new Error ( 'Tip must be non-negative' ) ;
66+ const parsed = BigInt ( trimmed ) ;
67+ if ( parsed < 0n ) {
68+ throw new Error ( ` ${ label } must be non-negative` ) ;
6769 }
68- return tip ;
70+ return parsed ;
71+ }
72+
73+ function parseTip ( value : string ) : bigint {
74+ return parseNonNegativeBigInt ( value , 'Tip' ) ;
6975}
7076
71- function renderOperation (
72- proofOperation : OrderedSubscribeProof [ 'proof' ] [ 'operations' ] [ number ] [ 'operation' ] ,
73- ) {
77+ function renderOperation ( proofOperation : OrderedOperation ) {
7478 switch ( proofOperation . type ) {
7579 case 'update' :
7680 return (
@@ -132,6 +136,11 @@ export function QmdbPanel({
132136 const [ rangeProof , setRangeProof ] = useState < VerifiedCurrentKeyRangeProof | null > ( null ) ;
133137 const [ isGettingRange , setIsGettingRange ] = useState ( false ) ;
134138
139+ const [ historyStartLocation , setHistoryStartLocation ] = useState ( '0' ) ;
140+ const [ historyMaxLocations , setHistoryMaxLocations ] = useState ( '5' ) ;
141+ const [ historyProof , setHistoryProof ] = useState < VerifiedHistoricalMultiProof | null > ( null ) ;
142+ const [ isGettingHistory , setIsGettingHistory ] = useState ( false ) ;
143+
135144 const [ keyMatcherKind , setKeyMatcherKind ] = useState < 'exact' | 'prefix' | 'regex' | 'none' > (
136145 'none' ,
137146 ) ;
@@ -245,6 +254,35 @@ export function QmdbPanel({
245254 }
246255 } ;
247256
257+ const handleGetOperationRange = async ( ) => {
258+ setIsGettingHistory ( true ) ;
259+ setHistoryProof ( null ) ;
260+ try {
261+ const maxLocations = Number ( historyMaxLocations ) ;
262+ if ( ! Number . isInteger ( maxLocations ) || maxLocations <= 0 ) {
263+ throw new Error ( 'Max Locations must be a positive integer' ) ;
264+ }
265+ const proof = await client . getOperationRange (
266+ {
267+ tip : parseTip ( tip ) ,
268+ startLocation : parseNonNegativeBigInt ( historyStartLocation , 'Start Location' ) ,
269+ maxLocations,
270+ } ,
271+ parseHexRoot ( expectedCurrentRoot ) ,
272+ ) ;
273+ setHistoryProof ( proof ) ;
274+ showNotification (
275+ 'success' ,
276+ 'QMDB Historical Range' ,
277+ `Verified ${ proof . operations . length } historical operations against expected root (${ formatProofSize ( proof . proofSizeBytes ) } )` ,
278+ ) ;
279+ } catch ( error ) {
280+ showNotification ( 'error' , 'QMDB Historical Range Failed' , String ( error ) ) ;
281+ } finally {
282+ setIsGettingHistory ( false ) ;
283+ }
284+ } ;
285+
248286 function buildFilter (
249287 kind : 'exact' | 'prefix' | 'regex' | 'none' ,
250288 value : string ,
@@ -310,9 +348,9 @@ export function QmdbPanel({
310348 < p className = "section-note" >
311349 Proofs are anchored to roots the writer emits per batch. Run `qmdb run`
312350 locally and `qmdb seed` to stream fresh tips; each line prints
313- `tip=N root=0x..`. Get Proof, Get Many, and Get Range verify
314- against that current root. Subscribe streams each proof with its tip
315- and included operations.
351+ `tip=N root=0x..`. Get Proof, Get Many, Get Range, and historical
352+ operation ranges verify against that current root. Subscribe streams
353+ each proof with its tip and included operations.
316354 </ p >
317355 < p > < strong > Server:</ strong > { qmdbUrl } </ p >
318356 < p > < strong > Merkle Family:</ strong > MMB</ p >
@@ -645,6 +683,67 @@ export function QmdbPanel({
645683 ) }
646684 </ div >
647685 </ div >
686+
687+ < div className = "form-section" >
688+ < h3 > Get Historical Operation Range</ h3 >
689+ < p className = "section-note" >
690+ Fetches a contiguous historical operation proof for the operation log and verifies it
691+ against the expected root for the selected tip.
692+ </ p >
693+ < div className = "form-row" >
694+ < div className = "form-group" >
695+ < label htmlFor = "qmdb-history-start" > Start Location</ label >
696+ < input
697+ id = "qmdb-history-start"
698+ type = "number"
699+ min = "0"
700+ value = { historyStartLocation }
701+ onChange = { ( event ) => setHistoryStartLocation ( event . target . value ) }
702+ />
703+ </ div >
704+ < div className = "form-group" >
705+ < label htmlFor = "qmdb-history-max" > Max Locations</ label >
706+ < input
707+ id = "qmdb-history-max"
708+ type = "number"
709+ min = "1"
710+ value = { historyMaxLocations }
711+ onChange = { ( event ) => setHistoryMaxLocations ( event . target . value ) }
712+ />
713+ </ div >
714+ </ div >
715+ < button
716+ className = { `btn-primary ${ isGettingHistory ? 'loading' : '' } ` }
717+ onClick = { handleGetOperationRange }
718+ disabled = {
719+ isGettingHistory ||
720+ ! historyStartLocation . trim ( ) ||
721+ ! historyMaxLocations . trim ( ) ||
722+ ! tip . trim ( ) ||
723+ ! expectedCurrentRoot . trim ( )
724+ }
725+ >
726+ { isGettingHistory ? 'Verifying...' : 'Get Historical Range' }
727+ </ button >
728+ { historyProof && (
729+ < div className = "result fade-in" >
730+ < h4 > Verified Historical Operations</ h4 >
731+ < p > < strong > Proof Size:</ strong > { formatProofSize ( historyProof . proofSizeBytes ) } </ p >
732+ < p > < strong > Operations:</ strong > { historyProof . operations . length } </ p >
733+ < div className = "result-list" >
734+ { historyProof . operations . map ( ( op , index ) => (
735+ < div
736+ key = { `${ op . location . toString ( ) } -${ index } ` }
737+ className = "result-row-block"
738+ >
739+ < p > < strong > Location:</ strong > { op . location . toString ( ) } </ p >
740+ { renderOperation ( op . operation ) }
741+ </ div >
742+ ) ) }
743+ </ div >
744+ </ div >
745+ ) }
746+ </ div >
648747 </ div >
649748 ) ;
650749}
0 commit comments