📄 src/web3/vue/MutationBus.ts
D-OPEN SOVEREIGN
1import { reactive, readonly } from 'vue';
2import type { WalletType } from '../client/types';
3
4export type ChainMutation =
5 | { type: 'retire'; wallet: WalletType; chainId: number; account: string }
6 | { type: 'vote'; wallet: WalletType; chainId: number; account: string; projectId: number; lang: string; amount: number }
7 | { type: 'save'; wallet: WalletType; chainId: number; account: string; newIndex: number }
8 | { type: 'subscribe'; wallet: WalletType; chainId: number; account: string; subscriber: number; target: number }
9 | { type: 'unsubscribe'; wallet: WalletType; chainId: number; account: string; subscriber: number; target: number };
10
11export interface MutationHandler<T extends ChainMutation> {
12 applyOptimistic: (mutation: T) => any;
13 rollback: (snapshot: any) => void;
14}
15
16const handlers = new Map<string, MutationHandler<any>>();
17
18export function registerHandler<T extends ChainMutation>(type: T['type'], handler: MutationHandler<T>) {
19 handlers.set(type, handler);
20}
21
22export type PendingStatus = 'awaiting_signature' | 'broadcasting';
23export interface PendingMutation {
24 id: string;
25 label: string;
26 status: PendingStatus;
27}
28
29export interface MutationError {
30 id: string;
31 label: string;
32 reason: 'rejected' | 'failed';
33}
34
35const pendingMutations = reactive<PendingMutation[]>([]);
36const mutationErrors = reactive<MutationError[]>([]);
37
38let idCounter = 0;
39
40export interface MutationBusType {
41 pending: ReadonlyArray<PendingMutation>;
42 errors: ReadonlyArray<MutationError>;
43 clearError: (id: string) => void;
44 dispatch: <T extends ChainMutation>(
45 mutation: T,
46 commit: (onAccepted: () => void) => Promise<boolean | void>,
47 label: string
48 ) => Promise<boolean>;
49}
50
51export const mutationBus: MutationBusType = {
52 pending: readonly(pendingMutations) as unknown as ReadonlyArray<PendingMutation>,
53 errors: readonly(mutationErrors) as unknown as ReadonlyArray<MutationError>,
54
55 clearError(id: string) {
56 const idx = mutationErrors.findIndex((e) => e.id === id);
57 if (idx !== -1) mutationErrors.splice(idx, 1);
58 },
59
60 async dispatch<T extends ChainMutation>(
61 mutation: T,
62 commit: (onAccepted: () => void) => Promise<boolean | void>,
63 label: string
64 ): Promise<boolean> {
65 const id = `mut_${++idCounter}`;
66 const initialStatus = mutation.wallet === 'private_key' ? 'broadcasting' : 'awaiting_signature';
67
68 pendingMutations.push({ id, label, status: initialStatus });
69
70 const handler = handlers.get(mutation.type);
71 let snapshot: any = null;
72
73 try {
74 if (handler) {
75 snapshot = handler.applyOptimistic(mutation);
76 }
77
78 const onAccepted = () => {
79 const item = pendingMutations.find((p) => p.id === id);
80 if (item) item.status = 'broadcasting';
81 };
82
83 const result = await commit(onAccepted);
84
85 if (result === false) {
86 throw new Error('Transaction failed');
87 }
88
89 return true;
90 } catch (e: any) {
91 if (handler && snapshot) {
92 try {
93 handler.rollback(snapshot);
94 } catch (rollbackError) {
95 console.error('Rollback failed:', rollbackError);
96 }
97 }
98
99 const isRejected =
100 e.code === 4001 ||
101 e.code === 'ACTION_REJECTED' ||
102 (e.message && (e.message.toLowerCase().includes('user rejected') || e.message.toLowerCase().includes('cancelled')));
103
104 mutationErrors.push({
105 id,
106 label,
107 reason: isRejected ? 'rejected' : 'failed',
108 });
109
110 return false;
111 } finally {
112 const idx = pendingMutations.findIndex((p) => p.id === id);
113 if (idx !== -1) pendingMutations.splice(idx, 1);
114 }
115 },
116};
117