UNPKG

fsl-authorization

Version:
969 lines (902 loc) 25.5 kB
import { BigNumber } from 'ethers'; import { omit } from 'lodash'; import { SignatureLike } from '@ethersproject/bytes'; import { verifyMessage, verifyTypedData } from 'ethers/lib/utils'; import { v4 as uuidv4 } from 'uuid'; import { Transaction, VersionedTransaction } from '@solana/web3.js'; import { IDomain, ITypes, IMessage } from './defination'; 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 = ( callback: ( res: (value: any | PromiseLike<any>) => void, rej: (reason?: any) => void, ) => any, clientWindow: Window | null, msg?: any, ) => { 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; isApp?: boolean; domain?: string; } class FSLAuthorization { responseType?: string; appKey: string; redirectUri?: string; scope?: string; state?: string; usePopup?: boolean; domain?: string; isApp?: 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, usePopup, domain, isApp, } = opt; this.appKey = appKey; this.responseType = responseType; this.redirectUri = redirectUri; this.scope = scope; this.usePopup = usePopup; this.state = state; this.domain = domain; this.isApp = isApp; } static init(opt: FSLLoginOptions) { return new FSLAuthorization(opt); } async signIn(args?: { withState: boolean }) { const callUrl = new URL( `${this.domain || 'https://id.fsl.com'}/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.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_login') { resolve(e.data.data); window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } } async signInV2() { return this.signIn({ withState: true }); } 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 } = args; const callUrl = new URL( `${ args.domain || this.domain || 'https://id.fsl.com' }/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', encodeURIComponent( JSON.stringify({ id: uuid, appKey: this.appKey, rpc, chainId, chain, }), ), ); if (args.uid) { callUrl.searchParams.append('uid', args.uid + ''); } 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, 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 signTransaction(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; }) { const callUrl = new URL( `${ args.domain || this.domain || 'https://id.fsl.com' }/authorization/trade`, ); callUrl.searchParams.append( 'arguments', JSON.stringify({ ...omit(args, 'domain', 'uid'), onlySign: 'onlySign', appKey: this.appKey, }), ); if (args.uid) { callUrl.searchParams.append('uid', args.uid + ''); } const url = callUrl.toString(); const clientWindow = window.open( url, `signEvmContractWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { const handleMessage = (e: any) => { if (e.data.type === 'fsl_auth') { if (typeof e.data.data === 'string') { resolve(e.data.data); } 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; uid?: number; confirmed?: boolean; }) { if (window.callContractWindow && !window.callContractWindow.closed) { if (window.callContractHandler) { window.removeEventListener('message', window.callContractHandler); window.callContractHandler = null; } if (window.callContractInterval) { clearInterval(window.callContractInterval); window.callContractInterval = void 0; } window.callContractWindow.close(); await new Promise((resolve) => setTimeout(() => { resolve(true); }, 1000), ); } const callUrl = new URL( `${ args.domain || this.domain || 'https://id.fsl.com' }/authorization/trade`, ); args.confirmed = args.confirmed || false; callUrl.searchParams.append( 'arguments', JSON.stringify({ ...omit(args, 'domain', 'uid'), appKey: this.appKey, }), ); if (args.uid) { callUrl.searchParams.append('uid', args.uid + ''); } const url = callUrl.toString(); window.callContractWindow = window.open( url, `evmContractWindow`, this.windowFeatures, ); return envPromiseCheckWrapper((resolve, reject) => { window.callContractHandler = (e: any) => { if (e.data.type === 'fsl_auth') { if ( e.data.data && typeof e.data.data === 'object' && 'transactionHash' in e.data.data ) { resolve(e.data.data); } else { reject(e.data.data); } window.callContractHandler && window.removeEventListener('message', window.callContractHandler); } }; window.addEventListener('message', window.callContractHandler, false); window.callContractInterval = clearLoopObserver( window.callContractWindow, window.callContractHandler, ); }, window.callContractWindow); } 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 } = args; const uuid = uuidv4(); const callUrl = new URL( `${ mockDomain || this.domain || 'https://id.fsl.com' }/authorization/sign-v4`, ); callUrl.searchParams.append( 'arguments', encodeURIComponent( JSON.stringify({ id: uuid, }), ), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } 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, appKey: this.appKey, }), }, '*', ); } 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, confirmed = false, } = args; const callUrl = new URL( `${ domain || this.domain || 'https://id.fsl.com' }/authorization/call-data`, ); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, onlySign: onlySign ? 'onlySign' : undefined, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } 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, appKey: this.appKey, }), }, '*', ); } 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 }); } signSolMessage(args: { msg: string; domain?: string; uid?: number }) { const { msg, domain, uid } = args; const callUrl = new URL( `${domain || this.domain || 'https://id.fsl.com'}/authorization/sol-sign`, ); callUrl.searchParams.append( 'arguments', encodeURIComponent( JSON.stringify({ msg, appKey: this.appKey, }), ), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } 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 ( 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); } callSolInstructions(args: { instructions: any[]; keypairs: any[]; rpc?: string; unitLimit?: number; unitPrice?: number; domain?: string; onlySign?: boolean; uid?: number; }) { const { domain, instructions, keypairs, rpc, unitPrice, unitLimit, onlySign, uid, } = args; const uuid = uuidv4(); const callUrl = new URL( `${ domain || this.domain || 'https://id.fsl.com' }/authorization/sol-trade`, ); callUrl.searchParams.append( 'arguments', JSON.stringify({ appKey: this.appKey, rpc, onlySign: onlySign ? 'onlySign' : void 0, id: uuid, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } 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, keypairs, unitLimit, unitPrice, 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); } signSolInstructions(args: { instructions: any[]; keypairs: any[]; rpc?: string; unitLimit?: number; unitPrice?: number; domain?: string; uid?: number; }) { return this.callSolInstructions({ ...args, onlySign: true }); } signSolTransaction(args: { transactions: any; uid?: number }) { const { transactions, 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 || 'https://id.fsl.com'}/authorization/sol-transaction`, ); callUrl.searchParams.append( 'arguments', JSON.stringify({ id: uuid, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } 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({ appKey: this.appKey, transactions: bufferStrs, versions, id: uuid, }), }, '*', ); } } else if (Array.isArray(e.data.data)) { const handledBufferStrs = e.data.data; const transactions: Array<VersionedTransaction | Transaction> = []; try { for (let i = 0; i < versions.length; i++) { if (versions[i] === 0) { const newTransaction = VersionedTransaction.deserialize( Buffer.from(handledBufferStrs[i], 'base64'), ); transactions.push(newTransaction); } else { const newTransaction = Transaction.from( Buffer.from(handledBufferStrs[i], 'base64'), ); transactions.push(newTransaction); } } } catch (err: any) { console.log(err.message); } resolve(transactions); window.removeEventListener('message', handleMessage); } else { reject(e.data.data); window.removeEventListener('message', handleMessage); } } }; window.addEventListener('message', handleMessage, false); clearLoopObserver(clientWindow, handleMessage); }, clientWindow); } async callEvmContractV2(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, } = args; const id = uuidv4(); const callUrl = new URL( `${domain || this.domain || 'https://id.fsl.com'}/authorization/trade-v2`, ); callUrl.searchParams.append( 'arguments', JSON.stringify({ appKey: this.appKey, onlySign: onlySign ? 'onlySign' : undefined, chainId, chain, rpc, id, }), ); if (uid) { callUrl.searchParams.append('uid', uid + ''); } const url = callUrl.toString(); 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, }), }, '*', ); } 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 signEvmContractV2(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.callEvmContractV2({ ...args, onlySign: true }); } } export default FSLAuthorization;