Skip to content

Commit f1e3521

Browse files
committed
feat: add rollback system
1 parent 9b4fd44 commit f1e3521

File tree

1 file changed

+57
-24
lines changed

1 file changed

+57
-24
lines changed

packages/keyring-eth-simple/src/simple-keyring-v2.ts

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,61 @@ export class SimpleKeyringV2
118118
await this.inner.deserialize(privateKeys);
119119
}
120120

121+
/**
122+
* Executes a transactional update on the inner keyring state.
123+
* If the callback throws, the state is automatically rolled back.
124+
*
125+
* @param callback - A function that receives the current private keys and performs the update.
126+
* Should return the result on success, or throw to trigger rollback.
127+
* @returns The result of the callback.
128+
* @throws Error if the callback throws (after rollback).
129+
*/
130+
async #withRollback<Result>(
131+
callback: (currentPrivateKeys: string[]) => Promise<Result>,
132+
): Promise<Result> {
133+
const originalPrivateKeys = await this.#getPrivateKeys();
134+
135+
try {
136+
return await callback(originalPrivateKeys);
137+
} catch (error) {
138+
// Rollback on error
139+
await this.#setPrivateKeys(originalPrivateKeys);
140+
throw error;
141+
}
142+
}
143+
144+
/**
145+
* Import a private key and return the new address.
146+
* If the import fails (no new address added), rolls back to the original state.
147+
*
148+
* @param privateKey - The private key to import in hexadecimal format.
149+
* @returns The address of the newly imported account.
150+
* @throws Error if the import fails or no new address is added.
151+
*/
152+
async #importPrivateKeyOrRollback(privateKey: string): Promise<Hex> {
153+
return this.#withRollback(async (currentPrivateKeys) => {
154+
// Get current addresses before import
155+
const addressesBefore = new Set(await this.inner.getAccounts());
156+
157+
// Import the new private key
158+
await this.#setPrivateKeys([...currentPrivateKeys, privateKey]);
159+
160+
// Get addresses after import and find the newly added one
161+
const addressesAfter = await this.inner.getAccounts();
162+
163+
// Find the new address by diffing the two sets
164+
const newAddresses = addressesAfter.filter(
165+
(addr) => !addressesBefore.has(addr),
166+
);
167+
168+
if (newAddresses.length !== 1 || !newAddresses[0]) {
169+
throw new Error('Failed to import private key');
170+
}
171+
172+
return newAddresses[0];
173+
});
174+
}
175+
121176
async getAccounts(): Promise<KeyringAccount[]> {
122177
const addresses = await this.inner.getAccounts();
123178

@@ -175,30 +230,8 @@ export class SimpleKeyringV2
175230
);
176231
}
177232

178-
// Get current addresses before import
179-
const addressesBefore = new Set(await this.inner.getAccounts());
180-
181-
// Get current accounts to preserve them (also used for rollback)
182-
const currentAccounts = await this.#getPrivateKeys();
183-
184-
// Import the new private key by deserializing with all accounts
185-
await this.#setPrivateKeys([...currentAccounts, privateKey]);
186-
187-
// Get addresses after import and find the newly added one
188-
const addressesAfter = await this.inner.getAccounts();
189-
190-
// Find the new address by diffing the two sets
191-
const newAddresses = addressesAfter.filter(
192-
(addr) => !addressesBefore.has(addr),
193-
);
194-
195-
if (newAddresses.length !== 1 || !newAddresses[0]) {
196-
// Rollback the inner keyring state to prevent corruption
197-
await this.#setPrivateKeys(currentAccounts);
198-
throw new Error('Failed to import private key');
199-
}
200-
201-
const newAddress = newAddresses[0];
233+
// Import the private key (with automatic rollback on failure)
234+
const newAddress = await this.#importPrivateKeyOrRollback(privateKey);
202235

203236
// Create and return the new KeyringAccount
204237
const newAccount = this.#createKeyringAccount(newAddress);

0 commit comments

Comments
 (0)