fsl-authorization
Version:
## What id FSL ID
969 lines (902 loc) • 25.5 kB
text/typescript
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;