diff --git a/package-lock.json b/package-lock.json index a3b64f8d..fd5ed68b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,8 @@ "@toruslabs/metadata-helpers": "^5.x", "@toruslabs/openlogin-session-manager": "^3.0.0", "@toruslabs/torus.js": "^11.0.6", - "@toruslabs/tss-client": "^1.7.1", - "@toruslabs/tss-lib": "^1.7.1", + "@toruslabs/tss-client": "^2.0.0", + "@toruslabs/tss-lib": "^2.0.0", "@web3auth-mpc/ethereum-provider": "^2.3.0", "@web3auth/base": "^7.0.1", "@web3auth/base-provider": "^7.0.1", @@ -38,6 +38,7 @@ "@toruslabs/config": "^2.0.2", "@toruslabs/eslint-config-typescript": "^3.0.1", "@toruslabs/torus-scripts": "^5.0.5", + "@toruslabs/tss-lib-node": "^1.1.3", "@types/chai": "^4.3.6", "@types/elliptic": "^6.4.14", "@types/node": "^20.6.3", @@ -61,7 +62,7 @@ }, "peerDependencies": { "@babel/runtime": "^7.x", - "@toruslabs/metadata-helpers": "^4.x" + "@toruslabs/metadata-helpers": "^5.x" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4072,7 +4073,8 @@ }, "node_modules/@toruslabs/metadata-helpers": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@toruslabs/metadata-helpers/-/metadata-helpers-5.0.0.tgz", + "integrity": "sha512-ZUFfOHJVJC53c8wJYHjdF3bIgN2ZvfqehbTZ/zJ7oVFfrrd6O66V3gQ1i1zxBjH3yhOvZKQwc0DaMmh3G0NUXQ==", "dependencies": { "@toruslabs/eccrypto": "^4.0.0", "@toruslabs/http-helpers": "^5.0.0", @@ -4133,7 +4135,8 @@ }, "node_modules/@toruslabs/metadata-helpers/node_modules/@toruslabs/http-helpers": { "version": "5.0.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@toruslabs/http-helpers/-/http-helpers-5.0.0.tgz", + "integrity": "sha512-GmezWz9JeF6YyhjLSm+9XDF4YaeICEckY0Jbo43i86SjhfJYgRWqEi63VSiNsaqc/z810Q0FQvEk1TnBRX2tgA==", "dependencies": { "lodash.merge": "^4.6.2", "loglevel": "^1.8.1" @@ -4464,19 +4467,86 @@ } }, "node_modules/@toruslabs/tss-client": { - "version": "1.7.1", - "license": "ISC", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@toruslabs/tss-client/-/tss-client-2.0.0.tgz", + "integrity": "sha512-I15AdbkNoV08cJyctLvYuiJKRuQFBlaVq9UlMdgJd5r/0i5NtpyCLsWI6cY5yysppGLOOccyy8fNHB8XUDfQng==", "dependencies": { "@toruslabs/eccrypto": "^4.0.0", - "@toruslabs/tss-lib": "^1.7.1", + "@toruslabs/tss-lib": "^2.0.0", "bn.js": "^5.2.1", "elliptic": "^6.5.4", - "keccak256": "^1.0.6", + "ethereum-cryptography": "^2.1.2", "socket.io-client": "^4.7.2" } }, + "node_modules/@toruslabs/tss-client/node_modules/@noble/curves": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", + "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", + "dependencies": { + "@noble/hashes": "1.3.1" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@toruslabs/tss-client/node_modules/@noble/hashes": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", + "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@toruslabs/tss-client/node_modules/@scure/bip32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", + "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", + "dependencies": { + "@noble/curves": "~1.1.0", + "@noble/hashes": "~1.3.1", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@toruslabs/tss-client/node_modules/@scure/bip39": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", + "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", + "dependencies": { + "@noble/hashes": "~1.3.0", + "@scure/base": "~1.1.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@toruslabs/tss-client/node_modules/ethereum-cryptography": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz", + "integrity": "sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==", + "dependencies": { + "@noble/curves": "1.1.0", + "@noble/hashes": "1.3.1", + "@scure/bip32": "1.3.1", + "@scure/bip39": "1.2.1" + } + }, "node_modules/@toruslabs/tss-lib": { - "version": "1.7.1" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@toruslabs/tss-lib/-/tss-lib-2.0.0.tgz", + "integrity": "sha512-A6peWwpYuqeHLcTzoJh4X31jSxFlxrWEdcbQjU09H7GiO/3exTK2KyQ/r0yCcdONvCwSfpGy4uMUZIcfz9Valg==" + }, + "node_modules/@toruslabs/tss-lib-node": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@toruslabs/tss-lib-node/-/tss-lib-node-1.1.3.tgz", + "integrity": "sha512-yPJRHS0ykbWCthnUNe8pil3PNWFjqwKOw5vwW/UkHY8zEw4lpSQKnfGY2BSYW83ZEHak9b6E4y0dHQKKCkOpTA==", + "dev": true }, "node_modules/@tsconfig/node10": { "version": "1.0.9", @@ -10674,15 +10744,6 @@ "node": ">=10.0.0" } }, - "node_modules/keccak256": { - "version": "1.0.6", - "license": "MIT", - "dependencies": { - "bn.js": "^5.2.0", - "buffer": "^6.0.3", - "keccak": "^3.0.2" - } - }, "node_modules/keyv": { "version": "4.5.3", "dev": true, diff --git a/package.json b/package.json index 82ca0c8a..38b3eb25 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "homepage": "https://github.com/Web3Auth/mpc-core-kit/tree/master#readme", "license": "ISC", "scripts": { - "test": "echo no tests available", + "test": "node --test -r esbuild-register tests/*.spec.ts ", "dev": "torus-scripts start", "build": "torus-scripts build", "release": "torus-scripts release", @@ -32,7 +32,7 @@ ], "peerDependencies": { "@babel/runtime": "^7.x", - "@toruslabs/metadata-helpers": "^4.x" + "@toruslabs/metadata-helpers": "^5.x" }, "dependencies": { "@tkey-mpc/chrome-storage": "^9.0.2", @@ -50,8 +50,8 @@ "@toruslabs/metadata-helpers": "^5.x", "@toruslabs/openlogin-session-manager": "^3.0.0", "@toruslabs/torus.js": "^11.0.6", - "@toruslabs/tss-client": "^1.7.1", - "@toruslabs/tss-lib": "^1.7.1", + "@toruslabs/tss-client": "^2.0.0", + "@toruslabs/tss-lib": "^2.0.0", "@web3auth-mpc/ethereum-provider": "^2.3.0", "@web3auth/base": "^7.0.1", "@web3auth/base-provider": "^7.0.1", @@ -64,6 +64,7 @@ "@toruslabs/config": "^2.0.2", "@toruslabs/eslint-config-typescript": "^3.0.1", "@toruslabs/torus-scripts": "^5.0.5", + "@toruslabs/tss-lib-node": "^1.1.3", "@types/chai": "^4.3.6", "@types/elliptic": "^6.4.14", "@types/node": "^20.6.3", diff --git a/src/helper/browserStorage.ts b/src/helper/browserStorage.ts index 9e130f38..57f7862d 100644 --- a/src/helper/browserStorage.ts +++ b/src/helper/browserStorage.ts @@ -1,9 +1,29 @@ import BN from "bn.js"; import { FIELD_ELEMENT_HEX_LEN } from "../constants"; -import { ICoreKit, IStorage, TkeyLocalStoreData } from "../interfaces"; +import { ICoreKit, IStorage, SupportedStorageType, TkeyLocalStoreData } from "../interfaces"; import { storageAvailable } from "../utils"; +export class MemoryStorage implements IStorage { + private _store: Record = {}; + + getItem(key: string): string | null { + return this._store[key] || null; + } + + setItem(key: string, value: string): void { + this._store[key] = value; + } + + removeItem(key: string): void { + delete this._store[key]; + } + + clear(): void { + this._store = {}; + } +} + export class BrowserStorage { // eslint-disable-next-line no-use-before-define private static instance: BrowserStorage; @@ -24,14 +44,17 @@ export class BrowserStorage { } } - static getInstance(key: string, storageKey: "session" | "local" = "local"): BrowserStorage { + static getInstance(key: string, storageKey: SupportedStorageType = "local"): BrowserStorage { if (!this.instance) { - let storage: Storage | undefined; + let storage: IStorage | undefined; if (storageKey === "local" && storageAvailable("localStorage")) { storage = localStorage; - } - if (storageKey === "session" && storageAvailable("sessionStorage")) { + } else if (storageKey === "session" && storageAvailable("sessionStorage")) { storage = sessionStorage; + } else if (storageKey === "memory") { + storage = new MemoryStorage(); + } else if (typeof storageKey === "object") { + storage = storageKey; } if (!storage) { diff --git a/src/interfaces.ts b/src/interfaces.ts index 7a8eafb7..2a77d3af 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -13,11 +13,16 @@ import { CustomChainConfig, SafeEventEmitterProvider } from "@web3auth/base"; import BN from "bn.js"; import { FactorKeyTypeShareDescription, TssShareType, USER_PATH, WEB3AUTH_NETWORK } from "./constants"; + +export type CoreKitMode = UX_MODE_TYPE | "nodejs" | "react-native"; + export interface IStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; } +export type SupportedStorageType = "local" | "session" | "memory" | IStorage; + export interface InitParams { /** * @defaultValue `true` @@ -286,7 +291,7 @@ export interface Web3AuthOptions { * * @defaultValue `'local'` */ - storageKey?: "session" | "local"; + storageKey?: SupportedStorageType; /** * @defaultValue 86400 @@ -296,7 +301,7 @@ export interface Web3AuthOptions { /** * @defaultValue `'POPUP'` */ - uxMode?: UX_MODE_TYPE; + uxMode?: CoreKitMode; /** * @defaultValue `false` @@ -348,6 +353,14 @@ export interface Web3AuthOptions { */ disableHashedFactorKey?: boolean; + /** + * @defaultValue `null` + * Overwrite tss-lib for nodejs. + * Required for nodejs mode. + * Do not use this option for non nodejs mode. + */ + tssLib?: unknown; + /** * @defaultValue `Web3AuthOptions.web3AuthClientId` * Overwrites the default value ( clientId ) used as nonce for hashing the hash factor key. diff --git a/src/mpcCoreKit.ts b/src/mpcCoreKit.ts index 79e10ca5..90956ee0 100644 --- a/src/mpcCoreKit.ts +++ b/src/mpcCoreKit.ts @@ -5,12 +5,14 @@ import { TorusServiceProvider } from "@tkey-mpc/service-provider-torus"; import { ShareSerializationModule } from "@tkey-mpc/share-serialization"; import { TorusStorageLayer } from "@tkey-mpc/storage-layer-torus"; import { AGGREGATE_VERIFIER, TORUS_METHOD, TorusAggregateLoginResponse, TorusLoginResponse, UX_MODE } from "@toruslabs/customauth"; +import type { UX_MODE_TYPE } from "@toruslabs/customauth/dist/types/utils/enums"; import { generatePrivate } from "@toruslabs/eccrypto"; import { NodeDetailManager } from "@toruslabs/fetch-node-details"; import { keccak256 } from "@toruslabs/metadata-helpers"; import { OpenloginSessionManager } from "@toruslabs/openlogin-session-manager"; import TorusUtils, { TorusKey } from "@toruslabs/torus.js"; -import { Client, utils as tssUtils } from "@toruslabs/tss-client"; +import { Client, getDKLSCoeff, setupSockets } from "@toruslabs/tss-client"; +import type * as TssLib from "@toruslabs/tss-lib"; import { CHAIN_NAMESPACES, log, SafeEventEmitterProvider } from "@web3auth/base"; import { EthereumSigningProvider } from "@web3auth-mpc/ethereum-provider"; import BN from "bn.js"; @@ -33,6 +35,7 @@ import { BrowserStorage, storeWebBrowserFactor } from "./helper/browserStorage"; import { AggregateVerifierLoginParams, COREKIT_STATUS, + CoreKitMode, CreateFactorParams, EnableMFAParams, ICoreKit, @@ -93,6 +96,16 @@ export class Web3AuthMPCCoreKit implements ICoreKit { throw new Error("You must specify a web3auth clientId."); } + const isNodejsOrRN = this.isNodejsOrRN(options.uxMode); + + if (isNodejsOrRN && ["local", "session"].includes(options.storageKey.toString())) { + throw new Error(`${options.uxMode} mode do not storage of type : ${options.storageKey}`); + } + + if (isNodejsOrRN && !options.tssLib) { + throw new Error(`${options.uxMode} mode requires tssLib`); + } + if (options.enableLogging) { log.enableAll(); this.enableLogging = true; @@ -103,7 +116,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (!options.sessionTime) options.sessionTime = 86400; if (!options.uxMode) options.uxMode = UX_MODE.REDIRECT; if (!options.redirectPathName) options.redirectPathName = "redirect"; - if (!options.baseUrl) options.baseUrl = `${window.location.origin}/serviceworker`; + if (!options.baseUrl) options.baseUrl = isNodejsOrRN ? "https://localhost" : `${window?.location.origin}/serviceworker`; if (!options.disableHashedFactorKey) options.disableHashedFactorKey = false; if (!options.hashedFactorNonce) options.hashedFactorNonce = options.web3AuthClientId; @@ -144,6 +157,9 @@ export class Web3AuthMPCCoreKit implements ICoreKit { throw new Error("Not implemented"); } + // this return oauthkey which is used by demo to reset account. + // this is not the same metadataKey from tkey. + // will be fixed in next major release get metadataKey(): string | null { return this.state?.oAuthKey ? this.state.oAuthKey : null; } @@ -185,6 +201,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } public async init(params: InitParams = { handleRedirectResult: true }): Promise { + this.resetState(); if (params.rehydrate === undefined) params.rehydrate = true; const nodeDetails = await this.nodeDetailManager.getNodeDetails({ verifier: "test-verifier", verifierId: "test@example.com" }); @@ -197,8 +214,8 @@ export class Web3AuthMPCCoreKit implements ICoreKit { useTSS: true, customAuthArgs: { web3AuthClientId: this.options.web3AuthClientId, - baseUrl: this.options.baseUrl ? this.options.baseUrl : `${window.location.origin}/serviceworker`, - uxMode: this.options.uxMode, + baseUrl: this.options.baseUrl, + uxMode: this.isNodejsOrRN(this.options.uxMode) ? UX_MODE.REDIRECT : (this.options.uxMode as UX_MODE_TYPE), network: this.options.web3AuthNetwork, redirectPathName: this.options.redirectPathName, locationReplaceOnRedirect: true, @@ -226,13 +243,17 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (this.isRedirectMode) { await (this.tKey.serviceProvider as TorusServiceProvider).init({ skipSw: true, skipPrefetch: true }); - } else { + } else if (this.options.uxMode === UX_MODE.POPUP) { await (this.tKey.serviceProvider as TorusServiceProvider).init({}); } this.ready = true; // try handle redirect flow if enabled and return(redirect) from oauth login - if (params.handleRedirectResult && (window?.location.hash.includes("#state") || window?.location.hash.includes("#access_token"))) { + if ( + params.handleRedirectResult && + this.options.uxMode === UX_MODE.REDIRECT && + (window?.location.hash.includes("#state") || window?.location.hash.includes("#access_token")) + ) { await this.handleRedirectResult(); // if not redirect flow try to rehydrate session if available @@ -245,6 +266,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { public async loginWithOauth(params: OauthLoginParams): Promise { this.checkReady(); + if (this.isNodejsOrRN(this.options.uxMode)) throw new Error(`Oauth login is NOT supported in ${this.options.uxMode}`); const tkeyServiceProvider = this.tKey.serviceProvider as TorusServiceProvider; try { @@ -433,13 +455,24 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } try { - const browserInfo = bowser.parse(navigator.userAgent); - const browserName = `${browserInfo.browser.name}`; - const browserData = { - browserName, - browserVersion: browserInfo.browser.version, - deviceName: browserInfo.os.name, - }; + let browserData; + + if (this.isNodejsOrRN(this.options.uxMode)) { + browserData = { + browserName: "Node Env", + browserVersion: "", + deviceName: "nodejs", + }; + } else { + // try { + const browserInfo = bowser.parse(navigator.userAgent); + const browserName = `${browserInfo.browser.name}`; + browserData = { + browserName, + browserVersion: browserInfo.browser.version, + deviceName: browserInfo.os.name, + }; + } const deviceFactorKey = new BN(await this.createFactor({ shareType: TssShareType.DEVICE, additionalMetadata: browserData }), "hex"); storeWebBrowserFactor(deviceFactorKey, this); await this.inputFactorKey(new BN(deviceFactorKey, "hex")); @@ -504,6 +537,113 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } } + // function for setting up provider + public getPublic: () => Promise = async () => { + let { tssPubKey } = this.state; + if (tssPubKey.length === FIELD_ELEMENT_HEX_LEN + 1) { + tssPubKey = tssPubKey.subarray(1); + } + return tssPubKey; + }; + + public sign = async (msgHash: Buffer): Promise<{ v: number; r: Buffer; s: Buffer }> => { + // if (this.state.remoteClient) { + // return this.remoteSign(msgHash); + // } + return this.localSign(msgHash); + }; + + public localSign = async (msgHash: Buffer) => { + // PreSetup + let { tssShareIndex, tssPubKey } = this.state; + const { torusNodeTSSEndpoints } = await this.nodeDetailManager.getNodeDetails({ + verifier: "test-verifier", + verifierId: "test@example.com", + }); + + if (!this.state.factorKey) throw new Error("factorKey not present"); + const { tssShare } = await this.tKey.getTSSShare(this.state.factorKey); + const tssNonce = this.getTssNonce(); + + if (!tssPubKey || !torusNodeTSSEndpoints) { + throw new Error("tssPubKey or torusNodeTSSEndpoints not available"); + } + + if (tssPubKey.length === FIELD_ELEMENT_HEX_LEN + 1) { + tssPubKey = tssPubKey.subarray(1); + } + + const vid = `${this.verifier}${DELIMITERS.Delimiter1}${this.verifierId}`; + const sessionId = `${vid}${DELIMITERS.Delimiter2}default${DELIMITERS.Delimiter3}${tssNonce}${DELIMITERS.Delimiter4}`; + + const parties = 4; + const clientIndex = parties - 1; + // 1. setup + // generate endpoints for servers + const { nodeIndexes } = await (this.tKey.serviceProvider as TorusServiceProvider).getTSSPubKey( + this.tKey.tssTag, + this.tKey.metadata.tssNonces[this.tKey.tssTag] + ); + const { + endpoints, + tssWSEndpoints, + partyIndexes, + nodeIndexesReturned: participatingServerDKGIndexes, + } = generateTSSEndpoints(torusNodeTSSEndpoints, parties, clientIndex, nodeIndexes); + const randomSessionNonce = keccak256(Buffer.from(generatePrivate().toString("hex") + Date.now(), "utf8")).toString("hex"); + const tssImportUrl = `${torusNodeTSSEndpoints[0]}/v1/clientWasm`; + // session is needed for authentication to the web3auth infrastructure holding the factor 1 + const currentSession = `${sessionId}${randomSessionNonce}`; + + let tss: typeof TssLib; + if (this.isNodejsOrRN(this.options.uxMode)) { + tss = this.options.tssLib as typeof TssLib; + } else { + tss = await import("@toruslabs/tss-lib"); + await tss.default(tssImportUrl); + } + // setup mock shares, sockets and tss wasm files. + const [sockets] = await Promise.all([setupSockets(tssWSEndpoints, randomSessionNonce)]); + + const dklsCoeff = getDKLSCoeff(true, participatingServerDKGIndexes, tssShareIndex as number); + const denormalisedShare = dklsCoeff.mul(tssShare).umod(CURVE.curve.n); + const share = scalarBNToBufferSEC1(denormalisedShare).toString("base64"); + + if (!currentSession) { + throw new Error(`sessionAuth does not exist ${currentSession}`); + } + + const signatures = await this.getSigningSignatures(msgHash.toString("hex")); + if (!signatures) { + throw new Error(`Signature does not exist ${signatures}`); + } + + const client = new Client(currentSession, clientIndex, partyIndexes, endpoints, sockets, share, tssPubKey.toString("base64"), true, tssImportUrl); + const serverCoeffs: Record = {}; + for (let i = 0; i < participatingServerDKGIndexes.length; i++) { + const serverIndex = participatingServerDKGIndexes[i]; + serverCoeffs[serverIndex] = getDKLSCoeff(false, participatingServerDKGIndexes, tssShareIndex as number, serverIndex).toString("hex"); + } + + client.precompute(tss, { signatures, server_coeffs: serverCoeffs }); + + await client.ready().catch((err) => { + client.cleanup(tss, { signatures, server_coeffs: serverCoeffs }); + throw err; + }); + + let { r, s, recoveryParam } = await client.sign(tss, Buffer.from(msgHash).toString("base64"), true, "", "keccak256", { + signatures, + }); + + if (recoveryParam < 27) { + recoveryParam += 27; + } + // skip await cleanup + client.cleanup(tss, { signatures, server_coeffs: serverCoeffs }); + return { v: recoveryParam, r: scalarBNToBufferSEC1(r), s: scalarBNToBufferSEC1(s) }; + }; + async deleteFactor(factorPub: TkeyPoint, factorKey?: BNString): Promise { if (!this.state.factorKey) throw new Error("Factor key not present"); if (!this.tKey.metadata.factorPubs) throw new Error("Factor pubs not present"); @@ -539,13 +679,13 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } public async logout(): Promise { - if (!this.sessionManager.sessionId) { - throw new Error("User is not logged in."); + if (this.sessionManager.sessionId) { + // throw new Error("User is not logged in."); + await this.sessionManager.invalidateSession(); } - await this.sessionManager.invalidateSession(); this.currentStorage.set("sessionId", ""); this.resetState(); - await this.init(); + await this.init({ handleRedirectResult: false }); } public getUserInfo(): UserInfo { @@ -888,94 +1028,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { private async setupProvider(): Promise { const signingProvider = new EthereumSigningProvider({ config: { chainConfig: this.options.chainConfig } }); - let { tssShareIndex, tssPubKey } = this.state; - const { torusNodeTSSEndpoints } = await this.nodeDetailManager.getNodeDetails({ - verifier: "test-verifier", - verifierId: "test@example.com", - }); - - if (!this.state.factorKey) throw new Error("factorKey not present"); - const { tssShare } = await this.tKey.getTSSShare(this.state.factorKey); - const tssNonce = this.getTssNonce(); - - if (!tssPubKey || !torusNodeTSSEndpoints) { - throw new Error("tssPubKey or torusNodeTSSEndpoints not available"); - } - - if (tssPubKey.length === FIELD_ELEMENT_HEX_LEN + 1) { - tssPubKey = tssPubKey.subarray(1); - } - - const vid = `${this.verifier}${DELIMITERS.Delimiter1}${this.verifierId}`; - const sessionId = `${vid}${DELIMITERS.Delimiter2}default${DELIMITERS.Delimiter3}${tssNonce}${DELIMITERS.Delimiter4}`; - - const sign = async (msgHash: Buffer) => { - const parties = 4; - const clientIndex = parties - 1; - const tss = await import("@toruslabs/tss-lib"); - // 1. setup - // generate endpoints for servers - const { nodeIndexes } = await (this.tKey.serviceProvider as TorusServiceProvider).getTSSPubKey( - this.tKey.tssTag, - this.tKey.metadata.tssNonces[this.tKey.tssTag] - ); - const { endpoints, tssWSEndpoints, partyIndexes } = generateTSSEndpoints(torusNodeTSSEndpoints, parties, clientIndex, nodeIndexes); - const randomSessionNonce = keccak256(Buffer.from(generatePrivate().toString("hex") + Date.now(), "utf8")).toString("hex"); - const tssImportUrl = `${torusNodeTSSEndpoints[0]}/v1/clientWasm`; - // session is needed for authentication to the web3auth infrastructure holding the factor 1 - const currentSession = `${sessionId}${randomSessionNonce}`; - - // setup mock shares, sockets and tss wasm files. - const [sockets] = await Promise.all([tssUtils.setupSockets(tssWSEndpoints, randomSessionNonce), tss.default(tssImportUrl)]); - - const participatingServerDKGIndexes = nodeIndexes; - const dklsCoeff = tssUtils.getDKLSCoeff(true, participatingServerDKGIndexes, tssShareIndex as number); - const denormalisedShare = dklsCoeff.mul(tssShare).umod(CURVE.curve.n); - const share = scalarBNToBufferSEC1(denormalisedShare).toString("base64"); - - if (!currentSession) { - throw new Error(`sessionAuth does not exist ${currentSession}`); - } - - if (!this.signatures) { - throw new Error(`Signature does not exist ${this.signatures}`); - } - - const client = new Client( - currentSession, - clientIndex, - partyIndexes, - endpoints, - sockets, - share, - tssPubKey.toString("base64"), - true, - tssImportUrl - ); - const serverCoeffs: Record = {}; - for (let i = 0; i < participatingServerDKGIndexes.length; i++) { - const serverIndex = participatingServerDKGIndexes[i]; - serverCoeffs[serverIndex] = tssUtils.getDKLSCoeff(false, participatingServerDKGIndexes, tssShareIndex as number, serverIndex).toString("hex"); - } - client.precompute(tss, { signatures: this.signatures, server_coeffs: serverCoeffs }); - await client.ready(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let { r, s, recoveryParam } = await client.sign(tss as any, Buffer.from(msgHash).toString("base64"), true, "", "keccak256", { - signatures: this.signatures, - }); - - if (recoveryParam < 27) { - recoveryParam += 27; - } - await client.cleanup(tss, { signatures: this.signatures, server_coeffs: serverCoeffs }); - return { v: recoveryParam, r: scalarBNToBufferSEC1(r), s: scalarBNToBufferSEC1(s) }; - }; - - const getPublic: () => Promise = async () => { - return tssPubKey; - }; - - await signingProvider.setupProvider({ sign, getPublic }); + await signingProvider.setupProvider({ sign: this.sign, getPublic: this.getPublic }); this.privKeyProvider = signingProvider; } @@ -995,4 +1048,15 @@ export class Web3AuthMPCCoreKit implements ICoreKit { private _getSignatures(sessionData: TorusKey["sessionData"]["sessionTokenData"]): string[] { return sessionData.map((session) => JSON.stringify({ data: session.token, sig: session.signature })); } + + private async getSigningSignatures(data: string): Promise { + if (!this.signatures) throw new Error("signatures not present"); + log.info("data", data); + return this.signatures; + } + + private isNodejsOrRN(params: CoreKitMode): boolean { + const mode = params; + return mode === "nodejs" || mode === "react-native"; + } } diff --git a/src/point.ts b/src/point.ts index 6c1689c0..f87f2c2b 100644 --- a/src/point.ts +++ b/src/point.ts @@ -1,4 +1,5 @@ import { Point as TkeyPoint } from "@tkey-mpc/common-types"; +import type { BNString } from "@toruslabs/torus.js"; import BN from "bn.js"; import { curve } from "elliptic"; @@ -21,6 +22,16 @@ export class Point { this.p = p; } + /** + * Creates a new Point from a private Key. + * @param p - The TKey Point. + * @returns The Point encoded by `p`. + */ + public static fromPrivateKey(privateKey: BNString): Point { + const ep = CURVE.keyFromPrivate(privateKey.toString("hex")).getPublic(); + return new Point(ep); + } + /** * Creates a new Point from a TKey Point. * @param p - The TKey Point. diff --git a/src/utils.ts b/src/utils.ts index 5f6228b0..595fceec 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,6 +16,7 @@ export const generateTSSEndpoints = (tssNodeEndpoints: string[], parties: number const endpoints: string[] = []; const tssWSEndpoints: string[] = []; const partyIndexes: number[] = []; + const nodeIndexesReturned: number[] = []; for (let i = 0; i < parties; i++) { partyIndexes.push(i); @@ -28,9 +29,10 @@ export const generateTSSEndpoints = (tssNodeEndpoints: string[], parties: number const targetNodeIndex = nodeIndexes[i] - 1; endpoints.push(tssNodeEndpoints[targetNodeIndex]); tssWSEndpoints.push(new URL(tssNodeEndpoints[targetNodeIndex]).origin); + nodeIndexesReturned.push(nodeIndexes[i]); } } - return { endpoints, tssWSEndpoints, partyIndexes }; + return { endpoints, tssWSEndpoints, partyIndexes, nodeIndexesReturned }; }; export function storageAvailable(type: string): boolean { @@ -59,7 +61,7 @@ export function storageAvailable(type: string): boolean { export function parseToken(token: string) { const base64Url = token.split(".")[1]; const base64 = base64Url.replace("-", "+").replace("_", "/"); - return JSON.parse(window.atob(base64 || "")); + return JSON.parse(atob(base64 || "")); } /** diff --git a/tests/factors.spec.ts b/tests/factors.spec.ts new file mode 100644 index 00000000..07fd691f --- /dev/null +++ b/tests/factors.spec.ts @@ -0,0 +1,151 @@ +/* eslint-disable mocha/handle-done-callback */ +import assert from "node:assert"; +import test from "node:test"; + +import * as TssLib from "@toruslabs/tss-lib-node"; +import BN from "bn.js"; + +import { + COREKIT_STATUS, + getWebBrowserFactor, + MemoryStorage, + Point, + SupportedStorageType, + TssShareType, + WEB3AUTH_NETWORK, + Web3AuthMPCCoreKit, +} from "../src"; +import { criticalResetAccount, mockLogin } from "./setup"; + +type FactorTestVariable = { + types: TssShareType; + manualSync?: boolean; + storage: SupportedStorageType; +}; + +// const { types } = factor; +export const FactorManipulationTest = async (newInstance: () => Promise, testVariable: FactorTestVariable) => { + test(`#Factor manipulation - ${testVariable.types} `, async function (t) { + await t.before(async function () { + const coreKitInstance = await newInstance(); + await criticalResetAccount(coreKitInstance); + await coreKitInstance.logout(); + }); + + await t.test("should able to create factor", async function () { + const coreKitInstance = await newInstance(); + const firstFactor = coreKitInstance.getCurrentFactorKey(); + // try delete hash factor factor + try { + const pt = Point.fromPrivateKey(firstFactor.factorKey); + await coreKitInstance.deleteFactor(pt.toTkeyPoint()); + throw new Error("should not reach here"); + } catch {} + + // create factor + const factorKey1 = await coreKitInstance.createFactor({ + shareType: TssShareType.DEVICE, + }); + + const factorKey2 = await coreKitInstance.createFactor({ + shareType: TssShareType.RECOVERY, + }); + + // sync + if (testVariable.manualSync) { + await coreKitInstance.commitChanges(); + } + // clear session prevent rehydration + await coreKitInstance.logout(); + + // new instance + const instance2 = await newInstance(); + assert.strictEqual(instance2.getTssFactorPub().length, 3); + + // try inputFactor ( set as active factor ) + + // delete factor + const pt = Point.fromPrivateKey(factorKey1); + await instance2.deleteFactor(pt.toTkeyPoint()); + + // delete factor + const pt2 = Point.fromPrivateKey(factorKey2); + await instance2.deleteFactor(pt2.toTkeyPoint()); + + if (testVariable.manualSync) { + await instance2.commitChanges(); + } + + // new instance + const instance3 = await newInstance(); + assert.strictEqual(instance3.getTssFactorPub().length, 1); + }); + + // enable mfa + + await t.test("enable MFA", async function () { + const instance = await newInstance(); + const recoverFactor = await instance.enableMFA({}); + + if (testVariable.manualSync) { + await instance.commitChanges(); + } + + // to prevent rehydration ( rehydrate session id store in BrowserStorage) + await instance.logout(); + + // new instance + const instance2 = await newInstance(); + const browserFactor = await getWebBrowserFactor(instance2, testVariable.storage); + // try { + // checkLogin(instance2); + // } + + // login with mfa factor + await instance2.inputFactorKey(new BN(recoverFactor, "hex")); + assert.strictEqual(instance2.status, COREKIT_STATUS.LOGGED_IN); + await instance2.logout(); + + // new instance + const instance3 = await newInstance(); + assert.strictEqual(instance3.status, COREKIT_STATUS.REQUIRED_SHARE); + + await instance3.inputFactorKey(new BN(browserFactor, "hex")); + assert.strictEqual(instance3.status, COREKIT_STATUS.LOGGED_IN); + }); + }); +}; + +const variable: FactorTestVariable[] = [ + { types: TssShareType.DEVICE, manualSync: true, storage: new MemoryStorage() }, + { types: TssShareType.RECOVERY, manualSync: true, storage: "memory" }, +]; + +const email = "testmail99"; +variable.forEach(async (testVariable) => { + const newCoreKitLogInInstance = async () => { + const instance = new Web3AuthMPCCoreKit({ + web3AuthClientId: "torus-key-test", + web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, + baseUrl: "http://localhost:3000", + uxMode: "nodejs", + tssLib: TssLib, + storageKey: testVariable.storage, + manualSync: testVariable.manualSync, + }); + + const { idToken, parsedToken } = await mockLogin(email); + await instance.init({ handleRedirectResult: false }); + try { + await instance.loginWithJWT({ + verifier: "torus-test-health", + verifierId: parsedToken.email, + idToken, + }); + } catch (error) {} + + return instance; + }; + + await FactorManipulationTest(newCoreKitLogInInstance, testVariable); +}); diff --git a/tests/login.spec.ts b/tests/login.spec.ts new file mode 100644 index 00000000..c6bc03e8 --- /dev/null +++ b/tests/login.spec.ts @@ -0,0 +1,124 @@ +/* eslint-disable mocha/handle-done-callback */ +import assert from "node:assert"; +import test from "node:test"; + +import { UX_MODE_TYPE } from "@toruslabs/customauth"; +import { keccak256 } from "@toruslabs/metadata-helpers"; +import * as TssLib from "@toruslabs/tss-lib-node"; +import BN from "bn.js"; +import { ec as EC } from "elliptic"; + +import { COREKIT_STATUS, WEB3AUTH_NETWORK, WEB3AUTH_NETWORK_TYPE, Web3AuthMPCCoreKit } from "../src"; +import { criticalResetAccount, mockLogin } from "./setup"; + +type TestVariable = { + web3AuthNetwork: WEB3AUTH_NETWORK_TYPE; + uxMode: UX_MODE_TYPE | "nodejs"; + manualSync?: boolean; + + email: string; +}; + +const defaultTestEmail = "testEmail1"; +const variable: TestVariable[] = [ + { web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, uxMode: "nodejs", email: defaultTestEmail }, + // { web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, uxMode: UX_MODE.REDIRECT, email: defaultTestEmail }, + + { web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, uxMode: "nodejs", manualSync: true, email: defaultTestEmail }, + // { web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, uxMode: UX_MODE.REDIRECT, manualSync: true, email: defaultTestEmail }, +]; + +const checkLogin = async (coreKitInstance: Web3AuthMPCCoreKit) => { + const keyDetails = coreKitInstance.getKeyDetails(); + assert.strictEqual(coreKitInstance.status, COREKIT_STATUS.LOGGED_IN); + assert.strictEqual(keyDetails.requiredFactors, 0); + const factorkey = coreKitInstance.getCurrentFactorKey(); + await coreKitInstance.tKey.getTSSShare(new BN(factorkey.factorKey, "hex")); +}; + +variable.forEach((testVariable) => { + const { web3AuthNetwork, uxMode, manualSync, email } = testVariable; + const coreKitInstance = new Web3AuthMPCCoreKit({ + web3AuthClientId: "torus-key-test", + web3AuthNetwork, + baseUrl: "http://localhost:3000", + uxMode, + tssLib: TssLib, + storageKey: "memory", + manualSync, + }); + + const testNameSuffix = JSON.stringify(testVariable); + test(`#Login Test with JWT + logout : ${testNameSuffix}`, async (t) => { + t.before(async function () { + if (coreKitInstance.status === COREKIT_STATUS.INITIALIZED) await criticalResetAccount(coreKitInstance); + }); + + t.after(async function () { + // after all test tear down + }); + + // t.skip("#Login with Oauth", async function () { + // // popup + // // redirect flow + // // not testable + // }); + await t.test("#Login ", async function () { + // mocklogin + const { idToken, parsedToken } = await mockLogin(email); + await coreKitInstance.init({ handleRedirectResult: false }); + await coreKitInstance.loginWithJWT({ + verifier: "torus-test-health", + verifierId: parsedToken.email, + idToken, + }); + + // get key details + await checkLogin(coreKitInstance); + }); + + await t.test("#relogin ", async function () { + // reload without rehydrate + // await coreKitInstance.init({ rehydrate: false }); + + // rehydrate + await coreKitInstance.init({ handleRedirectResult: false }); + await checkLogin(coreKitInstance); + + // logout + await coreKitInstance.logout(); + + // rehydrate should fail + await coreKitInstance.init(); + assert.strictEqual(coreKitInstance.status, COREKIT_STATUS.INITIALIZED); + try { + coreKitInstance.getCurrentFactorKey(); + throw new Error("should not reach here"); + } catch (error) {} + + // relogin + const { idToken, parsedToken } = await mockLogin(email); + await coreKitInstance.loginWithJWT({ + verifier: "torus-test-health", + verifierId: parsedToken.email, + idToken, + }); + + // get key details + await checkLogin(coreKitInstance); + }); + + await t.test("#able to sign", async function () { + const msg = "hello world"; + const msgBuffer = Buffer.from(msg); + const msgHash = keccak256(msgBuffer); + const signature = await coreKitInstance.sign(msgHash); + + const secp256k1 = new EC("secp256k1"); + const pubkey = secp256k1.recoverPubKey(msgHash, signature, signature.v - 27); + const publicKeyPoint = coreKitInstance.getTssPublicKey(); + assert.strictEqual(pubkey.x.toString("hex"), publicKeyPoint.x.toString("hex")); + assert.strictEqual(pubkey.y.toString("hex"), publicKeyPoint.y.toString("hex")); + }); + }); +}); diff --git a/tests/securityQuestion.spec.ts b/tests/securityQuestion.spec.ts new file mode 100644 index 00000000..a068aa09 --- /dev/null +++ b/tests/securityQuestion.spec.ts @@ -0,0 +1,135 @@ +/* eslint-disable mocha/handle-done-callback */ +/* eslint-disable no-console */ +import assert from "node:assert"; +import test from "node:test"; + +// import { getPubKeyPoint } from "@tkey-mpc/common-types"; +import { UX_MODE_TYPE } from "@toruslabs/customauth"; +import * as TssLib from "@toruslabs/tss-lib-node"; +import BN from "bn.js"; + +import { COREKIT_STATUS, TssSecurityQuestion, WEB3AUTH_NETWORK, WEB3AUTH_NETWORK_TYPE, Web3AuthMPCCoreKit } from "../src"; +import { criticalResetAccount, mockLogin } from "./setup"; + +type TestVariable = { + web3AuthNetwork: WEB3AUTH_NETWORK_TYPE; + uxMode: UX_MODE_TYPE | "nodejs"; + manualSync?: boolean; +}; + +export const TssSecurityQuestionsTest = async (newInstance: () => Promise, testVariable: TestVariable) => { + test(`#Tss Security Question - ${testVariable.manualSync} `, async function (t) { + await t.before(async function () { + const coreKitInstance = await newInstance(); + if (coreKitInstance.status === COREKIT_STATUS.REQUIRED_SHARE) await criticalResetAccount(coreKitInstance); + await coreKitInstance.logout(); + }); + t.afterEach(function () { + return console.log("finished running test"); + }); + t.after(function () { + return console.log("finished running tests"); + }); + + await t.test("should work", async function () { + // set security question + const instance = await newInstance(); + const question = "test question"; + const answer = "test answer"; + const newQuestion = "new question"; + const newAnswer = "new answer"; + // const shareType = TssShareType.DEVICE; + + const securityQuestion = new TssSecurityQuestion(); + await securityQuestion.setSecurityQuestion({ + mpcCoreKit: instance, + question, + answer, + // shareType, + }); + + // recover factor + const factor = await securityQuestion.recoverFactor(instance, answer); + // check factor + await instance.tKey.getTSSShare(new BN(factor, "hex")); + // check wrong answer + try { + await securityQuestion.recoverFactor(instance, "wrong answer"); + throw new Error("should not reach here"); + } catch {} + + // change factor + await securityQuestion.changeSecurityQuestion({ + mpcCoreKit: instance, + newQuestion, + newAnswer, + answer, + }); + // recover factor + // check factor + const newFactor = await securityQuestion.recoverFactor(instance, newAnswer); + await instance.tKey.getTSSShare(new BN(newFactor, "hex")); + + try { + await instance.tKey.getTSSShare(new BN(factor, "hex")); + throw new Error("should not reach here"); + } catch {} + + // recover factor + // check wrong answer + try { + await securityQuestion.recoverFactor(instance, answer); + throw new Error("should not reach here"); + } catch {} + + // delete factor + await securityQuestion.deleteSecurityQuestion(instance); + // recover factor + try { + await securityQuestion.recoverFactor(instance, answer); + throw new Error("should not reach here"); + } catch {} + + // input factor + assert.strictEqual(true, true); + }); + }); +}; + +const variable: TestVariable[] = [ + // { web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, uxMode: UX_MODE.REDIRECT }, + // { web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, uxMode: UX_MODE.REDIRECT }, + + { web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, uxMode: "nodejs", manualSync: true }, + // { web3AuthNetwork: WEB3AUTH_NETWORK.MAINNET, uxMode: UX_MODE.REDIRECT, manualSync: true }, +]; + +const email = "testmail99"; + +variable.forEach(async (testVariable) => { + const newCoreKitLogInInstance = async () => { + const instance = new Web3AuthMPCCoreKit({ + web3AuthClientId: "torus-key-test", + web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, + baseUrl: "http://localhost:3000", + uxMode: "nodejs", + tssLib: TssLib, + storageKey: "memory", + manualSync: testVariable.manualSync, + }); + + const { idToken, parsedToken } = await mockLogin(email); + await instance.init({ handleRedirectResult: false }); + try { + await instance.loginWithJWT({ + verifier: "torus-test-health", + verifierId: parsedToken.email, + idToken, + }); + } catch (error) {} + + return instance; + }; + + await TssSecurityQuestionsTest(newCoreKitLogInInstance, testVariable); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 00000000..c9464030 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,94 @@ +import * as TssLib from "@toruslabs/tss-lib-node"; +import BN from "bn.js"; +import jwt, { Algorithm } from "jsonwebtoken"; + +import { parseToken, WEB3AUTH_NETWORK_TYPE, Web3AuthMPCCoreKit } from "../src"; + +export const mockLogin2 = async (email: string) => { + const req = new Request("https://li6lnimoyrwgn2iuqtgdwlrwvq0upwtr.lambda-url.eu-west-1.on.aws/", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ verifier: "torus-key-test", scope: "email", extraPayload: { email }, alg: "ES256" }), + }); + + const resp = await fetch(req); + const bodyJson = (await resp.json()) as { token: string }; + const idToken = bodyJson.token; + const parsedToken = parseToken(idToken); + return { idToken, parsedToken }; +}; + +export const criticalResetAccount = async (coreKitInstance: Web3AuthMPCCoreKit): Promise => { + // This is a critical function that should only be used for testing purposes + // Resetting your account means clearing all the metadata associated with it from the metadata server + // The key details will be deleted from our server and you will not be able to recover your account + if (!coreKitInstance) { + throw new Error("coreKitInstance is not set"); + } + + await coreKitInstance.tKey.storageLayer.setMetadata({ + privKey: new BN(coreKitInstance.metadataKey!, "hex"), + input: { message: "KEY_NOT_FOUND" }, + }); +}; + +const privateKey = "MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCD7oLrcKae+jVZPGx52Cb/lKhdKxpXjl9eGNa1MlY57A=="; +const jwtPrivateKey = `-----BEGIN PRIVATE KEY-----\n${privateKey}\n-----END PRIVATE KEY-----`; +const alg: Algorithm = "ES256"; + +export const mockLogin = async (email: string) => { + const iat = Math.floor(Date.now() / 1000); + const payload = { + iss: "torus-key-test", + aud: "torus-key-test", + name: email, + email, + scope: "email", + iat, + eat: iat + 120, + }; + + const algo = { + expiresIn: 120, + algorithm: alg, + }; + + const token = jwt.sign(payload, jwtPrivateKey, algo); + const idToken = token; + const parsedToken = parseToken(idToken); + return { idToken, parsedToken }; +}; + +export const newCoreKitLogInInstance = async ({ + network, + manualSync, + email, +}: { + network: WEB3AUTH_NETWORK_TYPE; + manualSync: boolean; + email: string; +}) => { + const instance = new Web3AuthMPCCoreKit({ + web3AuthClientId: "torus-key-test", + web3AuthNetwork: network, + baseUrl: "http://localhost:3000", + uxMode: "nodejs", + tssLib: TssLib, + storageKey: "memory", + manualSync, + }); + + const { idToken, parsedToken } = await mockLogin(email); + await instance.init(); + try { + await instance.loginWithJWT({ + verifier: "torus-test-health", + verifierId: parsedToken.email, + idToken, + }); + } catch (error) {} + + return instance; +}; diff --git a/tests/signing.spec.ts b/tests/signing.spec.ts new file mode 100644 index 00000000..e69de29b