import { ENDLESS_WALLET_COLOR_MODE_KEY, ENDLESS_WALLET_WEB3_SDK_ENABLETHEME_TOGGLE_KEY, Modal } from './ui/modal'; import { PostMessage } from './message'; import type { IRequestData } from './message/types'; import type { EndLessSDKEventType, EndLessSDKEventPayload } from './message/types'; import { EndLessSDKEvent } from './message/types'; import { IFRAMEURL } from './constants'; import { EndlessWalletOptions, UserResponse, AccountInfo, UserResponseStatus, EndlessSignMessageInput, EndlessSignMessageOutput, EndlessSignAndSubmitTransactionInput, EndlessSendTransactionType, EndlessWalletTransactionType, UserRejection, NetworkInfo, ColorMode, } from './types'; import { Network, AccountAddress, Endless, EndlessConfig, AnyRawTransaction, NetworkToChainId, NetworkToNetworkName, NetworkToNodeAPI, } from '@endlesslab/endless-ts-sdk'; import { getNetworkInfo } from './utils'; export interface Metadata { title: string; url: string; origin: string; icon: string; gameId: string; userId: string; walletAddress: string; } export { ENDLESS_WALLET_COLOR_MODE_KEY, ENDLESS_WALLET_WEB3_SDK_ENABLETHEME_TOGGLE_KEY } from './ui/modal'; export { ENDLESS_MESSAGE_TARGET, ENDLESS_WALLET_TARGET } from './message'; export { EndLessSDKEvent } from './message/types'; export { EndlessWalletOptions, UserResponseStatus, EndlessSendTransactionType, EndlessWalletTransactionType, } from './types'; export type { UserResponse, AccountInfo, EndlessSignAndSubmitTransactionInput } from './types'; export enum MethodName { INIT = 'init', SET_COLOR_MODE = 'setColorMode', CONNECT = 'connect', CREATE_WALLET = 'createWallet', GETACCOUNT = 'getAccount', DISCONNECT = 'disconnect', NETWORK_CHANGE = 'switchNetwork', SIGN_MESSAGE = 'signMessage', SEND_TRANSACTION = 'sendTransaction', SIGN_AND_SUBMIT_TRANSACTION = 'signAndSubmitTransaction', SIGN_TRANSACTION = 'signTransaction', ACCOUNT_CHANGE = 'accountChange', OPEN = 'open', CLOSE = 'close', } const rejectedCloseData: UserRejection = { status: UserResponseStatus.REJECTED, message: 'Wallet closed' }; export class EndlessJsSdk { static readonly version: string = '1.0.11'; private static _instance: EndlessJsSdk; private message: PostMessage | null = null; private _metadata: Metadata = {} as Metadata; private _initData: EndlessWalletOptions = {} as EndlessWalletOptions; private _walletAddress: string = IFRAMEURL; private _endless: Endless | null = null; private _endlessConfig: EndlessConfig | null = null; accountAddress: AccountAddress | null = null; static getIninData = () => { if (EndlessJsSdk._instance) { return EndlessJsSdk._instance._initData; } else { return {}; } }; static getAccountAddress = () => { if (EndlessJsSdk._instance) { return EndlessJsSdk._instance.accountAddress; } else { return null; } }; static setAccountAddress = (accountAddress: AccountAddress | null) => { if (EndlessJsSdk._instance) { EndlessJsSdk._instance.accountAddress = accountAddress; } }; constructor(initData: EndlessWalletOptions) { if (EndlessJsSdk._instance) return EndlessJsSdk._instance; this._initData.colorMode = localStorage.getItem(ENDLESS_WALLET_WEB3_SDK_ENABLETHEME_TOGGLE_KEY) ? (localStorage.getItem(ENDLESS_WALLET_COLOR_MODE_KEY) as ColorMode) : initData.colorMode || (localStorage.getItem(ENDLESS_WALLET_COLOR_MODE_KEY) as ColorMode); this._initData.windowWidth = (initData.windowWidth || 360) <= 360 ? 360 : initData.windowWidth; if (initData.walletUrl) { this._walletAddress = initData.walletUrl; } const modal = new Modal({ colorMode: this._initData.colorMode, url: this._walletAddress, windowWidth: this._initData.windowWidth, endless: { close: () => { // send to wallet to close this.message?.sendMessage({ uuid: new Date().getTime().toString(), methodName: MethodName.CLOSE, metadata: this._metadata, data: {}, }); // send to dapp to close this.message?.emit(EndLessSDKEvent.CLOSE, undefined); }, }, }); this.message = new PostMessage(modal); this.initWalletEvent(); this.getMetadata(); this.initConfig(initData); this.initWallet(); EndlessJsSdk._instance = this; } private initConfig(initData: EndlessWalletOptions) { if (initData.network === Network.CUSTOM) { if (!initData.fullnode || !initData.indexer) { throw new Error('Custom network must provide fullnode and indexer'); } this._initData.fullnode = initData.fullnode; this._initData.indexer = initData.indexer; this._initData.prover = initData.prover; this._initData.network = initData.network; this._endlessConfig = new EndlessConfig({ network: initData.network, indexer: initData.indexer, fullnode: initData.fullnode, prover: initData.prover, }); } else { this._initData.network = initData.network; this._endlessConfig = new EndlessConfig({ network: initData.network, }); } this._endless = new Endless(this._endlessConfig); } private initWallet() { this.message?.modal?.waitReady().then(() => { this.message?.sendMessage({ uuid: new Date().getTime().toString(), methodName: MethodName.INIT, metadata: this._metadata, data: this._initData, }); }); } private initWalletEvent() { this.on(EndLessSDKEvent.NETWORK_CHANGE, (payload) => { this.initConfig({ network: payload.name, }); }); } changeNetwork(initData: EndlessWalletOptions) { this.message?.modal?.waitReady().then(() => { this.message?.sendMessage({ uuid: new Date().getTime().toString(), methodName: MethodName.NETWORK_CHANGE, metadata: this._metadata, data: { ...this._initData, ...initData, }, }); }); } getNetwork = (): Promise> => { return new Promise((resolve) => { if (!this._endlessConfig?.network) { resolve({ status: UserResponseStatus.REJECTED, message: 'Network not set' }); return; } resolve({ status: UserResponseStatus.APPROVED, args: getNetworkInfo(this._endlessConfig!.network), }); }); }; private getMetadata() { const iconLink = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]'); let iconUrl = iconLink?.getAttribute('href') || ''; if (iconUrl && !iconUrl.startsWith('http')) { iconUrl = new URL(iconUrl, window.location.origin).href; } this._metadata.title = window.document.title; this._metadata.url = window.location.href; this._metadata.origin = window.location.origin; this._metadata.icon = iconUrl; } open = (callback?: () => void) => { this.message?.modal?.waitReady().then(() => { this.message?.sendMessage({ uuid: new Date().getTime().toString(), methodName: MethodName.OPEN, metadata: this._metadata, data: {}, }); this.message?.modal?.openModal(() => { this.message?.emit(EndLessSDKEvent.OPEN, undefined); callback && callback(); }); }); }; close = (callback?: () => void) => { this.message?.modal?.waitReady().then(() => { this.message?.sendMessage({ uuid: new Date().getTime().toString(), methodName: MethodName.CLOSE, metadata: this._metadata, data: {}, }); this.message?.modal?.closeModal(() => { callback && callback(); }); }); }; request = (data: IRequestData, callback?: (data: unknown) => void) => { if (this.message?.modal) { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: data.method, metadata: this._metadata, data: data.data, }, callback ); this.message?.modal?.openModal(); } }; getAccount = (): Promise> => { return new Promise((resolve) => { this.message?.modal?.waitReady().then(() => { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.GETACCOUNT, metadata: this._metadata, data: {}, }, (data) => { if (data.account) { this.accountAddress = AccountAddress.fromBs58String(data.account); const res: UserResponse = { status: UserResponseStatus.APPROVED, args: { ...data }, }; resolve(res); } else { const res: UserResponse = { status: UserResponseStatus.REJECTED, message: data?.message || 'Wallet is not connected', }; resolve(res); } } ); }); }); }; connect = (callback?: (data: AccountInfo) => void): Promise> => { return new Promise((resolve) => { this.message?.modal?.waitReady().then(() => { PostMessage.promiseMap[MethodName.CONNECT] = { reject: () => { resolve(rejectedCloseData); }, }; this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.CONNECT, metadata: this._metadata, data: {}, }, (data) => { delete PostMessage.promiseMap[MethodName.CONNECT]; if (data.account) { this.accountAddress = AccountAddress.fromBs58String(data.account); const res: UserResponse = { status: UserResponseStatus.APPROVED, args: { ...data }, }; resolve(res); } else { const res: UserResponse = { status: UserResponseStatus.REJECTED, message: data?.message }; resolve(res); } if (callback) callback({ ...data }); } ); }); }); }; disconnect = (callback?: (data: unknown) => void): Promise => { return new Promise((resolve, reject) => { if (this.message?.modal?.readyState) { this.message.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.DISCONNECT, metadata: this._metadata, data: {}, }, (data: unknown) => { delete PostMessage.promiseMap[MethodName.DISCONNECT]; this.accountAddress = null; if (callback) callback(data); resolve(); } ); } else { reject('Wallet is not ready'); } }); }; createWallet = (callback?: (data: unknown) => void) => { if (this.message?.modal) { this.message.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.CREATE_WALLET, metadata: this._metadata, data: {}, }, (data: unknown) => { if (callback) callback(data); } ); } }; setWalletColorMode = (data: { colorMode: ColorMode }): void => { if (localStorage.getItem(ENDLESS_WALLET_WEB3_SDK_ENABLETHEME_TOGGLE_KEY) !== null) return; if (!['dark', 'light'].includes(data.colorMode)) { throw new Error("Invalid color mode"); } this.message?.modal?.waitReady().then(() => { this.message?.modal?.setWalletContainerColorMode({ colorMode: data.colorMode }); this.message?.sendMessage({ uuid: new Date().getTime().toString(), methodName: MethodName.SET_COLOR_MODE, metadata: this._metadata, data: data, }); localStorage.setItem(ENDLESS_WALLET_COLOR_MODE_KEY, data.colorMode); }); }; signMessage = ( data: EndlessSignMessageInput, callback?: (data: unknown) => void ): Promise> => { return new Promise((resolve) => { this.message?.modal?.waitReady().then(() => { PostMessage.promiseMap[MethodName.SIGN_MESSAGE] = { resolve, reject: () => { resolve(rejectedCloseData); }, }; this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.SIGN_MESSAGE, metadata: this._metadata, data: data, }, (res) => { delete PostMessage.promiseMap[MethodName.SIGN_MESSAGE]; if (res.status === 'success') { const result: UserResponse = { status: UserResponseStatus.APPROVED, args: res, }; resolve(result); } else { const result: UserRejection = { status: UserResponseStatus.REJECTED, message: res?.message }; resolve(result); } if (callback) callback(res); } ); }); }); }; on = (methodName: K, callback: (payload: EndLessSDKEventPayload) => void) => { if (this.message?.addListener) { this.message?.addListener(methodName, callback); } }; off = (methodName: K, callback?: (payload: EndLessSDKEventPayload) => void) => { if (this.message?.removeListener) { this.message?.removeListener(methodName, callback); } }; signAndSubmitTransaction = (data: EndlessSignAndSubmitTransactionInput): Promise> => { return new Promise(async (resolve) => { PostMessage.promiseMap[MethodName.SIGN_AND_SUBMIT_TRANSACTION] = { resolve, reject: () => { resolve(rejectedCloseData); }, }; if (!this.accountAddress) { const res = await this.getAccount(); if (res.status === UserResponseStatus.APPROVED) { this.accountAddress = AccountAddress.fromBs58String(res.args.account); } else { delete PostMessage.promiseMap[MethodName.SIGN_AND_SUBMIT_TRANSACTION]; const result: UserRejection = { status: UserResponseStatus.REJECTED, message: 'Wallet not linked' }; resolve(result); return; } } if (!this.accountAddress || !this._endless) { delete PostMessage.promiseMap[MethodName.SIGN_AND_SUBMIT_TRANSACTION]; const result: UserRejection = { status: UserResponseStatus.REJECTED, message: 'Wallet not linked' }; resolve(result); return; } const transaction = await this._endless.transaction.build.simple({ sender: this.accountAddress, data: data.payload, options: data.options }); const transactionData = { options: data.options || {}, type: EndlessSendTransactionType.SIGN_AND_SUBMIT, serializedTransaction: transaction.bcsToHex().toString(), }; if (this.message?.modal?.readyState) { this.message.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.SIGN_AND_SUBMIT_TRANSACTION, metadata: this._metadata, data: transactionData, }, (res) => { delete PostMessage.promiseMap[MethodName.SIGN_AND_SUBMIT_TRANSACTION]; if (res.status === 'success') { const result: UserResponse<{ hash: string }> = { status: UserResponseStatus.APPROVED, args: { hash: res.hash, }, }; resolve(result); } else { const result: UserRejection = { status: UserResponseStatus.REJECTED, message: res?.message }; resolve(result); } } ); } }); }; signTransaction = ( transactionHex: string, transactionType: EndlessWalletTransactionType ): Promise> => { return new Promise(async (resolve) => { PostMessage.promiseMap[MethodName.SIGN_TRANSACTION] = { resolve, reject: () => { resolve(rejectedCloseData); }, }; if (!this.accountAddress) { const res = await this.getAccount(); if (res.status === UserResponseStatus.APPROVED) { this.accountAddress = AccountAddress.fromBs58String(res.args.account); } else { delete PostMessage.promiseMap[MethodName.SIGN_TRANSACTION]; const result: UserRejection = { status: UserResponseStatus.REJECTED, message: 'Wallet not linked' }; resolve(result); return; } } if (!this.accountAddress || !this._endless) { delete PostMessage.promiseMap[MethodName.SIGN_TRANSACTION]; const result: UserRejection = { status: UserResponseStatus.REJECTED, message: 'Wallet not linked' }; resolve(result); return; } const transactionData = { type: EndlessSendTransactionType.SIGNATURE_ONLY, serializedTransaction: transactionHex, transactionType, }; if (this.message?.modal?.readyState) { this.message.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.SIGN_TRANSACTION, metadata: this._metadata, data: transactionData, }, (res) => { delete PostMessage.promiseMap[MethodName.SIGN_TRANSACTION]; if (res.status === 'success') { const result = { status: UserResponseStatus.APPROVED, args: res, }; resolve(result); } else { const result: UserRejection = { status: UserResponseStatus.REJECTED, message: res?.message }; resolve(result); } } ); } }); }; onAccountChange = (callback: (data: AccountInfo) => void) => { this.on(EndLessSDKEvent.ACCOUNT_CHANGE, callback); }; onNetworkChange = (callback: (data: NetworkInfo) => void) => { this.on(EndLessSDKEvent.NETWORK_CHANGE, callback); }; }