diff --git a/packages/thirdweb/src/crypto/aes/decrypt.ts b/packages/thirdweb/src/crypto/aes/decrypt.ts new file mode 100644 index 00000000000..1477f7d2a7b --- /dev/null +++ b/packages/thirdweb/src/crypto/aes/decrypt.ts @@ -0,0 +1,80 @@ +import { getCachedTextDecoder, getCachedTextEncoder } from "../utils/cache"; +import { + decryptCryptoJSCipherBase64, + parseCryptoJSCipherBase64, +} from "./utils/crypto-js-compat"; +import { base64ToUint8Array } from "../utils/uint8array-extras"; +import { universalCrypto } from "../utils/universal-crypto"; + +/** + * Decrypts ciphertext encrypted with aesEncrypt() using supplied password. + * + * @param ciphertext - Ciphertext to be decrypted. + * @param password - Password to use to decrypt ciphertext. + * @returns Decrypted plaintext. + * + * @example + * const plaintext = await aesDecrypt(ciphertext, 'pw'); + */ +export async function aesDecrypt( + ciphertext: string, + password: string, +): Promise { + const crypto = await universalCrypto(); + // encode password as UTF-8 + const pwUtf8 = getCachedTextEncoder().encode(password); + // hash the password + const pwHash = await crypto.subtle.digest("SHA-256", pwUtf8); + + const cipherUint8Array = base64ToUint8Array(ciphertext); + + // iv + const iv = cipherUint8Array.slice(0, 12); + + // specify algorithm to use + const alg = { name: "AES-GCM", iv }; + + // generate key from pw + const key = await crypto.subtle.importKey("raw", pwHash, alg, false, [ + "decrypt", + ]); + + // ciphertext + const ctUint8 = cipherUint8Array.slice(12); + + try { + // decrypt ciphertext using key + const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); + // return the plaintext from ArrayBuffer + return getCachedTextDecoder().decode(plainBuffer); + } catch (e) { + throw new Error("Decrypt failed"); + } +} + +/** + * Decrypts ciphertext encrypted with aesEncrypt() OR "crypto-js".AES using supplied password. + * + * @param ciphertext - Ciphertext to be decrypted. + * @param password - Password to use to decrypt ciphertext. + * @returns Decrypted plaintext. + * + * @example + * const plaintext = await aesDecryptCompat(ciphertext, 'pw'); + */ +export async function aesDecryptCompat( + ciphertext: string, + password: string, +): Promise { + // determine if we're dealing with a legacy (cryptojs) ciphertext + const cryptoJs = parseCryptoJSCipherBase64(ciphertext); + if (cryptoJs.salt && cryptoJs.ciphertext) { + return decryptCryptoJSCipherBase64( + cryptoJs.salt, + cryptoJs.ciphertext, + password, + ); + } + // otherwise assume it's a ciphertext generated by aesEncrypt() + return aesDecrypt(ciphertext, password); +} diff --git a/packages/thirdweb/src/crypto/aes/encrypt.ts b/packages/thirdweb/src/crypto/aes/encrypt.ts new file mode 100644 index 00000000000..fbad71c1ffe --- /dev/null +++ b/packages/thirdweb/src/crypto/aes/encrypt.ts @@ -0,0 +1,47 @@ +import { + concatUint8Arrays, + uint8ArrayToBase64, +} from "../utils/uint8array-extras"; +import { getCachedTextEncoder } from "../utils/cache"; +import { universalCrypto } from "../utils/universal-crypto"; + +/** + * Encrypts plaintext using AES-GCM with supplied password, for decryption with aesDecrypt(). + * + * @param plaintext - Plaintext to be encrypted. + * @param password - Password to use to encrypt plaintext. + * @returns Encrypted ciphertext. + * + * @example + * const ciphertext = await aesEncrypt('my secret text', 'pw'); + */ +export async function aesEncrypt( + plaintext: string, + password: string, +): Promise { + const crypto = await universalCrypto(); + const textEncoder = getCachedTextEncoder(); + // encode password as UTF-8 + const pwUtf8 = textEncoder.encode(password); + // hash the password + const pwHash = await crypto.subtle.digest("SHA-256", pwUtf8); + + // get 96-bit random iv + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // specify algorithm to use + const alg = { name: "AES-GCM", iv }; + + // generate key from pw + const key = await crypto.subtle.importKey("raw", pwHash, alg, false, [ + "encrypt", + ]); + + // encode plaintext as UTF-8 + const ptUint8 = textEncoder.encode(plaintext); + // encrypt plaintext using key + const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8); + + // iv+ciphertext base64-encoded + return uint8ArrayToBase64(concatUint8Arrays([iv, new Uint8Array(ctBuffer)])); +} diff --git a/packages/thirdweb/src/crypto/aes/lib/md5.ts b/packages/thirdweb/src/crypto/aes/lib/md5.ts new file mode 100644 index 00000000000..619f4034bb5 --- /dev/null +++ b/packages/thirdweb/src/crypto/aes/lib/md5.ts @@ -0,0 +1,420 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +// stripped down version of `js-md5` +// changes: +// - we know we always have ArrayBuffer available +// - we only care about `arrayBuffer` output +// - we want to behave the same regardless of NODE or non NODE env +// - transformed into class +// - typescript + +/** + * [js-md5]{@link https://github.com/emn178/js-md5} + * @namespace md5 + * @version 0.8.3 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2023 + * @license MIT + */ + +const INPUT_ERROR = "input is invalid type"; +const FINALIZE_ERROR = "finalize already called"; + +const EXTRA = [128, 32768, 8388608, -2147483648]; + +// [message: string, isString: bool] +function formatMessage( + message: string | any[] | ArrayBuffer | ArrayLike, +): [string | Uint8Array | Array | ArrayBufferView, boolean] { + const type = typeof message; + if (typeof message === "string") { + return [message, true]; + } + if (type !== "object" || message === null) { + throw new Error(INPUT_ERROR); + } + if (message instanceof ArrayBuffer) { + return [new Uint8Array(message), false]; + } + if (!Array.isArray(message) && !ArrayBuffer.isView(message)) { + throw new Error(INPUT_ERROR); + } + return [message, false]; +} + +/** + * Md5 class + * @class Md5 + * @description This is internal class. + * @see {@link md5.create} + */ +class Md5 { + buffer8: Uint8Array; + blocks: Uint32Array; + h0: number; + h1: number; + h2: number; + h3: number; + start: number; + bytes: number; + hBytes: number; + finalized: boolean; + hashed: boolean; + first: boolean; + lastByteIndex: number = 0; + constructor() { + // eslint-disable-next-line @typescript-eslint/no-shadow + const buffer = new ArrayBuffer(68); + this.buffer8 = new Uint8Array(buffer); + this.blocks = new Uint32Array(buffer); + + this.h0 = + this.h1 = + this.h2 = + this.h3 = + this.start = + this.bytes = + this.hBytes = + 0; + this.finalized = this.hashed = false; + this.first = true; + } + /** + * @method update + * @memberof Md5 + * @instance + * @description Update hash + * @param {String|Array|Uint8Array|ArrayBuffer} message message to hash + * @returns {Md5} Md5 object. + * @see {@link md5.update} + * @internal + */ + update(inputMessage: string | any[] | ArrayBuffer | ArrayLike): Md5 { + if (this.finalized) { + throw new Error(FINALIZE_ERROR); + } + + const [message, isString] = formatMessage(inputMessage); + + const blocks = this.blocks; + let length = 0; + if (ArrayBuffer.isView(message)) { + length = message.byteLength; + } else { + length = message.length; + } + let code, + index = 0, + i; + + // eslint-disable-next-line @typescript-eslint/no-shadow + const buffer8 = this.buffer8; + + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = blocks[16]!; + blocks[16] = + blocks[1] = + blocks[2] = + blocks[3] = + blocks[4] = + blocks[5] = + blocks[6] = + blocks[7] = + blocks[8] = + blocks[9] = + blocks[10] = + blocks[11] = + blocks[12] = + blocks[13] = + blocks[14] = + blocks[15] = + 0; + } + + if (isString) { + for (i = this.start; index < length && i < 64; ++index) { + code = (message as string).charCodeAt(index); + if (code < 0x80) { + buffer8[i++] = code; + } else if (code < 0x800) { + buffer8[i++] = 0xc0 | (code >>> 6); + buffer8[i++] = 0x80 | (code & 0x3f); + } else if (code < 0xd800 || code >= 0xe000) { + buffer8[i++] = 0xe0 | (code >>> 12); + buffer8[i++] = 0x80 | ((code >>> 6) & 0x3f); + buffer8[i++] = 0x80 | (code & 0x3f); + } else { + code = + 0x10000 + + (((code & 0x3ff) << 10) | + ((message as string).charCodeAt(++index) & 0x3ff)); + buffer8[i++] = 0xf0 | (code >>> 18); + buffer8[i++] = 0x80 | ((code >>> 12) & 0x3f); + buffer8[i++] = 0x80 | ((code >>> 6) & 0x3f); + buffer8[i++] = 0x80 | (code & 0x3f); + } + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + // at this point we know it's not a string + buffer8[i++] = (message as any)[index]; + } + } + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += (this.bytes / 4294967296) << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + } + finalize() { + if (this.finalized) { + return; + } + this.finalized = true; + // eslint-disable-next-line @typescript-eslint/no-shadow + const blocks = this.blocks, + i = this.lastByteIndex; + blocks[i >>> 2] |= EXTRA[i & 3]!; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = blocks[16]!; + blocks[16] = + blocks[1] = + blocks[2] = + blocks[3] = + blocks[4] = + blocks[5] = + blocks[6] = + blocks[7] = + blocks[8] = + blocks[9] = + blocks[10] = + blocks[11] = + blocks[12] = + blocks[13] = + blocks[14] = + blocks[15] = + 0; + } + blocks[14] = this.bytes << 3; + blocks[15] = (this.hBytes << 3) | (this.bytes >>> 29); + this.hash(); + } + hash() { + const blocks = this.blocks; + let a, b, c, d, bc, da; + + if (this.first) { + a = blocks[0]! - 680876937; + a = (((a << 7) | (a >>> 25)) - 271733879) << 0; + d = (-1732584194 ^ (a & 2004318071)) + blocks[1]! - 117830708; + d = (((d << 12) | (d >>> 20)) + a) << 0; + c = (-271733879 ^ (d & (a ^ -271733879))) + blocks[2]! - 1126478375; + c = (((c << 17) | (c >>> 15)) + d) << 0; + b = (a ^ (c & (d ^ a))) + blocks[3]! - 1316259209; + b = (((b << 22) | (b >>> 10)) + c) << 0; + } else { + a = this.h0; + b = this.h1; + c = this.h2; + d = this.h3; + a += (d ^ (b & (c ^ d))) + blocks[0]! - 680876936; + a = (((a << 7) | (a >>> 25)) + b) << 0; + d += (c ^ (a & (b ^ c))) + blocks[1]! - 389564586; + d = (((d << 12) | (d >>> 20)) + a) << 0; + c += (b ^ (d & (a ^ b))) + blocks[2]! + 606105819; + c = (((c << 17) | (c >>> 15)) + d) << 0; + b += (a ^ (c & (d ^ a))) + blocks[3]! - 1044525330; + b = (((b << 22) | (b >>> 10)) + c) << 0; + } + + a += (d ^ (b & (c ^ d))) + blocks[4]! - 176418897; + a = (((a << 7) | (a >>> 25)) + b) << 0; + d += (c ^ (a & (b ^ c))) + blocks[5]! + 1200080426; + d = (((d << 12) | (d >>> 20)) + a) << 0; + c += (b ^ (d & (a ^ b))) + blocks[6]! - 1473231341; + c = (((c << 17) | (c >>> 15)) + d) << 0; + b += (a ^ (c & (d ^ a))) + blocks[7]! - 45705983; + b = (((b << 22) | (b >>> 10)) + c) << 0; + a += (d ^ (b & (c ^ d))) + blocks[8]! + 1770035416; + a = (((a << 7) | (a >>> 25)) + b) << 0; + d += (c ^ (a & (b ^ c))) + blocks[9]! - 1958414417; + d = (((d << 12) | (d >>> 20)) + a) << 0; + c += (b ^ (d & (a ^ b))) + blocks[10]! - 42063; + c = (((c << 17) | (c >>> 15)) + d) << 0; + b += (a ^ (c & (d ^ a))) + blocks[11]! - 1990404162; + b = (((b << 22) | (b >>> 10)) + c) << 0; + a += (d ^ (b & (c ^ d))) + blocks[12]! + 1804603682; + a = (((a << 7) | (a >>> 25)) + b) << 0; + d += (c ^ (a & (b ^ c))) + blocks[13]! - 40341101; + d = (((d << 12) | (d >>> 20)) + a) << 0; + c += (b ^ (d & (a ^ b))) + blocks[14]! - 1502002290; + c = (((c << 17) | (c >>> 15)) + d) << 0; + b += (a ^ (c & (d ^ a))) + blocks[15]! + 1236535329; + b = (((b << 22) | (b >>> 10)) + c) << 0; + a += (c ^ (d & (b ^ c))) + blocks[1]! - 165796510; + a = (((a << 5) | (a >>> 27)) + b) << 0; + d += (b ^ (c & (a ^ b))) + blocks[6]! - 1069501632; + d = (((d << 9) | (d >>> 23)) + a) << 0; + c += (a ^ (b & (d ^ a))) + blocks[11]! + 643717713; + c = (((c << 14) | (c >>> 18)) + d) << 0; + b += (d ^ (a & (c ^ d))) + blocks[0]! - 373897302; + b = (((b << 20) | (b >>> 12)) + c) << 0; + a += (c ^ (d & (b ^ c))) + blocks[5]! - 701558691; + a = (((a << 5) | (a >>> 27)) + b) << 0; + d += (b ^ (c & (a ^ b))) + blocks[10]! + 38016083; + d = (((d << 9) | (d >>> 23)) + a) << 0; + c += (a ^ (b & (d ^ a))) + blocks[15]! - 660478335; + c = (((c << 14) | (c >>> 18)) + d) << 0; + b += (d ^ (a & (c ^ d))) + blocks[4]! - 405537848; + b = (((b << 20) | (b >>> 12)) + c) << 0; + a += (c ^ (d & (b ^ c))) + blocks[9]! + 568446438; + a = (((a << 5) | (a >>> 27)) + b) << 0; + d += (b ^ (c & (a ^ b))) + blocks[14]! - 1019803690; + d = (((d << 9) | (d >>> 23)) + a) << 0; + c += (a ^ (b & (d ^ a))) + blocks[3]! - 187363961; + c = (((c << 14) | (c >>> 18)) + d) << 0; + b += (d ^ (a & (c ^ d))) + blocks[8]! + 1163531501; + b = (((b << 20) | (b >>> 12)) + c) << 0; + a += (c ^ (d & (b ^ c))) + blocks[13]! - 1444681467; + a = (((a << 5) | (a >>> 27)) + b) << 0; + d += (b ^ (c & (a ^ b))) + blocks[2]! - 51403784; + d = (((d << 9) | (d >>> 23)) + a) << 0; + c += (a ^ (b & (d ^ a))) + blocks[7]! + 1735328473; + c = (((c << 14) | (c >>> 18)) + d) << 0; + b += (d ^ (a & (c ^ d))) + blocks[12]! - 1926607734; + b = (((b << 20) | (b >>> 12)) + c) << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[5]! - 378558; + a = (((a << 4) | (a >>> 28)) + b) << 0; + d += (bc ^ a) + blocks[8]! - 2022574463; + d = (((d << 11) | (d >>> 21)) + a) << 0; + da = d ^ a; + c += (da ^ b) + blocks[11]! + 1839030562; + c = (((c << 16) | (c >>> 16)) + d) << 0; + b += (da ^ c) + blocks[14]! - 35309556; + b = (((b << 23) | (b >>> 9)) + c) << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[1]! - 1530992060; + a = (((a << 4) | (a >>> 28)) + b) << 0; + d += (bc ^ a) + blocks[4]! + 1272893353; + d = (((d << 11) | (d >>> 21)) + a) << 0; + da = d ^ a; + c += (da ^ b) + blocks[7]! - 155497632; + c = (((c << 16) | (c >>> 16)) + d) << 0; + b += (da ^ c) + blocks[10]! - 1094730640; + b = (((b << 23) | (b >>> 9)) + c) << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[13]! + 681279174; + a = (((a << 4) | (a >>> 28)) + b) << 0; + d += (bc ^ a) + blocks[0]! - 358537222; + d = (((d << 11) | (d >>> 21)) + a) << 0; + da = d ^ a; + c += (da ^ b) + blocks[3]! - 722521979; + c = (((c << 16) | (c >>> 16)) + d) << 0; + b += (da ^ c) + blocks[6]! + 76029189; + b = (((b << 23) | (b >>> 9)) + c) << 0; + bc = b ^ c; + a += (bc ^ d) + blocks[9]! - 640364487; + a = (((a << 4) | (a >>> 28)) + b) << 0; + d += (bc ^ a) + blocks[12]! - 421815835; + d = (((d << 11) | (d >>> 21)) + a) << 0; + da = d ^ a; + c += (da ^ b) + blocks[15]! + 530742520; + c = (((c << 16) | (c >>> 16)) + d) << 0; + b += (da ^ c) + blocks[2]! - 995338651; + b = (((b << 23) | (b >>> 9)) + c) << 0; + a += (c ^ (b | ~d)) + blocks[0]! - 198630844; + a = (((a << 6) | (a >>> 26)) + b) << 0; + d += (b ^ (a | ~c)) + blocks[7]! + 1126891415; + d = (((d << 10) | (d >>> 22)) + a) << 0; + c += (a ^ (d | ~b)) + blocks[14]! - 1416354905; + c = (((c << 15) | (c >>> 17)) + d) << 0; + b += (d ^ (c | ~a)) + blocks[5]! - 57434055; + b = (((b << 21) | (b >>> 11)) + c) << 0; + a += (c ^ (b | ~d)) + blocks[12]! + 1700485571; + a = (((a << 6) | (a >>> 26)) + b) << 0; + d += (b ^ (a | ~c)) + blocks[3]! - 1894986606; + d = (((d << 10) | (d >>> 22)) + a) << 0; + c += (a ^ (d | ~b)) + blocks[10]! - 1051523; + c = (((c << 15) | (c >>> 17)) + d) << 0; + b += (d ^ (c | ~a)) + blocks[1]! - 2054922799; + b = (((b << 21) | (b >>> 11)) + c) << 0; + a += (c ^ (b | ~d)) + blocks[8]! + 1873313359; + a = (((a << 6) | (a >>> 26)) + b) << 0; + d += (b ^ (a | ~c)) + blocks[15]! - 30611744; + d = (((d << 10) | (d >>> 22)) + a) << 0; + c += (a ^ (d | ~b)) + blocks[6]! - 1560198380; + c = (((c << 15) | (c >>> 17)) + d) << 0; + b += (d ^ (c | ~a)) + blocks[13]! + 1309151649; + b = (((b << 21) | (b >>> 11)) + c) << 0; + a += (c ^ (b | ~d)) + blocks[4]! - 145523070; + a = (((a << 6) | (a >>> 26)) + b) << 0; + d += (b ^ (a | ~c)) + blocks[11]! - 1120210379; + d = (((d << 10) | (d >>> 22)) + a) << 0; + c += (a ^ (d | ~b)) + blocks[2]! + 718787259; + c = (((c << 15) | (c >>> 17)) + d) << 0; + b += (d ^ (c | ~a)) + blocks[9]! - 343485551; + b = (((b << 21) | (b >>> 11)) + c) << 0; + + if (this.first) { + this.h0 = (a + 1732584193) << 0; + this.h1 = (b - 271733879) << 0; + this.h2 = (c - 1732584194) << 0; + this.h3 = (d + 271733878) << 0; + this.first = false; + } else { + this.h0 = (this.h0 + a) << 0; + this.h1 = (this.h1 + b) << 0; + this.h2 = (this.h2 + c) << 0; + this.h3 = (this.h3 + d) << 0; + } + } + + /** + * @method arrayBuffer + * @memberof Md5 + * @instance + * @description Output hash as ArrayBuffer + * @returns {ArrayBuffer} ArrayBuffer + * @see {@link md5.arrayBuffer} + * @example + * hash.arrayBuffer(); + * @internal + */ + arrayBuffer(): ArrayBuffer { + this.finalize(); + + // eslint-disable-next-line @typescript-eslint/no-shadow + const buffer = new ArrayBuffer(16); + // eslint-disable-next-line @typescript-eslint/no-shadow + const blocks = new Uint32Array(buffer); + blocks[0] = this.h0; + blocks[1] = this.h1; + blocks[2] = this.h2; + blocks[3] = this.h3; + return buffer; + } +} + +/** + * @internal + */ +export function arrayBuffer(uint8Arr: Uint8Array) { + const md5 = new Md5(); + md5.update(uint8Arr); + return md5.arrayBuffer(); +} diff --git a/packages/thirdweb/src/crypto/aes/utils/crypto-js-compat.ts b/packages/thirdweb/src/crypto/aes/utils/crypto-js-compat.ts new file mode 100644 index 00000000000..ada675f1ea3 --- /dev/null +++ b/packages/thirdweb/src/crypto/aes/utils/crypto-js-compat.ts @@ -0,0 +1,128 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { + getCachedTextDecoder, + getCachedTextEncoder, +} from "../../utils/cache.js"; +import { arrayBuffer } from "../lib/md5.js"; +import { + base64ToUint8Array, + concatUint8Arrays, +} from "../../utils/uint8array-extras.js"; +import { universalCrypto } from "../../utils/universal-crypto.js"; + +/** + * This is an implementation of the CryptoJS AES decryption scheme, without actually relying on crypto-js. + */ + +const HEAD_SIZE_DWORD = 2; +const SALT_SIZE_DWORD = 2; + +/** + * @internal + */ +export async function decryptCryptoJSCipherBase64( + salt: Uint8Array, + ciphertext: Uint8Array, + password: string, + { keySizeDWORD = 256 / 32, ivSizeDWORD = 128 / 32, iterations = 1 } = {}, +) { + const crypto = await universalCrypto(); + const { key, iv } = await dangerouslyDeriveParameters( + password, + salt, + keySizeDWORD, + ivSizeDWORD, + iterations, + ); + + try { + // decrypt ciphertext using key + const plainBuffer = await crypto.subtle.decrypt( + { name: "AES-CBC", iv }, + key, + ciphertext, + ); + // return the plaintext from ArrayBuffer + return getCachedTextDecoder().decode(plainBuffer); + } catch (e) { + throw new Error("Decrypt failed"); + } +} + +/** + * @internal + */ +export function parseCryptoJSCipherBase64(cryptoJSCipherBase64: string) { + let salt: Uint8Array | null = null; + let ciphertext = base64ToUint8Array(cryptoJSCipherBase64); + + const [head, body] = splitUint8Array(ciphertext, HEAD_SIZE_DWORD * 4); + + // This effectively checks if the ciphertext starts with 'Salted__', which is the crypto-js convention. + const headDataView = new DataView(head!.buffer); + if ( + headDataView.getInt32(0) === 0x53616c74 && + headDataView.getInt32(4) === 0x65645f5f + ) { + const [_salt, _ciphertext] = splitUint8Array(body!, SALT_SIZE_DWORD * 4); + salt = _salt!; + ciphertext = _ciphertext!; + } + + return { ciphertext, salt }; +} + +async function dangerouslyDeriveParameters( + password: string, + salt: Uint8Array, + keySizeDWORD: number, + ivSizeDWORD: number, + iterations: number, +) { + const crypto = await universalCrypto(); + const passwordUint8Array = getCachedTextEncoder().encode(password); + + const keyPlusIV = dangerousEVPKDF( + passwordUint8Array, + salt, + keySizeDWORD + ivSizeDWORD, + iterations, + ); + const [rawKey, iv] = splitUint8Array(keyPlusIV, keySizeDWORD * 4); + + const key = await crypto.subtle.importKey("raw", rawKey!, "AES-CBC", false, [ + "decrypt", + ]); + + return { key, iv }; +} + +function dangerousEVPKDF( + passwordUint8Array: Uint8Array, + saltUint8Array: Uint8Array, + keySizeDWORD: number, + iterations: number, +) { + let derivedKey = new Uint8Array(); + let block = new Uint8Array(); + + while (derivedKey.byteLength < keySizeDWORD * 4) { + block = new Uint8Array( + arrayBuffer( + concatUint8Arrays([block, passwordUint8Array, saltUint8Array]), + ), + ); + + for (let i = 1; i < iterations; i++) { + block = new Uint8Array(arrayBuffer(block)); + } + + derivedKey = concatUint8Arrays([derivedKey, block]); + } + + return derivedKey; +} + +function splitUint8Array(a: Uint8Array, i: number) { + return [a.subarray(0, i), a.subarray(i, a.length)]; +} diff --git a/packages/thirdweb/src/crypto/utils/cache.ts b/packages/thirdweb/src/crypto/utils/cache.ts new file mode 100644 index 00000000000..797ebb78d96 --- /dev/null +++ b/packages/thirdweb/src/crypto/utils/cache.ts @@ -0,0 +1,35 @@ +class TextProcessorCache { + private _encoder: TextEncoder | undefined; + private _decoder: TextDecoder | undefined; + + get encoder(): TextEncoder { + if (!this._encoder) { + this._encoder = new TextEncoder(); + } + return this._encoder; + } + + get decoder(): TextDecoder { + if (!this._decoder) { + this._decoder = new TextDecoder(); + } + return this._decoder; + } +} + +// create a singleton instance of the TextProcessorCache +const textProcessorSingleton = new TextProcessorCache(); + +/** + * @internal + */ +export function getCachedTextEncoder(): TextEncoder { + return textProcessorSingleton.encoder; +} + +/** + * @internal + */ +export function getCachedTextDecoder(): TextDecoder { + return textProcessorSingleton.decoder; +} diff --git a/packages/thirdweb/src/crypto/utils/uint8array-extras.d.ts b/packages/thirdweb/src/crypto/utils/uint8array-extras.d.ts new file mode 100644 index 00000000000..4a15919f8af --- /dev/null +++ b/packages/thirdweb/src/crypto/utils/uint8array-extras.d.ts @@ -0,0 +1,251 @@ +export type TypedArray = + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array + | BigInt64Array + | BigUint64Array; + +/** +Check if the given value is an instance of `Uint8Array`. + +Replacement for [`Buffer.isBuffer()`](https://nodejs.org/api/buffer.html#static-method-bufferisbufferobj). + +@example +``` +import {isUint8Array} from 'uint8array-extras'; + +console.log(isUint8Array(new Uint8Array())); +//=> true + +console.log(isUint8Array(Buffer.from('x'))); +//=> true + +console.log(isUint8Array(new ArrayBuffer(10))); +//=> false +``` +*/ +export function isUint8Array(value: unknown): value is Uint8Array; + +/** +Throw a `TypeError` if the given value is not an instance of `Uint8Array`. + +@example +``` +import {assertUint8Array} from 'uint8array-extras'; + +try { + assertUint8Array(new ArrayBuffer(10)); // Throws a TypeError +} catch (error) { + console.error(error.message); +} +``` +*/ +export function assertUint8Array(value: unknown): asserts value is Uint8Array; + +/** +Convert a value to a `Uint8Array` without copying its data. + +This can be useful for converting a `Buffer` to a pure `Uint8Array`. `Buffer` is already an `Uint8Array` subclass, but [`Buffer` alters some behavior](https://sindresorhus.com/blog/goodbye-nodejs-buffer), so it can be useful to cast it to a pure `Uint8Array` before returning it. + +Tip: If you want a copy, just call `.slice()` on the return value. +*/ +export function toUint8Array(value: TypedArray | ArrayBuffer | DataView): Uint8Array; + +/** +Concatenate the given arrays into a new array. + +If `arrays` is empty, it will return a zero-sized `Uint8Array`. + +If `totalLength` is not specified, it is calculated from summing the lengths of the given arrays. + +Replacement for [`Buffer.concat()`](https://nodejs.org/api/buffer.html#static-method-bufferconcatlist-totallength). + +@example +``` +import {concatUint8Arrays} from 'uint8array-extras'; + +const a = new Uint8Array([1, 2, 3]); +const b = new Uint8Array([4, 5, 6]); + +console.log(concatUint8Arrays([a, b])); +//=> Uint8Array [1, 2, 3, 4, 5, 6] +``` +*/ +export function concatUint8Arrays(arrays: Uint8Array[], totalLength?: number): Uint8Array; + +/** +Check if two arrays are identical by verifying that they contain the same bytes in the same sequence. + +Replacement for [`Buffer#equals()`](https://nodejs.org/api/buffer.html#bufequalsotherbuffer). + +@example +``` +import {areUint8ArraysEqual} from 'uint8array-extras'; + +const a = new Uint8Array([1, 2, 3]); +const b = new Uint8Array([1, 2, 3]); +const c = new Uint8Array([4, 5, 6]); + +console.log(areUint8ArraysEqual(a, b)); +//=> true + +console.log(areUint8ArraysEqual(a, c)); +//=> false +``` +*/ +export function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean; + +/** +Compare two arrays and indicate their relative order or equality. Useful for sorting. + +Replacement for [`Buffer.compare()`](https://nodejs.org/api/buffer.html#static-method-buffercomparebuf1-buf2). + +@example +``` +import {compareUint8Arrays} from 'uint8array-extras'; + +const array1 = new Uint8Array([1, 2, 3]); +const array2 = new Uint8Array([4, 5, 6]); +const array3 = new Uint8Array([7, 8, 9]); + +[array3, array1, array2].sort(compareUint8Arrays); +//=> [[1, 2, 3], [4, 5, 6], [7, 8, 9]] +``` +*/ +export function compareUint8Arrays(a: Uint8Array, b: Uint8Array): 0 | 1 | -1; + +/** +Convert a `Uint8Array` (containing a UTF-8 string) to a string. + +Replacement for [`Buffer#toString()`](https://nodejs.org/api/buffer.html#buftostringencoding-start-end). + +@example +``` +import {uint8ArrayToString} from 'uint8array-extras'; + +const byteArray = new Uint8Array([72, 101, 108, 108, 111]); + +console.log(uint8ArrayToString(byteArray)); +//=> 'Hello' +``` +*/ +export function uint8ArrayToString(array: Uint8Array): string; + +/** +Convert a string to a `Uint8Array` (using UTF-8 encoding). + +Replacement for [`Buffer.from('Hello')`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding). + +@example +``` +import {stringToUint8Array} from 'uint8array-extras'; + +console.log(stringToUint8Array('Hello')); +//=> Uint8Array [72, 101, 108, 108, 111] +``` +*/ +export function stringToUint8Array(string: string): Uint8Array; + +/** +Convert a `Uint8Array` to a Base64-encoded string. + +Specify `{urlSafe: true}` to get a [Base64URL](https://base64.guru/standards/base64url)-encoded string. + +Replacement for [`Buffer#toString('base64')`](https://nodejs.org/api/buffer.html#buftostringencoding-start-end). + +@example +``` +import {uint8ArrayToBase64} from 'uint8array-extras'; + +const byteArray = new Uint8Array([72, 101, 108, 108, 111]); + +console.log(uint8ArrayToBase64(byteArray)); +//=> 'SGVsbG8=' +``` +*/ +export function uint8ArrayToBase64(array: Uint8Array, options?: {urlSafe: boolean}): string; + +/** +Convert a Base64-encoded or [Base64URL](https://base64.guru/standards/base64url)-encoded string to a `Uint8Array`. + +Replacement for [`Buffer.from('SGVsbG8=', 'base64')`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding). + +@example +``` +import {base64ToUint8Array} from 'uint8array-extras'; + +console.log(base64ToUint8Array('SGVsbG8=')); +//=> Uint8Array [72, 101, 108, 108, 111] +``` +*/ +export function base64ToUint8Array(string: string): Uint8Array; + +/** +Encode a string to Base64-encoded string. + +Specify `{urlSafe: true}` to get a [Base64URL](https://base64.guru/standards/base64url)-encoded string. + +Replacement for `Buffer.from('Hello').toString('base64')` and [`btoa()`](https://developer.mozilla.org/en-US/docs/Web/API/btoa). + +@example +``` +import {stringToBase64} from 'uint8array-extras'; + +console.log(stringToBase64('Hello')); +//=> 'SGVsbG8=' +``` +*/ +export function stringToBase64(string: string, options?: {urlSafe: boolean}): string; + +/** +Decode a Base64-encoded or [Base64URL](https://base64.guru/standards/base64url)-encoded string to a string. + +Replacement for `Buffer.from('SGVsbG8=', 'base64').toString()` and [`atob()`](https://developer.mozilla.org/en-US/docs/Web/API/atob). + +@example +``` +import {base64ToString} from 'uint8array-extras'; + +console.log(base64ToString('SGVsbG8=')); +//=> 'Hello' +``` +*/ +export function base64ToString(base64String: string): string; + +/** +Convert a `Uint8Array` to a Hex string. + +Replacement for [`Buffer#toString('hex')`](https://nodejs.org/api/buffer.html#buftostringencoding-start-end). + +@example +``` +import {uint8ArrayToHex} from 'uint8array-extras'; + +const byteArray = new Uint8Array([72, 101, 108, 108, 111]); + +console.log(uint8ArrayToHex(byteArray)); +//=> '48656c6c6f' +``` +*/ +export function uint8ArrayToHex(array: Uint8Array): string; + +/** +Convert a Hex string to a `Uint8Array`. + +Replacement for [`Buffer.from('48656c6c6f', 'hex')`](https://nodejs.org/api/buffer.html#static-method-bufferfromstring-encoding). + +@example +``` +import {hexToUint8Array} from 'uint8array-extras'; + +console.log(hexToUint8Array('48656c6c6f')); +//=> Uint8Array [72, 101, 108, 108, 111] +``` +*/ +export function hexToUint8Array(hexString: string): Uint8Array; \ No newline at end of file diff --git a/packages/thirdweb/src/crypto/utils/uint8array-extras.js b/packages/thirdweb/src/crypto/utils/uint8array-extras.js new file mode 100644 index 00000000000..53bfacbffe7 --- /dev/null +++ b/packages/thirdweb/src/crypto/utils/uint8array-extras.js @@ -0,0 +1,242 @@ +// taken from: https://github.com/sindresorhus/uint8array-extras +// to make it work across CJS and ESM -> once we're fully ESM we can remove this and instead use the package directly + +import { getCachedTextDecoder, getCachedTextEncoder } from "./cache"; + +const objectToString = Object.prototype.toString; +const uint8ArrayStringified = "[object Uint8Array]"; + +export function isUint8Array(value) { + if (!value) { + return false; + } + + if (value.constructor === Uint8Array) { + return true; + } + + return objectToString.call(value) === uint8ArrayStringified; +} + +export function assertUint8Array(value) { + if (!isUint8Array(value)) { + throw new TypeError(`Expected \`Uint8Array\`, got \`${typeof value}\``); + } +} + +export function toUint8Array(value) { + if (value instanceof ArrayBuffer) { + return new Uint8Array(value); + } + + if (ArrayBuffer.isView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + + throw new TypeError(`Unsupported value, got \`${typeof value}\`.`); +} + +export function concatUint8Arrays(arrays, totalLength) { + if (arrays.length === 0) { + return new Uint8Array(0); + } + + totalLength ??= arrays.reduce( + (accumulator, currentValue) => accumulator + currentValue.length, + 0, + ); + + const returnValue = new Uint8Array(totalLength); + + let offset = 0; + for (const array of arrays) { + assertUint8Array(array); + returnValue.set(array, offset); + offset += array.length; + } + + return returnValue; +} + +export function areUint8ArraysEqual(a, b) { + assertUint8Array(a); + assertUint8Array(b); + + if (a === b) { + return true; + } + + if (a.length !== b.length) { + return false; + } + + for (let index = 0; index < a.length; index++) { + if (a[index] !== b[index]) { + return false; + } + } + + return true; +} + +export function compareUint8Arrays(a, b) { + assertUint8Array(a); + assertUint8Array(b); + + const length = Math.min(a.length, b.length); + + for (let index = 0; index < length; index++) { + if (a[index] < b[index]) { + return -1; + } + + if (a[index] > b[index]) { + return 1; + } + } + + // At this point, all the compared elements are equal. + // The shorter array should come first if the arrays are of different lengths. + if (a.length > b.length) { + return 1; + } + + if (a.length < b.length) { + return -1; + } + + return 0; +} + +export function uint8ArrayToString(array) { + assertUint8Array(array); + return getCachedTextDecoder().decode(array); +} + +function assertString(value) { + if (typeof value !== "string") { + throw new TypeError(`Expected \`string\`, got \`${typeof value}\``); + } +} + +export function stringToUint8Array(string) { + assertString(string); + return getCachedTextEncoder().encode(string); +} + +function base64ToBase64Url(base64) { + return base64.replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, ""); +} + +function base64UrlToBase64(base64url) { + return base64url.replaceAll("-", "+").replaceAll("_", "/"); +} + +// Reference: https://phuoc.ng/collection/this-vs-that/concat-vs-push/ +const MAX_BLOCK_SIZE = 65_535; + +export function uint8ArrayToBase64(array, { urlSafe = false } = {}) { + assertUint8Array(array); + + let base64; + + if (array.length < MAX_BLOCK_SIZE) { + // Required as `btoa` and `atob` don't properly support Unicode: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem + base64 = globalThis.btoa(String.fromCodePoint.apply(this, array)); + } else { + base64 = ""; + for (const value of array) { + base64 += String.fromCodePoint(value); + } + + base64 = globalThis.btoa(base64); + } + + return urlSafe ? base64ToBase64Url(base64) : base64; +} + +export function base64ToUint8Array(base64String) { + assertString(base64String); + return Uint8Array.from( + globalThis.atob(base64UrlToBase64(base64String)), + (x) => x.codePointAt(0), + ); +} + +export function stringToBase64(string, { urlSafe = false } = {}) { + assertString(string); + return uint8ArrayToBase64(stringToUint8Array(string), { urlSafe }); +} + +export function base64ToString(base64String) { + assertString(base64String); + return uint8ArrayToString(base64ToUint8Array(base64String)); +} + +const byteToHexLookupTable = Array.from({ length: 256 }, (_, index) => + index.toString(16).padStart(2, "0"), +); + +export function uint8ArrayToHex(array) { + assertUint8Array(array); + + // Concatenating a string is faster than using an array. + let hexString = ""; + + for (let index = 0; index < array.length; index++) { + hexString += byteToHexLookupTable[array[index]]; + } + + return hexString; +} + +const hexToDecimalLookupTable = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + 4: 4, + 5: 5, + 6: 6, + 7: 7, + 8: 8, + 9: 9, + a: 10, + b: 11, + c: 12, + d: 13, + e: 14, + f: 15, + A: 10, + B: 11, + C: 12, + D: 13, + E: 14, + F: 15, +}; + +export function hexToUint8Array(hexString) { + assertString(hexString); + + if (hexString.length % 2 !== 0) { + throw new Error("Invalid Hex string length."); + } + + const resultLength = hexString.length / 2; + const bytes = new Uint8Array(resultLength); + + for (let index = 0; index < resultLength; index++) { + const highNibble = hexToDecimalLookupTable[hexString[index * 2]]; + const lowNibble = hexToDecimalLookupTable[hexString[index * 2 + 1]]; + + if (highNibble === undefined || lowNibble === undefined) { + throw new Error( + `Invalid Hex character encountered at position ${index * 2}`, + ); + } + + bytes[index] = (highNibble << 4) | lowNibble; // eslint-disable-line no-bitwise + } + + return bytes; +} diff --git a/packages/thirdweb/src/crypto/utils/universal-crypto.ts b/packages/thirdweb/src/crypto/utils/universal-crypto.ts new file mode 100644 index 00000000000..a40f11d8d67 --- /dev/null +++ b/packages/thirdweb/src/crypto/utils/universal-crypto.ts @@ -0,0 +1,11 @@ +export async function universalCrypto(): Promise { + if ("crypto" in globalThis) { + return globalThis.crypto; + } + + // otherwise we are in node 18 so we can use `webcrypto` off of the "node:crypto" package and treat it as native + // trick bundlers so that they leave this alone :) + const pto = "pto"; + // this becomes `node:crypto` at runtime + return (await import("node" + ":cry" + pto)).webcrypto as Crypto; +} diff --git a/packages/thirdweb/src/exports/wallets.ts b/packages/thirdweb/src/exports/wallets.ts index bcee01f5c63..8035650ba6c 100644 --- a/packages/thirdweb/src/exports/wallets.ts +++ b/packages/thirdweb/src/exports/wallets.ts @@ -77,8 +77,8 @@ export { getSavedConnectParamsFromStorage, saveConnectParamsToStorage, deleteConnectParamsFromStorage, -} from "../wallets/manager/storage.js"; -export type { WithPersonalWalletConnectionOptions } from "../wallets/manager/storage.js"; +} from "../wallets/storage/walletStorage.js"; +export type { WithPersonalWalletConnectionOptions } from "../wallets/storage/walletStorage.js"; export { getStoredActiveWalletId, @@ -104,3 +104,21 @@ export { type MultiStepAuthArgsType, type SingleStepAuthArgsType, } from "../wallets/embedded/core/authentication/type.js"; + +// local wallet +export { + localWallet, + type LocalWallet, + type LocalWalletConnectionOptions, + type LocalWalletCreationOptions, +} from "../wallets/local/index.js"; +export type { + LocalWalletDecryptOptions, + LocalWalletEncryptOptions, + LocalWalletExportOptions, + LocalWalletImportOptions, + LocalWalletLoadOptions, + LocalWalletLoadOrCreateOptions, + LocalWalletSaveOptions, + LocalWalletStorageData, +} from "../wallets/local/types.js"; diff --git a/packages/thirdweb/src/exports/wallets/index.ts b/packages/thirdweb/src/exports/wallets/index.ts index f5a76586615..9f5efda4559 100644 --- a/packages/thirdweb/src/exports/wallets/index.ts +++ b/packages/thirdweb/src/exports/wallets/index.ts @@ -58,7 +58,7 @@ export type { WalletConnectConnectionOptions } from "../../wallets/wallet-connec export { smartWallet } from "../../wallets/smart/index.js"; export type { SmartWalletOptions } from "../../wallets/smart/types.js"; -export type { WithPersonalWalletConnectionOptions } from "../../wallets/manager/storage.js"; +export type { WithPersonalWalletConnectionOptions } from "../../wallets/storage/walletStorage.js"; export { coinbaseSDKWallet, diff --git a/packages/thirdweb/src/exports/wallets/local.ts b/packages/thirdweb/src/exports/wallets/local.ts new file mode 100644 index 00000000000..9d36a8b48a5 --- /dev/null +++ b/packages/thirdweb/src/exports/wallets/local.ts @@ -0,0 +1,16 @@ +export { + localWallet, + type LocalWallet, + type LocalWalletConnectionOptions, + type LocalWalletCreationOptions, +} from "../../wallets/local/index.js"; +export type { + LocalWalletDecryptOptions, + LocalWalletEncryptOptions, + LocalWalletExportOptions, + LocalWalletImportOptions, + LocalWalletLoadOptions, + LocalWalletLoadOrCreateOptions, + LocalWalletSaveOptions, + LocalWalletStorageData, +} from "../../wallets/local/types.js"; diff --git a/packages/thirdweb/src/react/hooks/connection/useAutoConnect.ts b/packages/thirdweb/src/react/hooks/connection/useAutoConnect.ts index efbf67321f3..822ea1bb9d2 100644 --- a/packages/thirdweb/src/react/hooks/connection/useAutoConnect.ts +++ b/packages/thirdweb/src/react/hooks/connection/useAutoConnect.ts @@ -13,7 +13,7 @@ import { import { getSavedConnectParamsFromStorage, type WithPersonalWalletConnectionOptions, -} from "../../../wallets/manager/storage.js"; +} from "../../../wallets/storage/walletStorage.js"; import type { WalletWithPersonalWallet } from "../../../wallets/interfaces/wallet.js"; let autoConnectAttempted = false; diff --git a/packages/thirdweb/src/wallets/coinbase/coinbaseSDKWallet.ts b/packages/thirdweb/src/wallets/coinbase/coinbaseSDKWallet.ts index 579f703315d..1f6ca625501 100644 --- a/packages/thirdweb/src/wallets/coinbase/coinbaseSDKWallet.ts +++ b/packages/thirdweb/src/wallets/coinbase/coinbaseSDKWallet.ts @@ -16,7 +16,7 @@ import { coinbaseMetadata } from "./coinbaseMetadata.js"; import { getSavedConnectParamsFromStorage, saveConnectParamsToStorage, -} from "../manager/storage.js"; +} from "../storage/walletStorage.js"; import { defineChain, getChainDataForChain } from "../../chains/utils.js"; import type { Chain } from "../../chains/types.js"; import { ethereum } from "../../chains/chain-definitions/ethereum.js"; diff --git a/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts b/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts index 8100e72675a..7744a05dbf8 100644 --- a/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts +++ b/packages/thirdweb/src/wallets/embedded/core/wallet/index.ts @@ -15,7 +15,7 @@ import { ethereum } from "../../../../chains/chain-definitions/ethereum.js"; import { getSavedConnectParamsFromStorage, saveConnectParamsToStorage, -} from "../../../manager/storage.js"; +} from "../../../storage/walletStorage.js"; import type { InitializedUser } from "../../implementations/index.js"; type SavedConnectParams = { diff --git a/packages/thirdweb/src/wallets/local/index.ts b/packages/thirdweb/src/wallets/local/index.ts new file mode 100644 index 00000000000..4c9cb084b44 --- /dev/null +++ b/packages/thirdweb/src/wallets/local/index.ts @@ -0,0 +1,605 @@ +import { mnemonicToAccount } from "viem/accounts"; +import type { Chain } from "../../chains/types.js"; +import type { ThirdwebClient } from "../../client/client.js"; +import type { Account, Wallet } from "../interfaces/wallet.js"; +import { privateKeyAccount, viemToThirdwebAccount } from "../private-key.js"; +import { + saveConnectParamsToStorage, + walletStorage, +} from "../storage/walletStorage.js"; +import type { WalletMetadata } from "../types.js"; +import type { AsyncStorage } from "../storage/AsyncStorage.js"; +import type { + LocalWalletLoadOrCreateOptions, + LocalWalletImportOptions, + LocalWalletLoadOptions, + LocalWalletSaveOptions, + LocalWalletStorageData, + LocalWalletExportOptions, +} from "./types.js"; +import { + getDecryptionFunction, + getEncryptionFunction, + isValidPrivateKey, +} from "./utils.js"; +import { english, generateMnemonic } from "viem/accounts"; +import { ethereum } from "../../chains/chain-definitions/ethereum.js"; +import { toHex } from "../../utils/encoding/hex.js"; + +export type LocalWalletCreationOptions = { + client: ThirdwebClient; +}; + +export type LocalWalletConnectionOptions = { + chain?: Chain; +}; + +type SavedConnectParams = { + chain?: Chain; +}; + +const STORAGE_KEY_WALLET_DATA = "tw:localWalletData"; + +export const localWalletMetadata: WalletMetadata = { + id: "local", + iconUrl: + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiByeD0iMTIiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl8xXzY0KSIvPgo8ZyBjbGlwLXBhdGg9InVybCgjY2xpcDBfMV82NCkiPgo8cGF0aCBkPSJNNTguNzUgMTkuMTY2N0gyMS4yNUMxOC45NTgzIDE5LjE2NjcgMTcuMDgzMyAyMS4wNDE3IDE3LjA4MzMgMjMuMzMzNFY0OC4zMzM0QzE3LjA4MzMgNTAuNjI1IDE4Ljk1ODMgNTIuNSAyMS4yNSA1Mi41SDM1LjgzMzNMMzEuNjY2NyA1OC43NVY2MC44MzM0SDQ4LjMzMzNWNTguNzVMNDQuMTY2NyA1Mi41SDU4Ljc1QzYxLjA0MTcgNTIuNSA2Mi45MTY3IDUwLjYyNSA2Mi45MTY3IDQ4LjMzMzRWMjMuMzMzNEM2Mi45MTY3IDIxLjA0MTcgNjEuMDQxNyAxOS4xNjY3IDU4Ljc1IDE5LjE2NjdaTTU4Ljc1IDQ0LjE2NjdIMjEuMjVWMjMuMzMzNEg1OC43NVY0NC4xNjY3WiIgZmlsbD0id2hpdGUiLz4KPC9nPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzFfNjQiIHgxPSI0MCIgeTE9IjAiIHgyPSI0MCIgeTI9IjgwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDRTExQUIiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOTAwQkI1Ii8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDBfMV82NCI+CjxyZWN0IHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE1IDE1KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=", + name: "Local Wallet", +}; + +/** + * Allow users to connect to your app by generating a + * [Local Wallet](https://portal.thirdweb.com/glossary/local-wallet) directly in your application. + * + * A local wallet is a low-level wallet that allows you to create wallets within your application or project. + * It is a non-custodial solution that simplifies the onboarding process and improves the user experience for web3 apps in two ways: + * + * 1. It enables non-web3 native users to get started easily without having to create a wallet. + * 2. It hides transaction confirmations from users. + * + * After generating wallets for your users, you can offer multiple persistence and backup options. + * @param options - The options of type `LocalWalletCreationOptions`. + * Refer to [`LocalWalletCreationOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletCreationOptions) for more details. + * @example + * Creating a `LocalWallet` instance requires a `client` object of type `ThirdwebClient`. + * Refer to [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation to learn how to create a client. + * + * You can then use methods like `generate`, `import`, `load` to initialize the wallet + * and `save` to save the wallet data to storage. You can also use `export` to get the wallet data in various formats. + * + * ```ts + * import { LocalWallet } from "thirdweb/wallets"; + * + * const wallet = new LocalWallet({ + * client, + * }) + * + * ``` + * @returns A `LocalWallet` instance + */ +export function localWallet(options: LocalWalletCreationOptions) { + return new LocalWallet(options); +} + +/** + * Allow users to connect to your app by generating a + * [Local Wallet](https://portal.thirdweb.com/glossary/local-wallet) directly in your application. + * + * A local wallet is a low-level wallet that allows you to create wallets within your application or project. + * It is a non-custodial solution that simplifies the onboarding process and improves the user experience for web3 apps in two ways: + * + * 1. It enables non-web3 native users to get started easily without having to create a wallet. + * 2. It hides transaction confirmations from users. + * + * After generating wallets for your users, you can offer multiple persistence and backup options. + * + * + * ## Create a LocalWallet instance. + * + * Creating a `LocalWallet` instance requires a `client` object of type `ThirdwebClient`. + * Refer to [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation to learn how to create a client. + * + * ```ts + * import { LocalWallet } from "thirdweb/wallets"; + * + * const wallet = new LocalWallet({ + * client, + * }) + * + * ``` + * + * You can then use methods like `generate`, `import`, `load` to initialize the wallet + * and `save` to save the wallet data to storage. You can also use `export` to get the wallet data in various formats. + * + */ +export class LocalWallet implements Wallet { + metadata: Wallet["metadata"]; + + private options: LocalWalletCreationOptions; + private account?: Account; + private chain?: Chain; + + // when generating a random wallet - we generate a mnemonic and save both mnemonic and private key + // so that it can be exported in both mnemonic and private key format + private privateKey?: string; + private mnemonic?: string; + + /** + * Create `LocalWallet` instance + * @param options - The options of type `LocalWalletCreationOptions`. + * Refer to [`LocalWalletCreationOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletCreationOptions) for more details. + * @example + * ```ts + * const wallet = new LocalWallet({ + * client, + * }); + * ``` + */ + constructor(options: LocalWalletCreationOptions) { + this.options = options; + this.metadata = localWalletMetadata; + } + + /** + * Get the `Chain` object of the blockchain that the wallet is connected to. + * @returns The `Chain` object + * @example + * ```ts + * const chain = wallet.getChain(); + * ``` + */ + getChain(): Chain | undefined { + return this.chain; + } + + /** + * Get the connected `Account` + * @returns The connected `Account` object + * @example + * ```ts + * const account = wallet.getAccount(); + * ``` + */ + getAccount(): Account | undefined { + return this.account; + } + + /** + * Connect LocalWallet + * @param options - The `options` object of type `LocalWalletConnectionOptions`. + * Refer to [`LocalWalletConnectionOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletConnectionOptions) for more details. + * + * Before calling this method, wallet needs to be initialized using `generate`, `load` or `import` method. + * @example + * ```ts + * await wallet.generate(); + * const account = await wallet.connect(); + * ``` + * @returns The connected `Account` object + */ + async connect(options?: LocalWalletConnectionOptions) { + this.chain = options?.chain || ethereum; + if (!this.account) { + throw new Error("Wallet is not initialized"); + } + const params: SavedConnectParams = { + chain: options?.chain, + }; + saveConnectParamsToStorage(this.metadata.id, params); + return this.account; + } + + /** + * Auto connect is not supported for Local Wallet because initializing the wallet the wallet requires user input ( password for decryption ) and password is not stored in storage. + * @example + * ```ts + * await wallet.autoConnect(); // throws error + * ``` + */ + async autoConnect(): Promise { + throw new Error("Auto connect is not supported for Local Wallet"); + } + + /** + * Switch the wallet to a different blockchain by passing the `Chain` object of it. + * + * You can create a `Chain` object using the [`defineChain`](https://portal.thirdweb.com/references/typescript/v5/defineChain) function. + * At minimum, you need to pass the `id` of the blockchain. + * @param chain - The `Chain` object of the blockchain to switch to. + * @example + * ```ts + * import { defineChain } from "thirdweb"; + * const mumbai = defineChain({ + * id: 80001, + * }); + * await wallet.switchChain(mumbai) + * ``` + */ + async switchChain(chain: Chain) { + this.chain = chain; + } + + /** + * Load the saved wallet from storage if it exists or generate a random wallet + * + * Once the wallet is loaded or created, you should call `connect` method to connect to get the `Account` object. + * @example + * ```js + * await wallet.loadOrCreate({ + * strategy: "privateKey", + * encryption: { + * password: "your-password", + * } + * }); + * const account = await wallet.connect(); + * ``` + * @param options - Options object of type `LocalWalletLoadOrCreateOptions`. + * Refer to [`LocalWalletLoadOrCreateOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletLoadOrCreateOptions) for more details. + */ + async loadOrCreate(options: LocalWalletLoadOrCreateOptions) { + if (await this.getSavedData(options.storage)) { + await this.load(options); + } else { + await this.generate(); + } + } + + /** + * Generates a random `Account` by generating a random private key. + * + * Once the wallet is generated, you should call `connect` method to connect to get the `Account` object. + * + * If an account is already initialized, it throws an error to avoid overwriting the existing account. + * @example + * ```ts + * await wallet.generate(); + * const account = await wallet.connect(); + * ``` + */ + async generate() { + if (this.account) { + throw new Error("Account is already initialized"); + } + + // generate a random mnemonic and get first account (private key) + // we generate a mnemonic so that we can export the wallet in both mnemonic and private key format + const mnemonic = generateMnemonic(english); + const hdAccount = mnemonicToAccount(mnemonic); + const hdKey = hdAccount.getHdKey(); + + if (hdKey.privateKey) { + this.privateKey = toHex(hdKey.privateKey); + } + + this.mnemonic = mnemonic; + + this.account = viemToThirdwebAccount(hdAccount, this.options.client); + } + + /** + * Create local wallet from a private key, mnemonic or encrypted JSON. + * @example + * ```javascript + * const address = await localWallet.import({ + * privateKey: "...", + * encryption: false, + * }); + * ``` + * @param options - The `options` object must be of type `LocalWalletImportOptions` + * Refer to [`LocalWalletImportOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletImportOptions) for more details. + */ + async import(options: LocalWalletImportOptions): Promise { + if (this.account) { + throw new Error("wallet is already initialized"); + } + + if ("privateKey" in options) { + if (!options.encryption && !isValidPrivateKey(options.privateKey)) { + throw new Error("invalid private key"); + } + + const privateKey = await getDecryptionFunction(options.encryption)( + options.privateKey, + ); + + if ( + options.encryption && + (privateKey === "" || !isValidPrivateKey(privateKey)) + ) { + throw new Error("invalid password"); + } + + this.account = privateKeyAccount({ + client: this.options.client, + privateKey, + }); + + this.privateKey = privateKey; + } else if ("mnemonic" in options) { + const mnemonic = await getDecryptionFunction(options.encryption)( + options.mnemonic, + ); + + if (options.encryption && mnemonic === "") { + throw new Error("invalid password"); + } + + const hdAccount = mnemonicToAccount(mnemonic); + const hdKey = hdAccount.getHdKey(); + + this.account = viemToThirdwebAccount(hdAccount, this.options.client); + + this.mnemonic = mnemonic; + if (hdKey.privateKey) { + this.privateKey = toHex(hdKey.privateKey); + } + } else { + throw new Error("invalid import strategy"); + } + } + + /** + * Initialize the wallet from saved wallet data in storage + * @param options - The `options` object of type `LocalWalletLoadOptions`. + * Refer to [`LocalWalletLoadOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletLoadOptions) for more details. + * @example + * ```ts + * await wallet.load({ + * strategy: "privateKey", + * encryption: { + * password: "your-password", + * } + * }); + * ``` + */ + async load(options: LocalWalletLoadOptions): Promise { + if (this.account) { + throw new Error("wallet is already initialized"); + } + + const walletData = await this.getSavedData(); + + if (!walletData) { + throw new Error("No Saved wallet found in storage"); + } + + // strategy mismatch + if (walletData.type !== options.strategy) { + throw new Error( + `Saved wallet data is not ${options.strategy}, it is ${walletData.type}`, + ); + } + + // encryption mismatch + if (walletData.isEncrypted && !options.encryption) { + throw new Error( + "Saved wallet data is encrypted, but no password is provided", + ); + } + + if (!walletData.isEncrypted && options.encryption) { + throw new Error( + "Saved wallet data is not encrypted, but encryption config is provided", + ); + } + + if (options.strategy === "privateKey") { + await this.import({ + privateKey: walletData.data, + encryption: options.encryption, + }); + } else if (options.strategy === "mnemonic") { + await this.import({ + mnemonic: walletData.data, + encryption: options.encryption, + }); + } else { + throw new Error("invalid load strategy"); + } + } + + /** + * Save the wallet data to storage in various formats. + * Note: Saving wallet data in an encrypted format is highly recommended - especially when the storage is not secure. ( such as `window.localStorage` ) + * @example + * ```js + * wallet.save({ + * strategy: "privateKey", + * encryption: { + * password: "your-password", + * } + * }); + * ``` + * @param options - The `options` object must be of type `LocalWalletSaveOptions`. + * Refer to [`LocalWalletSaveOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletSaveOptions) for more details. + */ + async save(options: LocalWalletSaveOptions): Promise { + const account = this.account; + if (!account) { + throw new Error("Wallet is not initialized"); + } + + if (options.strategy === "privateKey") { + if (!this.privateKey) { + throw new Error( + "No private key found - Failed to save wallet data in privateKey format", + ); + } + + const privateKey = await getEncryptionFunction(options.encryption)( + this.privateKey, + ); + + await this.saveData( + { + address: account.address, + data: privateKey, + type: "privateKey", + isEncrypted: !!options.encryption, + }, + options.storage, + ); + } + + if (options.strategy === "mnemonic") { + if (!this.mnemonic) { + throw new Error( + "No mnemonic found - Failed to save wallet data in mnemonic format", + ); + } + + const mnemonic = await getEncryptionFunction(options.encryption)( + this.mnemonic, + ); + + await this.saveData( + { + address: account.address, + data: mnemonic, + type: "mnemonic", + isEncrypted: !!options.encryption, + }, + options.storage, + ); + } + } + + /** + * Check if the current initialized wallet's data is saved in storage. + * @returns `true` if initialized wallet's data is saved in storage + * @example + * ```ts + * const isSaved = await wallet.isSaved(); + * ``` + */ + async isSaved() { + if (!this.account) { + throw new Error("Wallet is not initialized"); + } + + try { + const data = await this.getSavedData(); + if (data?.address === this.account.address) { + return true; + } + return false; + } catch (e) { + return false; + } + } + + /** + * Delete the saved wallet from storage. + * This action is irreversible, use with caution. + * @param storage - storage interface of type [`AsyncStorage`](https://portal.thirdweb.com/references/typescript/v5/AsyncStorage) to delete the wallet data from. If not provided, it defaults to `window.localStorage`. + * @example + * ```ts + * await wallet.deleteSaved(); + * ``` + */ + async deleteSaved(storage?: AsyncStorage) { + const _storage = storage || walletStorage; + await _storage.removeItem(STORAGE_KEY_WALLET_DATA); + } + + /** + * Encrypts the wallet's private key or mnemonic (seed phrase) and returns the encrypted data. + * @example + * ```ts + * const data = await wallet.export({ + * strategy: "privateKey", + * encryption: { + * password: "your-password", + * } + * }); + * ``` + * @param options - The `options` object must be of type `LocalWalletExportOptions`. + * Refer to [`LocalWalletExportOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletExportOptions) for more details. + * @returns Promise that resolves to a `string` that contains encrypted wallet data + */ + async export(options: LocalWalletExportOptions): Promise { + const account = this.account; + if (!account) { + throw new Error("Wallet is not initialized"); + } + + if (options.strategy === "privateKey") { + if (!this.privateKey) { + throw new Error( + "Private key not found - Failed to export wallet data in privateKey format", + ); + } + return getEncryptionFunction(options.encryption)(this.privateKey); + } else if (options.strategy === "mnemonic") { + if (!this.mnemonic) { + throw new Error( + "Mnemonic not found - Failed to export wallet data in mnemonic format", + ); + } + + return getEncryptionFunction(options.encryption)(this.mnemonic); + } else { + throw new Error("Invalid export strategy"); + } + } + + /** + * Get the saved wallet data from storage + * @param storage - storage interface of type [`AsyncStorage`](https://portal.thirdweb.com/references/typescript/v5/AsyncStorage) to get the wallet data from. If not provided, it defaults to `window.localStorage`. + * @example + * ```javascript + * const savedData = await wallet.getSaved(); + * ``` + * @returns `Promise` which resolves to a `LocalWalletStorageData` object or `null` if no wallet data is found in storage. + * Refer to [`LocalWalletStorageData`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletStorageData) for more details. + */ + async getSavedData( + storage?: AsyncStorage, + ): Promise { + try { + const savedDataStr = await (storage || walletStorage).getItem( + STORAGE_KEY_WALLET_DATA, + ); + if (!savedDataStr) { + return null; + } + + const savedData = JSON.parse(savedDataStr); + if (!savedData) { + return null; + } + + return savedData as LocalWalletStorageData; + } catch (e) { + return null; + } + } + + /** + * Store the wallet data to storage + * @param data - The wallet data of type [`LocalWalletStorageData`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletStorageData) to save to storage. + * @param storage - storage interface of type [`AsyncStorage`](https://portal.thirdweb.com/references/typescript/v5/AsyncStorage) to save the wallet data to. If not provided, it defaults to `window.localStorage`. + * @example + * ```js + * await wallet.saveData(data); + * ``` + */ + private async saveData(data: LocalWalletStorageData, storage?: AsyncStorage) { + const _storage = storage || walletStorage; + await _storage.setItem(STORAGE_KEY_WALLET_DATA, JSON.stringify(data)); + } + + /** + * Disconnect the wallet + * @example + * ```ts + * await wallet.disconnect(); + * ``` + */ + async disconnect() { + this.account = undefined; + this.chain = undefined; + this.privateKey = undefined; + this.mnemonic = undefined; + } +} diff --git a/packages/thirdweb/src/wallets/local/types.ts b/packages/thirdweb/src/wallets/local/types.ts new file mode 100644 index 00000000000..f6ca97d702a --- /dev/null +++ b/packages/thirdweb/src/wallets/local/types.ts @@ -0,0 +1,94 @@ +import type { AsyncStorage } from "../storage/AsyncStorage.js"; + +// TODO: export all +// TODO: check encryption is supposed to be required or not + +/** + * Type of object that is saved in storage by `LocalWallet` when `save` method is called. + */ +export type LocalWalletStorageData = { + /** + * Address of the wallet + */ + address: string; + /** + * Type of the data saved in storage + */ + type: "mnemonic" | "privateKey"; + /** + * the wallet data - It can be a private key or a mnemonic string - encrypted or un-encrypted + */ + data: string; + /** + * Flag to indicate if the data is encrypted or not + */ + isEncrypted: boolean; +}; + +/** + * Options for `loadOrCreate` method of `LocalWallet`. + * + * It contains the following properties: + * + * ### strategy + * It can be either `"privateKey"` or `"mnemonic"` + * + * ### encryption (optional) + * The encryption object of type [`LocalWalletDecryptOptions`](https://portal.thirdweb.com/references/typescript/v5/LocalWalletDecryptOptions) to decrypt the wallet data. + * It is only required if the wallet data is encrypted. + * + * ### storage (optional) + * object of type [`AsyncStorage`](https://portal.thirdweb.com/references/typescript/v5/AsyncStorage) to get the wallet data from. + * + * If not provided, it defaults to `window.localStorage`. + */ +export type LocalWalletLoadOrCreateOptions = { + strategy: "privateKey" | "mnemonic"; + storage?: AsyncStorage; + encryption: LocalWalletDecryptOptions; +}; + +export type LocalWalletDecryptOptions = + | { + decrypt?: (message: string, password: string) => Promise; + password: string; + } + | false; + +export type LocalWalletSaveOptions = { + storage?: AsyncStorage; + encryption: LocalWalletEncryptOptions; + strategy: "privateKey" | "mnemonic"; +}; + +export type LocalWalletImportOptions = { + encryption: LocalWalletDecryptOptions; +} & ( + | { + privateKey: string; + } + | { + mnemonic: string; + } +); + +export type LocalWalletEncryptOptions = + | { + encrypt?: (message: string, password: string) => Promise; + password: string; + } + | false; + +export type LocalWalletExportOptions = { + encryption: LocalWalletEncryptOptions; + strategy: "privateKey" | "mnemonic"; +}; + +/** + * The options for `load` method of `LocalWallet`. + * The object contains a `strategy` and `encryption` property. The `encryption` property is only required if the wallet data is encrypted. + */ +export type LocalWalletLoadOptions = { + strategy: "privateKey" | "mnemonic"; + encryption: LocalWalletDecryptOptions; +}; diff --git a/packages/thirdweb/src/wallets/local/utils.ts b/packages/thirdweb/src/wallets/local/utils.ts new file mode 100644 index 00000000000..94a80ed5898 --- /dev/null +++ b/packages/thirdweb/src/wallets/local/utils.ts @@ -0,0 +1,57 @@ +import type { + LocalWalletDecryptOptions, + LocalWalletEncryptOptions, +} from "./types.js"; + +/** + * @internal + */ +export function isValidPrivateKey(value: string) { + return !!value.match(/^(0x)?[0-9a-f]{64}$/i); +} + +/** + * if encryption object is provided + * - use encryption.decrypt function if given, else return the default decrypt function + * if encryption object is not provided + * - return a noop function + * @internal + */ +export function getDecryptionFunction( + encryption: LocalWalletDecryptOptions | undefined, +) { + return async (msg: string) => { + if (!encryption) { + return msg; + } + + if (encryption.decrypt) { + return encryption.decrypt(msg, encryption.password); + } + + const { aesDecryptCompat } = await import("../../crypto/aes/decrypt.js"); + return aesDecryptCompat(msg, encryption.password); + }; +} + +/** + * if encryption object is provided - use encryption.encrypt function if given, else use default encrypt function + * if no encryption object is provided - do not encrypt + * @internal + */ +export function getEncryptionFunction( + encryption: LocalWalletEncryptOptions | undefined, +) { + return async (msg: string) => { + if (!encryption) { + return msg; + } + + if (encryption.encrypt) { + return encryption.encrypt(msg, encryption.password); + } + + const { aesEncrypt } = await import("../../crypto/aes/encrypt.js"); + return aesEncrypt(msg, encryption.password); + }; +} diff --git a/packages/thirdweb/src/wallets/manager/index.ts b/packages/thirdweb/src/wallets/manager/index.ts index 809fb41dfc1..6a03231556c 100644 --- a/packages/thirdweb/src/wallets/manager/index.ts +++ b/packages/thirdweb/src/wallets/manager/index.ts @@ -5,7 +5,7 @@ import { effect } from "../../reactive/effect.js"; import { createStore } from "../../reactive/store.js"; import type { Account, Wallet } from "../interfaces/wallet.js"; import { normalizeChainId } from "../utils/normalizeChainId.js"; -import { walletStorage } from "./storage.js"; +import { walletStorage } from "../storage/walletStorage.js"; type WalletIdToConnectedWalletMap = Map; export type ConnectionStatus = @@ -149,7 +149,7 @@ export function createConnectionManager() { .map((acc) => acc?.metadata.id) .filter((c) => !!c) as string[]; - walletStorage.set(CONNECTED_WALLET_IDS, JSON.stringify(ids)); + walletStorage.setItem(CONNECTED_WALLET_IDS, JSON.stringify(ids)); }, [connectedWallets], false, @@ -160,9 +160,9 @@ export function createConnectionManager() { () => { const value = activeWallet.getValue()?.metadata.id; if (value) { - walletStorage.set(ACTIVE_WALLET_ID, value); + walletStorage.setItem(ACTIVE_WALLET_ID, value); } else { - walletStorage.remove(ACTIVE_WALLET_ID); + walletStorage.removeItem(ACTIVE_WALLET_ID); } }, [activeWallet], @@ -206,7 +206,7 @@ export function createConnectionManager() { */ export async function getStoredConnectedWalletIds(): Promise { try { - const value = await walletStorage.get(CONNECTED_WALLET_IDS); + const value = await walletStorage.getItem(CONNECTED_WALLET_IDS); if (value) { return JSON.parse(value) as string[]; } @@ -221,7 +221,7 @@ export async function getStoredConnectedWalletIds(): Promise { */ export async function getStoredActiveWalletId(): Promise { try { - const value = await walletStorage.get(ACTIVE_WALLET_ID); + const value = await walletStorage.getItem(ACTIVE_WALLET_ID); if (value) { return value; } diff --git a/packages/thirdweb/src/wallets/private-key.ts b/packages/thirdweb/src/wallets/private-key.ts index 3c8f29498b7..372bf83ce08 100644 --- a/packages/thirdweb/src/wallets/private-key.ts +++ b/packages/thirdweb/src/wallets/private-key.ts @@ -1,4 +1,9 @@ -import type { Hex, TransactionSerializable } from "viem"; +import type { + HDAccount, + PrivateKeyAccount, + Hex, + TransactionSerializable, +} from "viem"; import { privateKeyToAccount } from "viem/accounts"; import type { ThirdwebClient } from "../client/client.js"; import { defineChain } from "../chains/utils.js"; @@ -57,6 +62,40 @@ export function privateKeyAccount(options: PrivateKeyAccountOptions): Account { options.privateKey = "0x" + options.privateKey; } const viemAccount = privateKeyToAccount(options.privateKey as Hex); + return viemToThirdwebAccount(viemAccount, options.client); +} + +export type MnemonicAccountOptions = { + /** + * A client is the entry point to the thirdweb SDK. + * It is required for all other actions. + * You can create a client using the `createThirdwebClient` function. Refer to the [Creating a Client](https://portal.thirdweb.com/typescript/v5/client) documentation for more information. + * + * You must provide a `clientId` or `secretKey` in order to initialize a client. Pass `clientId` if you want for client-side usage and `secretKey` for server-side usage. + * + * ```tsx + * import { createThirdwebClient } from "thirdweb"; + * + * const client = createThirdwebClient({ + * clientId: "", + * }) + * ``` + */ + client: ThirdwebClient; + + /** + * The mnemonic to use for the account. + */ + mnemonic: string; +}; + +/** + * @internal + */ +export function viemToThirdwebAccount( + viemAccount: HDAccount | PrivateKeyAccount, + client: ThirdwebClient, +) { const account: Account = { address: viemAccount.address, sendTransaction: async ( @@ -65,7 +104,7 @@ export function privateKeyAccount(options: PrivateKeyAccountOptions): Account { tx: TransactionSerializable & { chainId: number }, ) => { const rpcRequest = getRpcClient({ - client: options.client, + client: client, chain: defineChain(tx.chainId), }); const signedTx = await viemAccount.signTransaction(tx); diff --git a/packages/thirdweb/src/wallets/smart/index.ts b/packages/thirdweb/src/wallets/smart/index.ts index 6db0f2187b8..bfa91c6079a 100644 --- a/packages/thirdweb/src/wallets/smart/index.ts +++ b/packages/thirdweb/src/wallets/smart/index.ts @@ -19,7 +19,7 @@ import { import { saveConnectParamsToStorage, type WithPersonalWalletConnectionOptions, -} from "../manager/storage.js"; +} from "../storage/walletStorage.js"; import type { Chain } from "../../chains/types.js"; import type { PreparedTransaction } from "../../transaction/prepare-transaction.js"; diff --git a/packages/thirdweb/src/wallets/storage/AsyncStorage.ts b/packages/thirdweb/src/wallets/storage/AsyncStorage.ts new file mode 100644 index 00000000000..370cda215b4 --- /dev/null +++ b/packages/thirdweb/src/wallets/storage/AsyncStorage.ts @@ -0,0 +1,5 @@ +export interface AsyncStorage { + getItem(key: string): Promise; + setItem(key: string, value: string): Promise; + removeItem(key: string): Promise; +} diff --git a/packages/thirdweb/src/wallets/manager/storage.ts b/packages/thirdweb/src/wallets/storage/walletStorage.ts similarity index 78% rename from packages/thirdweb/src/wallets/manager/storage.ts rename to packages/thirdweb/src/wallets/storage/walletStorage.ts index d1e5b743ec1..04410c24ef8 100644 --- a/packages/thirdweb/src/wallets/manager/storage.ts +++ b/packages/thirdweb/src/wallets/storage/walletStorage.ts @@ -1,17 +1,13 @@ -type WalletStorage = { - get: (key: string) => Promise; - set: (key: string, value: string) => Promise; - remove: (key: string) => Promise; -}; +import type { AsyncStorage } from "./AsyncStorage.js"; -export const walletStorage: WalletStorage = { - async get(key: string) { +export const walletStorage: AsyncStorage = { + async getItem(key: string) { return localStorage.getItem(key); }, - async set(key: string, value: string) { + async setItem(key: string, value: string) { localStorage.setItem(key, value); }, - async remove(key: string) { + async removeItem(key: string) { localStorage.removeItem(key); }, }; @@ -37,7 +33,7 @@ export async function saveConnectParamsToStorage( throw new Error("given params are not stringifiable"); } - const currentValueStr = await walletStorage.get(CONNECT_PARAMS_MAP_KEY); + const currentValueStr = await walletStorage.getItem(CONNECT_PARAMS_MAP_KEY); let value: Record; @@ -55,7 +51,7 @@ export async function saveConnectParamsToStorage( }; } - walletStorage.set(CONNECT_PARAMS_MAP_KEY, JSON.stringify(value)); + walletStorage.setItem(CONNECT_PARAMS_MAP_KEY, JSON.stringify(value)); } /** @@ -68,7 +64,7 @@ export async function saveConnectParamsToStorage( * @internal */ export async function deleteConnectParamsFromStorage(walletId: string) { - const currentValueStr = await walletStorage.get(CONNECT_PARAMS_MAP_KEY); + const currentValueStr = await walletStorage.getItem(CONNECT_PARAMS_MAP_KEY); let value: Record; @@ -80,7 +76,7 @@ export async function deleteConnectParamsFromStorage(walletId: string) { } delete value[walletId]; - walletStorage.set(CONNECT_PARAMS_MAP_KEY, JSON.stringify(value)); + walletStorage.setItem(CONNECT_PARAMS_MAP_KEY, JSON.stringify(value)); } } @@ -91,7 +87,7 @@ export async function deleteConnectParamsFromStorage(walletId: string) { export async function getSavedConnectParamsFromStorage( walletId: string, ): Promise { - const valueStr = await walletStorage.get(CONNECT_PARAMS_MAP_KEY); + const valueStr = await walletStorage.getItem(CONNECT_PARAMS_MAP_KEY); if (!valueStr) { return null; diff --git a/packages/thirdweb/src/wallets/wallet-connect/index.ts b/packages/thirdweb/src/wallets/wallet-connect/index.ts index 8ffa7275737..62948658278 100644 --- a/packages/thirdweb/src/wallets/wallet-connect/index.ts +++ b/packages/thirdweb/src/wallets/wallet-connect/index.ts @@ -14,7 +14,7 @@ import { getSavedConnectParamsFromStorage, saveConnectParamsToStorage, walletStorage, -} from "../manager/storage.js"; +} from "../storage/walletStorage.js"; import type { Account, @@ -568,7 +568,7 @@ export class WalletConnect implements Wallet { * @internal */ private async getRequestedChainsIds(): Promise { - const data = await walletStorage.get(storageKeys.requestedChains); + const data = await walletStorage.getItem(storageKeys.requestedChains); return data ? JSON.parse(data) : []; } @@ -626,7 +626,7 @@ export class WalletConnect implements Wallet { * @internal */ private setRequestedChainsIds(chains: number[]) { - walletStorage.set(storageKeys.requestedChains, JSON.stringify(chains)); + walletStorage.setItem(storageKeys.requestedChains, JSON.stringify(chains)); } /** @@ -636,7 +636,7 @@ export class WalletConnect implements Wallet { */ private onDisconnect = () => { this.setRequestedChainsIds([]); - walletStorage.remove(storageKeys.lastUsedChainId); + walletStorage.removeItem(storageKeys.lastUsedChainId); const provider = this.provider; if (provider) { @@ -657,6 +657,6 @@ export class WalletConnect implements Wallet { private onChainChanged = (newChainId: number | string) => { const chainId = normalizeChainId(newChainId); this.chain = defineChain(chainId); - walletStorage.set(storageKeys.lastUsedChainId, String(chainId)); + walletStorage.setItem(storageKeys.lastUsedChainId, String(chainId)); }; }