11// Copyright (c) Mysten Labs, Inc.
22// SPDX-License-Identifier: Apache-2.0
33
4- import type {
5- AutoApprovalPolicy ,
6- AutoApprovalPolicySettings ,
7- AutoApprovalState ,
8- } from './types/index.js' ;
94import type { Experimental_SuiClientTypes } from '@mysten/sui/experimental' ;
105import { parse , safeParse } from 'valibot' ;
11- import {
12- AutoApprovalStateSchema ,
13- AutoApprovalPolicySchema ,
14- AutoApprovalPolicySettingsSchema ,
15- } from './schemas.js' ;
166import type { BaseAnalysis } from '../transaction-analyzer/base.js' ;
177import type { CoinValueAnalysis , TransactionAnalysisIssue } from '../transaction-analyzer/index.js' ;
8+ import type { AutoApprovalState } from './schemas/state.js' ;
9+ import { AutoApprovalStateSchema } from './schemas/state.js' ;
10+ import type { AutoApprovalSettings } from './schemas/policy.js' ;
11+ import { AutoApprovalPolicySchema , AutoApprovalSettingsSchema } from './schemas/policy.js' ;
1812
1913export interface AutoApprovalManagerOptions {
14+ policy : string ;
2015 state : string | null ;
2116 network : string ;
2217 origin : string ;
2318}
2419
2520export interface AutoApprovalAnalysis {
2621 results : BaseAnalysis & {
27- coinValue : CoinValueAnalysis ;
22+ usdValue : CoinValueAnalysis ;
2823 operationType : string | null ;
2924 } ;
3025 issues : TransactionAnalysisIssue [ ] ;
@@ -34,29 +29,31 @@ export class AutoApprovalManager {
3429 #state: AutoApprovalState ;
3530
3631 constructor ( options : AutoApprovalManagerOptions ) {
37- // Initialize or parse existing state with validation
32+ let state : AutoApprovalState | null = null ;
33+
3834 if ( options . state ) {
3935 const parseResult = safeParse ( AutoApprovalStateSchema , JSON . parse ( options . state ) ) ;
40- if ( ! parseResult . success ) {
41- throw new Error (
42- `Invalid state: ${ parseResult . issues . map ( ( i : any ) => i . message ) . join ( ', ' ) } ` ,
43- ) ;
36+ if ( parseResult . success ) {
37+ const providedPolicy = parse ( AutoApprovalPolicySchema , JSON . parse ( options . policy ) ) ;
38+ const currentPolicy = parseResult . output . policy ;
39+
40+ if ( JSON . stringify ( currentPolicy ) === JSON . stringify ( providedPolicy ) ) {
41+ state = parseResult . output ;
42+ }
4443 }
45- this . #state = parseResult . output ;
46- } else {
47- this . #state = parse ( AutoApprovalStateSchema , {
48- version : '1.0.0' ,
44+ }
45+
46+ this . #state =
47+ state ??
48+ parse ( AutoApprovalStateSchema , {
49+ schemaVersion : '1.0.0' ,
4950 origin : options . origin ,
5051 network : options . network ,
51- approvedAt : null ,
52- policy : null ,
52+ policy : parse ( AutoApprovalPolicySchema , JSON . parse ( options . policy ) ) ,
5353 settings : null ,
54- balanceChanges : { } ,
5554 createdObjects : { } ,
56- approvedDigests : [ ] ,
5755 pendingDigests : [ ] ,
58- } ) ;
59- }
56+ } satisfies AutoApprovalState ) ;
6057
6158 if ( this . #state. network !== options . network ) {
6259 throw new Error ( `Network mismatch: expected ${ options . network } , got ${ this . #state. network } ` ) ;
@@ -67,31 +64,19 @@ export class AutoApprovalManager {
6764 }
6865 }
6966
70- /**
71- * Check if a transaction analysis can be auto-approved based on policy settings
72- * This is a fast synchronous operation
73- *
74- * @param analysis - The transaction analysis result
75- * @param policy - Policy to check against (defaults to current state)
76- * @param settings - Settings to check against (defaults to current state)
77- * @param ruleSetId - Specific ruleset to check (extracted from analysis if not provided)
78- */
7967 canAutoApprove ( analysis : AutoApprovalAnalysis ) : boolean {
8068 if ( ! this . #state. policy || ! this . #state. settings ) {
8169 return false ;
8270 }
8371
84- // Must have policy enabled (approved)
85- if ( ! this . #state. approvedAt && ! this . #state. settings ) {
72+ if ( analysis . issues . length > 0 ) {
8673 return false ;
8774 }
8875
89- // Must not be expired
9076 if ( new Date ( ) > new Date ( this . #state. settings . expiration ) ) {
9177 return false ;
9278 }
9379
94- // Must have remaining transactions
9580 if (
9681 this . #state. settings . remainingTransactions !== null &&
9782 this . #state. settings . remainingTransactions <= 0
@@ -101,7 +86,7 @@ export class AutoApprovalManager {
10186
10287 if (
10388 ! analysis . results . operationType ||
104- ! this . #state. settings . approvedRuleSets . includes ( analysis . results . operationType )
89+ ! this . #state. settings . approvedOperations . includes ( analysis . results . operationType )
10590 ) {
10691 return false ;
10792 }
@@ -110,147 +95,111 @@ export class AutoApprovalManager {
11095 return false ;
11196 }
11297
113- // All checks passed
98+ // TODO: analyze balances and budgets
99+
114100 return true ;
115101 }
116102
117- commitTransaction ( analysis : BaseAnalysis ) : void {
118- // Update remaining transactions
103+ commitTransaction ( analysis : AutoApprovalAnalysis ) : void {
119104 if ( this . #state. settings ?. remainingTransactions !== null && this . #state. settings ) {
120105 this . #state. settings . remainingTransactions = Math . max (
121106 0 ,
122107 this . #state. settings . remainingTransactions - 1 ,
123108 ) ;
124109 }
125110
126- // Track balance changes
127- for ( const outflow of analysis . coinFlows ) {
128- const existing = this . #state. balanceChanges [ outflow . coinType ] ;
129- const currentBalance = existing ? BigInt ( existing . balanceChange ) : 0n ;
130- const newBalance = currentBalance - outflow . amount ;
131-
132- this . #state. balanceChanges [ outflow . coinType ] = {
133- balanceChange : newBalance . toString ( ) ,
134- lastUpdated : new Date ( ) . toISOString ( ) ,
135- } ;
111+ for ( const outflow of analysis . results . coinFlows ) {
112+ const currentBudget = BigInt ( this . #state. settings ?. coinBudgets [ outflow . coinType ] ?? '0' ) ;
113+ const newBalance = currentBudget - outflow . amount ;
114+
115+ if ( this . #state. settings ) {
116+ this . #state. settings . coinBudgets [ outflow . coinType ] = newBalance . toString ( ) ;
117+ }
136118 }
137119
138- // Add to pending digests
139- if ( analysis . digest ) {
140- this . #state. pendingDigests . push ( analysis . digest ) ;
120+ // TODO: track USD budget
121+
122+ this . #state. pendingDigests . push ( analysis . results . digest ) ;
123+ }
124+
125+ revertTransaction ( analysis : AutoApprovalAnalysis ) : void {
126+ this . #removePendingDigest( analysis . results . digest ) ;
127+
128+ if ( this . #state. settings ?. remainingTransactions !== null && this . #state. settings ) {
129+ this . #state. settings . remainingTransactions += 1 ;
141130 }
131+
132+ this . #revertCoinFlows( analysis . results ) ;
142133 }
143134
144- applyTransactionEffects ( effects : Experimental_SuiClientTypes . TransactionEffects ) : void {
145- const digest = effects . transactionDigest ;
135+ #revertCoinFlows( analysis : BaseAnalysis ) : void {
136+ for ( const outflow of analysis . coinFlows ) {
137+ const currentBudget = BigInt ( this . #state. settings ?. coinBudgets [ outflow . coinType ] ?? '0' ) ;
138+ const newBalance = currentBudget + outflow . amount ;
146139
147- // Move from pending to approved
140+ if ( this . #state. settings ) {
141+ this . #state. settings . coinBudgets [ outflow . coinType ] = newBalance . toString ( ) ;
142+ }
143+ }
144+
145+ // TODO: revert USD budget
146+ }
147+
148+ #removePendingDigest( digest : string ) : void {
148149 const pendingIndex = this . #state. pendingDigests . indexOf ( digest ) ;
149150 if ( pendingIndex >= 0 ) {
150151 this . #state. pendingDigests . splice ( pendingIndex , 1 ) ;
151- this . #state. approvedDigests . push ( digest ) ;
152+ } else {
153+ throw new Error ( `Transaction with digest ${ digest } not found in pending digests` ) ;
152154 }
155+ }
153156
154- // Track created objects from changed objects
155- for ( const changed of effects . changedObjects ) {
157+ applyTransactionEffects (
158+ analysis : AutoApprovalAnalysis ,
159+ result : Experimental_SuiClientTypes . TransactionResponse ,
160+ ) : void {
161+ this . #removePendingDigest( result . digest ) ;
162+
163+ for ( const changed of result . effects . changedObjects ) {
156164 if ( changed . idOperation === 'Created' && changed . outputState === 'ObjectWrite' ) {
157165 this . #state. createdObjects [ changed . id ] = {
158166 objectId : changed . id ,
159167 version : changed . outputVersion ! ,
160168 digest : changed . outputDigest ! ,
161- objectType : 'unknown' , // Would need object type lookup
169+ objectType : analysis . results . objectsById . get ( changed . id ) ?. type || 'unknown' ,
162170 } ;
163171 }
164172 }
165- }
166173
167- detectChange ( newPolicy : AutoApprovalPolicy ) : boolean {
168- if ( ! this . #state. policy ) {
169- return true ;
170- }
171-
172- // Normalize both policies through validation to ensure consistent comparison
173- try {
174- const normalizedCurrent = parse ( AutoApprovalPolicySchema , this . #state. policy ) ;
175- const normalizedNew = parse ( AutoApprovalPolicySchema , newPolicy ) ;
176-
177- const currentHash = JSON . stringify ( normalizedCurrent ) ;
178- const newHash = JSON . stringify ( normalizedNew ) ;
179- return currentHash !== newHash ;
180- } catch ( error ) {
181- // If validation fails, assume change is needed
182- return true ;
183- }
184- }
185-
186- update ( policy : AutoApprovalPolicy , settings : AutoApprovalPolicySettings ) {
187- // Validate policy and settings
188- const validatedPolicy = parse ( AutoApprovalPolicySchema , policy ) ;
189- const validatedSettings = parse ( AutoApprovalPolicySettingsSchema , settings ) ;
190-
191- this . reset ( ) ;
192- this . #state. policy = validatedPolicy ;
193- this . #state. settings = validatedSettings ;
194- }
195-
196- approve ( ) {
197- if ( ! this . #state. policy ) {
198- throw new Error ( 'No policy to approve' ) ;
174+ // Revert coin flows and use real balance changes instead
175+ this . #revertCoinFlows( analysis . results ) ;
176+ for ( const change of result . balanceChanges ) {
177+ const currentBudget = BigInt ( this . #state. settings ?. coinBudgets [ change . coinType ] ?? '0' ) ;
178+ const newBalance = currentBudget + BigInt ( change . amount ) ;
179+ if ( this . #state. settings ) {
180+ this . #state. settings . coinBudgets [ change . coinType ] = newBalance . toString ( ) ;
181+ }
199182 }
200183
201- if ( ! this . #state. settings ) {
202- throw new Error ( 'Policy settings have not been set' ) ;
203- }
204- const timestamp = new Date ( ) . toISOString ( ) ;
205- this . #state. approvedAt = timestamp ;
184+ // TODO: track USD budget
206185 }
207186
208187 reset ( ) {
209- this . #state. approvedAt = null ;
210- this . #state. balanceChanges = { } ;
188+ this . #state. settings = null ;
211189 this . #state. createdObjects = { } ;
212- this . #state. approvedDigests = [ ] ;
213190 this . #state. pendingDigests = [ ] ;
214191 }
215192
216- applySettings ( settings : AutoApprovalPolicySettings ) {
217- const validatedSettings = parse ( AutoApprovalPolicySettingsSchema , settings ) ;
218- this . #state. settings = validatedSettings ;
219- }
220-
221193 export ( ) : string {
222- // Validate state before exporting
223- const validatedState = parse ( AutoApprovalStateSchema , this . #state) ;
224- return JSON . stringify ( validatedState ) ;
225- }
226-
227- serialize ( ) : string {
228- return this . export ( ) ;
229- }
230-
231- hasPolicy ( ) : boolean {
232- return this . #state. policy !== null ;
233- }
234-
235- isPolicyEnabled ( ) : boolean {
236- return this . #state. approvedAt !== null ;
194+ return JSON . stringify ( parse ( AutoApprovalStateSchema , this . #state) ) ;
237195 }
238196
239- getCurrentSettings ( ) : AutoApprovalPolicySettings | null {
197+ getSettings ( ) : AutoApprovalSettings | null {
240198 return this . #state. settings ;
241199 }
242200
243- createPolicy ( policy : AutoApprovalPolicy , settings : AutoApprovalPolicySettings ) : void {
244- this . update ( policy , settings ) ;
245- }
246-
247- updateSettings ( settings : AutoApprovalPolicySettings ) : void {
248- this . applySettings ( settings ) ;
249- }
250-
251- clearPolicy ( ) : void {
252- this . #state. policy = null ;
253- this . #state. settings = null ;
254- this . reset ( ) ;
201+ updateSettings ( settings : AutoApprovalSettings ) : void {
202+ const validatedSettings = parse ( AutoApprovalSettingsSchema , settings ) ;
203+ this . #state. settings = validatedSettings ;
255204 }
256205}
0 commit comments