UNPKG

fsl-authorization

Version:
1,148 lines (1,074 loc) 31.1 kB
import { BigNumber } from 'ethers'; import { omit, pick } from 'lodash'; import { SignatureLike } from '@ethersproject/bytes'; import { verifyMessage, verifyTypedData } from 'ethers/lib/utils'; import { v4 as uuidv4 } from 'uuid'; import { Transaction, TransactionInstruction, VersionedTransaction, } from '@solana/web3.js'; import { getAppInfo, initRequestToken, logout, optAutoApproveState, } from './api'; import { IDomain, ITypes, IMessage } from './defination'; import { bufferToTransaction, closeModal, DEVICE_TYPE, getUserAgent, initModal, showModal, } from './utils'; import SDKStorage from './store'; const clearLoopObserver = ( clientWindow: Window | null, handleMessage: (e: any) => void, ) => { const interval = setInterval(function () { if (!clientWindow || clientWindow.closed) { clearInterval(interval); window.removeEventListener('message', handleMessage); } }, 500); return interval; }; const checkWindowOpenStatus = ( clientWindow: Window | null, reject: (reason?: any) => void, msg: any = 'The pop-up cannot be ejected', ) => { const id = setTimeout(() => { clearTimeout(id); if (!clientWindow) { reject(msg); } }, 2000); }; const envPromiseCheckWrapper = <T>( callback: ( res: (value: any | PromiseLike<any>) => void, rej: (reason?: any) => void, ) => any, clientWindow: Window | null, msg?: any, ): Promise<T> => { return new Promise((resolve, reject) => { checkWindowOpenStatus(clientWindow, reject, msg); callback(resolve, reject); }); }; interface FSLLoginOptions { responseType: string; appKey: string; redirectUri?: string; scope?: string; state?: string; usePopup?: boolean; useModal?: boolean; isApp?: boolean; domain?: string; uid?: number; } class FSLAuthorization { responseType: string; appKey: string; redirectUri?: string; scope?: string; state?: string; usePopup?: boolean; useModal?: boolean; domain: string = 'https://id.fsl.com'; isApp?: boolean; inWhiteList: boolean; requestToken?: string; sdkStorage?: SDKStorage; uid?: number; handling: boolean; windowFeatures = `left=${window.screen.width / 2 - 200},top=${ window.screen.height / 2 - 500 },width=500,height=800,popup=1`; private constructor(opt: FSLLoginOptions) { const { responseType, appKey, redirectUri, scope, state, useModal, usePopup, domain, isApp, uid, } = opt; this.appKey = appKey; this.responseType = responseType; this.redirectUri = redirectUri; this.scope = scope; this.usePopup = usePopup; this.useModal = useModal; this.state = state; this.isApp = isApp; this.inWhiteList = false; this.uid = uid; this.handling = false; if (domain) this.domain = domain; window.apiOrigin = this.domain; if (this.uid) { this.sdkStorage = new SDKStorage(this.uid + ''); const token = this.sdkStorage.getItem('token') as string; if (token) { initRequestToken(token); } } } private receiveLoginMessage = (e: MessageEvent) => { this.sdkStorage!.setItem('token', e.data.data.requestToken); this.sdkStorage!.setItem('passkeyHash', e.data.data.passkeyHash); this.sdkStorage!.setItem('hash', e.data.data.hash); this.sdkStorage!.setItem('local', e.data.data.local); this.sdkStorage!.setItem('hasDownload', e.data.data.hasDownload); this.sdkStorage!.setItem('email', e.data.data.email); this.sdkStorage!.setItem('ico', e.data.data.ico); this.sdkStorage!.setItem('refreshToken', e.data.data.refreshToken); this.sdkStorage!.setItem('pwdKey', e.data.data.pwdKey); this.sdkStorage!.setItem('accessToken', e.data.data.accessToken); this.sdkStorage!.setItem('credentialId', e.data.data.credentialId); e.data.data.requestToken && initRequestToken(e.data.data.requestToken); }; static async init(opt: FSLLoginOptions) { const auth = new FSLAuthorization(opt); let doneCount = 0, MAX_DONE = 1; let authResolve!: (a: FSLAuthorization) => void, authReject!: (e: any) => void; const authPromise = new Promise<FSLAuthorization>((res, rej) => { authResolve = res; authReject = rej; }); if (opt.useModal) { try { const { state } = (await getAppInfo(opt.appKey)) as any; if (state & 1) { auth.inWhiteList = true; // opener initiate wallet if (window.opener && opt.uid) { // set maximum number of callback MAX_DONE = 2; const handler = (e: MessageEvent) => { if (e.data.type === 'fsl_login' && opt.uid === +e.data.data.id) { auth.receiveLoginMessage(e); window.removeEventListener('message', handler); doneCount++; if (doneCount === MAX_DONE) { authResolve(auth); } } }; window.addEventListener('message', handler, false); window.opener.postMessage( { type: 'fsl_login', data: 'done', }, auth.domain, ); } initModal(auth.domain); const childFrame = showModal(auth.domain, true); const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth' && e.data.data === 'done') { childFrame && childFrame.contentWindow?.postMessage( { type: 'fsl_users', data: JSON.stringify(SDKStorage.getAllUsers()), }, auth.domain, ); window.removeEventListener('message', handleMessage); doneCount++; if (doneCount === MAX_DONE) { authResolve(auth); } } }; window.addEventListener('message', handleMessage, false); } else { authResolve(auth); } } catch (err: any) { console.log(err.msg || err.message); authReject(err); } } else { authResolve(auth); } return authPromise; } async getApproveDDL() { return optAutoApproveState(this.appKey); } async openAutoApprove() { return optAutoApproveState(this.appKey, 1); } async closeAutoApprove() { return optAutoApproveState(this.appKey, -1); } async signIn(args?: { withState: boolean }) { const callUrl = new URL(`${this.domain}/login/fslUsers`); const commonArgs: Record<string, string | undefined> = { response_type: this.responseType, appkey: this.appKey, scope: this.scope, state: this.state, is_app: this.isApp ? '1' : undefined, withState: args?.withState ? '1' : undefined, }; for (let key in commonArgs) { if (commonArgs[key]) { callUrl.searchParams.append(key, commonArgs[key]!); } } if (this.inWhiteList && this.useModal) { callUrl.searchParams.append('use_modal', '1'); getUserAgent() !== DEVICE_TYPE.PC && callUrl.searchParams.append('isMobile', '1'); showModal(callUrl.toString()); return new Promise((resolve) => { window.handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { resolve(pick(e.data.data, this.responseType, 'token')); this.uid = +e.data.data.id; this.sdkStorage = new SDKStorage(e.data.data.id + ''); this.receiveLoginMessage(e); window.removeEventListener('message', window.handleMessage!); closeModal(); window.handleMessage = null; } }; window.addEventListener('message', window.handleMessage, false); }); } if (!this.usePopup) { callUrl.searchParams.append('redirect_uri', this.redirectUri!); location.href = callUrl.toString(); return Promise.resolve(null); } else { callUrl.searchParams.append('use_popup', '1'); if (this.isApp) { callUrl.searchParams.append('redirect_uri', this.redirectUri!); } const clientWindow = window.open( callUrl.toString(), this.isApp ? '_blank' : `signWindow`, this.windowFeatures, ); if (this.isApp) { return Promise.resolve(null); } return envPromiseCheckWrapper((resolve) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { resolve(e.data.data); window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } } async signOut() { this.sdkStorage?.removeItem('token'); return logout(); } async signInV2() { return this.signIn({ withState: true }); } async checkAndAutoHandle<T>( uid: number | undefined, callUrl: URL, params: Record<string, any>, format?: (data: any) => T, ): Promise<T | any> { if (this.handling) { return Promise.reject({ message: 'There is currently one transaction that has not been processed', }); } this.handling = true; let time = 0; let childFrame: HTMLIFrameElement; if ( uid && (!this.sdkStorage || +(this.sdkStorage.getItem('id') as string) !== uid) ) { this.sdkStorage = new SDKStorage(uid + ''); const token = this.sdkStorage.getItem('token') as string; if (token) initRequestToken(token); } try { time = (await this.getApproveDDL()) as unknown as number; } catch (err) { this.handling = false; return err; } const terminalType = getUserAgent(); if (time) { const url = new URL(`${this.domain}/authorization/silent`); url.search = callUrl.search; // token && url.searchParams.append('token', token); childFrame = showModal(url.toString(), true); } else { terminalType !== DEVICE_TYPE.PC && callUrl.searchParams.append('isMobile', '1'); // token && callUrl.searchParams.append('token', token); childFrame = showModal(callUrl.toString()); } return new Promise((resolve, reject) => { window.handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { childFrame && childFrame.contentWindow?.postMessage( { type: 'fsl_params', data: JSON.stringify(params), }, this.domain, ); return; } else { resolve(e.data.data); } } else if ( typeof e.data.data === 'object' && ('msg' in e.data.data || 'message' in e.data.data) ) { reject(e.data.data); } else { const data = format ? format(e.data.data) : e.data.data; resolve(data); } window.removeEventListener('message', window.handleMessage!); closeModal(); window.handleMessage = null; this.handling = false; } }; window.addEventListener('message', window.handleMessage, false); }); } static evmVerifyMessage(msg: string, signature: SignatureLike) { return verifyMessage(msg, signature); } static evmVerifyTypedData( domain: IDomain, types: ITypes, message: IMessage, signature: SignatureLike, ) { return verifyTypedData(domain, types, message, signature); } async callEvmSign(args: { msg: any; rpc?: string; chainId: number; chain?: string; domain?: string; uid?: number; signDigest?: boolean; }) { const { msg, chainId, chain, rpc, signDigest, uid = this.uid } = args; const callUrl = new URL(`${args.domain || this.domain}/authorization/sign`); let type: string; switch (true) { case msg instanceof Uint8Array: type = 'unit8Array'; break; case msg instanceof Uint16Array: type = 'unit16Array'; break; case msg instanceof Uint32Array: type = 'unit32Array'; break; default: type = ''; } const uuid = uuidv4(); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, appKey: this.appKey, chainId, chain, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle(uid, callUrl, { id: uuid, msg, type, rpc, signDigest: signDigest ? 1 : undefined, name: 'callEvmSign', }); } const url = callUrl.toString(); const clientWindow = window.open(url, `evmSignWindow`, this.windowFeatures); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ id: uuid, msg, type, rpc, signDigest: signDigest ? 1 : undefined, }), }, '*', ); return; } else { resolve(e.data.data); } } else { reject(e.data.data); } window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async callEvmSignDigest(args: { msg: any; rpc?: string; chainId: number; chain?: string; domain?: string; uid?: number; }) { return this.callEvmSign({ ...args, signDigest: true }); } async signTypedData(args: { domain: IDomain; types: ITypes; message: IMessage; chainId: number; mockDomain?: string; chain?: string; uid?: number; }) { const { domain, types, message, mockDomain, chain, chainId, uid = this.uid, } = args; const uuid = uuidv4(); const callUrl = new URL( `${mockDomain || this.domain}/authorization/sign-v4`, ); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, appKey: this.appKey, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle(uid, callUrl, { message, chain, chainId, types, domain, id: uuid, name: 'signTypedData', }); } const url = callUrl.toString(); const clientWindow = window.open( url, `typedSignWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ message, chain, chainId, types, domain, id: uuid, }), }, '*', ); } else { resolve(e.data.data); window.removeEventListener('message', handleMessage); } } else { reject(e.data.data); window.removeEventListener('message', handleMessage); } } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async callEvmContractByCallData(args: { contractAddress: string; callData: string; chainId: number; gasLimit: string; value?: string; chain?: string; rpc?: string; domain?: string; nonce?: number; onlySign?: boolean; maxPriorityFeePerGasValue?: BigNumber; maxFeePerGasValue?: BigNumber; uid?: number; confirmed?: boolean; }) { const uuid = uuidv4(); const { contractAddress, callData, chainId, gasLimit, value, chain, rpc, domain, nonce, onlySign, maxPriorityFeePerGasValue, maxFeePerGasValue, uid = this.uid, confirmed = false, } = args; const callUrl = new URL(`${domain || this.domain}/authorization/call-data`); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, appKey: this.appKey, chainId, chain, onlySign: onlySign ? 'onlySign' : undefined, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle(uid, callUrl, { contractAddress, callData, gasLimit, value, rpc, chainId, chain, nonce, onlySign: onlySign ? 'onlySign' : undefined, maxFeePerGasValue, maxPriorityFeePerGasValue, id: uuid, confirmed, name: 'callEvmContractByCallData', }); } const url = callUrl.toString(); const clientWindow = window.open( url, `callDataWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ contractAddress, callData, chainId, gasLimit, value, chain, rpc, nonce, maxFeePerGasValue, maxPriorityFeePerGasValue, id: uuid, confirmed, }), }, '*', ); } else { resolve(e.data.data); window.removeEventListener('message', handleMessage); } } else if ( typeof e.data.data === 'object' && 'transactionHash' in e.data.data ) { resolve(e.data.data); window.removeEventListener('message', handleMessage); } else { reject(e.data.data); window.removeEventListener('message', handleMessage); } } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async signCallDataTransaction(args: { contractAddress: string; callData: string; chainId: number; gasLimit: string; value?: string; chain?: string; rpc?: string; domain?: string; nonce?: number; maxPriorityFeePerGasValue?: BigNumber; maxFeePerGasValue?: BigNumber; uid?: number; }) { return this.callEvmContractByCallData({ ...args, onlySign: true }); } async signSolMessage(args: { msg: string; domain?: string; uid?: number }) { const { msg, domain, uid = this.uid } = args; const uuid = uuidv4(); const callUrl = new URL(`${domain || this.domain}/authorization/sol-sign`); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, appKey: this.appKey, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle(uid, callUrl, { msg, id: uuid, name: 'signSolMessage', }); } const url = callUrl.toString(); const clientWindow = window.open( url, `signSolMsgWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ msg, id: uuid, }), }, '*', ); } } else if ( e.data.data && typeof e.data.data === 'object' && 'length' in e.data.data ) { resolve(e.data.data); } else { reject(e.data.data); } window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async callSolInstructions(args: { instructions: TransactionInstruction[]; rpc?: string; unitLimit?: number; unitPrice?: number; domain?: string; onlySign?: boolean; uid?: number; }) { const { domain, instructions, rpc, unitPrice, unitLimit, onlySign, uid = this.uid, } = args; const uuid = uuidv4(); const callUrl = new URL(`${domain || this.domain}/authorization/sol-trade`); callUrl.searchParams.append( 'arguments', JSON.stringify({ appKey: this.appKey, onlySign: onlySign ? 'onlySign' : void 0, id: uuid, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle(uid, callUrl, { instructions, unitLimit, unitPrice, id: uuid, rpc, onlySign: onlySign ? 'onlySign' : void 0, name: 'callSolInstructions', }); } const url = callUrl.toString(); const clientWindow = window.open( url, `signSolCallWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = async (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ instructions, unitLimit, unitPrice, rpc, id: uuid, }), }, '*', ); } else { resolve(e.data.data); window.removeEventListener('message', handleMessage); } } else { reject(e.data.data); window.removeEventListener('message', handleMessage); } } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async signSolInstructions(args: { instructions: any[]; rpc?: string; unitLimit?: number; unitPrice?: number; domain?: string; uid?: number; }) { return this.callSolInstructions({ ...args, onlySign: true }); } async signSolTransaction(args: { transactions: Array<Transaction> | Array<VersionedTransaction>; uid?: number; }): Promise<Array<Transaction> | Array<VersionedTransaction>> { const { transactions, uid = this.uid } = args; let bufferStrs: string[] = []; const versions: Array<'legacy' | 0> = []; try { bufferStrs = transactions.map((item: any) => { if (item.version === 0) { versions.push(0); return Buffer.from( item.serialize({ verifySignatures: false }), ).toString('base64'); } else { versions.push('legacy'); return item.serialize({ verifySignatures: false }).toString('base64'); } }); } catch (err: any) { return Promise.reject(err.message); } const uuid = uuidv4(); const callUrl = new URL(`${this.domain}/authorization/sol-transaction`); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, appKey: this.appKey, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle< Array<Transaction> | Array<VersionedTransaction> >( uid, callUrl, { transactions: bufferStrs, versions, id: uuid, name: 'signSolTransaction', }, (data: any) => { let transactions = data; if (Array.isArray(data)) { transactions = bufferToTransaction(data, versions); } return transactions; }, ); } const url = callUrl.toString(); const clientWindow = window.open( url, `signSolTrsWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = async (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ transactions: bufferStrs, versions, id: uuid, }), }, '*', ); } return; } else if (Array.isArray(e.data.data)) { const transactions = bufferToTransaction(e.data.data, versions); resolve(transactions); } else { reject(e.data.data); } window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async callEvmContract(args: { contractAddress: string; methodName: string; abi?: any; chainId: number; chain?: string; value?: string; gasLimit: string; params?: any[]; to?: string; rpc?: string; domain?: string; nonce?: number; maxPriorityFeePerGasValue?: BigNumber; maxFeePerGasValue?: BigNumber; onlySign?: boolean; uid?: number; }) { const { contractAddress, methodName, abi, chainId, chain, value, gasLimit, params, to, rpc, domain, nonce, maxPriorityFeePerGasValue, maxFeePerGasValue, onlySign, uid = this.uid, } = args; const id = uuidv4(); const callUrl = new URL(`${domain || this.domain}/authorization/trade-v2`); callUrl.searchParams.append( 'arguments', JSON.stringify({ appKey: this.appKey, onlySign: onlySign ? 'onlySign' : undefined, chainId, chain, id, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } const url = callUrl.toString(); if (this.useModal && this.inWhiteList) { return this.checkAndAutoHandle(uid, callUrl, { contractAddress, methodName, abi, value, gasLimit, params, to, nonce, maxPriorityFeePerGasValue, maxFeePerGasValue, id, rpc, onlySign: onlySign ? 'onlySign' : undefined, name: 'callEvmContract', }); } const clientWindow = window.open( url, `evmContractWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { if (e.data.data === 'done') { clientWindow && clientWindow.postMessage( { type: 'fsl_params', data: JSON.stringify({ contractAddress, methodName, abi, value, gasLimit, params, to, nonce, maxPriorityFeePerGasValue, maxFeePerGasValue, id, rpc, }), }, '*', ); } else { resolve(e.data.data); window.removeEventListener('message', handleMessage); } } else if ( e.data.data && typeof e.data.data === 'object' && 'transactionHash' in e.data.data ) { resolve(e.data.data); window.removeEventListener('message', handleMessage); } else { reject(e.data.data); window.removeEventListener('message', handleMessage); } } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async signEvmContract(args: { contractAddress: string; methodName: string; abi?: any; chainId: number; chain?: string; value?: string; gasLimit: string; params?: any[]; to?: string; rpc?: string; domain?: string; nonce?: number; maxPriorityFeePerGasValue?: BigNumber; maxFeePerGasValue?: BigNumber; uid?: number; }) { return this.callEvmContract({ ...args, onlySign: true }); } } export default FSLAuthorization;