Skip to content

Commit 9763243

Browse files
committed
feat: export wallet
1 parent 2def429 commit 9763243

File tree

2 files changed

+44
-33
lines changed

2 files changed

+44
-33
lines changed

account-kit/rn-signer/src/client.ts

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import "react-native-get-random-values";
33
import "./utils/buffer-polyfill";
44
import "./utils/mmkv-localstorage-polyfill";
5-
6-
/* eslint-disable import/extensions */
75
import { type ConnectionConfig } from "@aa-sdk/core";
86
import {
97
createPasskey,
@@ -291,7 +289,7 @@ export class RNSignerClient extends BaseSignerClient<ExportWalletParams> {
291289

292290
/**
293291
* Exports the wallet's private key for the authenticated user.
294-
*
292+
*
295293
* This uses the local storage approach recommended by Turnkey for mobile contexts.
296294
* A P256 key pair is generated locally, the public key is used to encrypt the export,
297295
* and the private key is used to decrypt the bundle locally.
@@ -308,28 +306,30 @@ export class RNSignerClient extends BaseSignerClient<ExportWalletParams> {
308306

309307
/**
310308
* Exports the wallet and returns the decrypted private key or seed phrase.
311-
*
309+
*
312310
* @param {ExportWalletParams} params Export parameters
313311
* @returns {Promise<ExportWalletResult>} The decrypted export data
314312
* @throws {Error} If the user is not authenticated or export fails
315313
*/
316-
async exportWalletWithResult(params?: ExportWalletParams): Promise<ExportWalletResult> {
314+
async exportWalletWithResult(params?: ExportWalletParams): Promise<any> {
317315
if (!this.user) {
318316
throw new Error("User must be authenticated to export wallet");
319317
}
320318

321319
const exportAs = params?.exportAs || "PRIVATE_KEY";
322-
320+
323321
// Step 1: Generate a P256 key pair for encryption
324322
const embeddedKey = generateP256KeyPair();
325-
323+
326324
// Step 2: Save the private key in secure storage
327325
const keyId = `export_key_${Date.now()}`;
328326
this.storage.set(keyId, embeddedKey.privateKey);
329-
327+
328+
console.log("exportWalletWithResult");
329+
330330
try {
331331
let exportBundle: string;
332-
332+
333333
if (exportAs === "PRIVATE_KEY") {
334334
// Step 3a: Export as private key
335335
const { activity } = await this.turnkeyClient.exportWalletAccount({
@@ -347,18 +347,18 @@ export class RNSignerClient extends BaseSignerClient<ExportWalletParams> {
347347
this.user.orgId,
348348
"exportWalletAccountResult",
349349
);
350-
350+
351351
if (!result.exportBundle) {
352352
throw new Error("Failed to export wallet: no export bundle returned");
353353
}
354-
354+
355355
exportBundle = result.exportBundle;
356356
} else {
357357
// Step 3b: Export as seed phrase (need to find the wallet first)
358358
const { wallets } = await this.turnkeyClient.getWallets({
359359
organizationId: this.user.orgId,
360360
});
361-
361+
362362
const walletAccounts = await Promise.all(
363363
wallets.map(({ walletId }) =>
364364
this.turnkeyClient.getWalletAccounts({
@@ -367,7 +367,7 @@ export class RNSignerClient extends BaseSignerClient<ExportWalletParams> {
367367
}),
368368
),
369369
).then((x) => x.flatMap((x) => x.accounts));
370-
370+
371371
const walletAccount = walletAccounts.find(
372372
(x) => x.address === this.user!.address,
373373
);
@@ -391,43 +391,54 @@ export class RNSignerClient extends BaseSignerClient<ExportWalletParams> {
391391
this.user.orgId,
392392
"exportWalletResult",
393393
);
394-
394+
395395
if (!result.exportBundle) {
396396
throw new Error("Failed to export wallet: no export bundle returned");
397397
}
398-
398+
399399
exportBundle = result.exportBundle;
400400
}
401401

402-
// Step 4: Decrypt the export bundle using HPKE
403-
// The export bundle is hex-encoded, so we need to convert it to bytes
404-
const bundleBytes = Buffer.from(exportBundle, 'hex');
405-
const encappedKeyBuf = bundleBytes.slice(0, 65);
406-
const ciphertextBuf = bundleBytes.slice(65);
407-
402+
// Step 4: Parse the export bundle and decrypt using HPKE
403+
// The export bundle is a JSON string containing version, data, etc.
404+
const bundleJson = JSON.parse(exportBundle);
405+
406+
// The data field contains another JSON string that's hex-encoded
407+
const innerDataHex = bundleJson.data;
408+
const innerDataJson = JSON.parse(
409+
Buffer.from(innerDataHex, "hex").toString(),
410+
);
411+
412+
// Extract the encapped public key and ciphertext from the inner data
413+
const encappedPublicKeyHex = innerDataJson.encappedPublic;
414+
const ciphertextHex = innerDataJson.ciphertext;
415+
416+
const encappedKeyBuf = Buffer.from(encappedPublicKeyHex, "hex");
417+
const ciphertextBuf = Buffer.from(ciphertextHex, "hex");
418+
419+
// Decrypt the data using HPKE
408420
const decryptedData = hpkeDecrypt({
409-
encappedKeyBuf,
410-
ciphertextBuf,
421+
ciphertextBuf: ciphertextBuf,
422+
encappedKeyBuf: encappedKeyBuf,
411423
receiverPriv: embeddedKey.privateKey,
412424
});
413425

414-
// Step 5: Parse the decrypted data
415-
const exportData = JSON.parse(new TextDecoder().decode(decryptedData));
416-
417-
// Return the result based on export type
426+
// Step 5: Process the decrypted data based on export type
418427
const result: ExportWalletResult = {
419428
address: this.user.address,
420429
exportAs,
421430
};
422-
431+
423432
if (exportAs === "PRIVATE_KEY") {
424-
result.privateKey = exportData.privateKey;
433+
// For private key, the decrypted data is the raw private key bytes
434+
// Convert to hex string with 0x prefix
435+
result.privateKey = "0x" + Buffer.from(decryptedData).toString("hex");
425436
} else {
426-
result.seedPhrase = exportData.mnemonic;
437+
// For seed phrase, the decrypted data is the mnemonic string
438+
result.seedPhrase = new TextDecoder().decode(decryptedData);
427439
}
428-
440+
429441
return result;
430-
431442
} finally {
432443
// Step 6: Clean up - remove the embedded key from storage
433444
this.storage.delete(keyId);

account-kit/rn-signer/src/signer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class RNAlchemySignerSingleton extends BaseAlchemySigner<RNSignerClient>
5656
/**
5757
* Exports the wallet and returns the decrypted private key or seed phrase.
5858
* This is the recommended method for React Native apps.
59-
*
59+
*
6060
* @param {import("./client").ExportWalletParams} params Export parameters
6161
* @returns {Promise<import("./client").ExportWalletResult>} The decrypted export data
6262
* @throws {Error} If the user is not authenticated or export fails

0 commit comments

Comments
 (0)