Skip to content

Commit fa507bb

Browse files
committed
rewrite manager
1 parent 3dd39d2 commit fa507bb

10 files changed

Lines changed: 287 additions & 362 deletions

File tree

packages/wallet-sdk/src/auto-approvals/index.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,5 @@ export {
1111
// Auto-approval system exports
1212
export { AutoApprovalManager } from './manager.js';
1313
export type { AutoApprovalAnalysis } from './manager.js';
14-
export {
15-
type AutoApprovalPolicy,
16-
type AutoApprovalRuleSet,
17-
type AutoApprovalObjectTypeRule,
18-
type CoinBalanceRule,
19-
type AllBalancesRule,
20-
type AutoApprovalPolicySettings,
21-
type ObjectPermissions,
22-
} from './types/index.js';
14+
15+
export * from './schemas/index.js';
Lines changed: 86 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
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';
94
import type { Experimental_SuiClientTypes } from '@mysten/sui/experimental';
105
import { parse, safeParse } from 'valibot';
11-
import {
12-
AutoApprovalStateSchema,
13-
AutoApprovalPolicySchema,
14-
AutoApprovalPolicySettingsSchema,
15-
} from './schemas.js';
166
import type { BaseAnalysis } from '../transaction-analyzer/base.js';
177
import 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

1913
export interface AutoApprovalManagerOptions {
14+
policy: string;
2015
state: string | null;
2116
network: string;
2217
origin: string;
2318
}
2419

2520
export 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

Comments
 (0)