UNPKG

@luffalab/luffa-evm-sdk

Version:

luffa evm ts sdk

400 lines (377 loc) 11.9 kB
/* import { Buffer } from 'buffer'; globalThis.Buffer = Buffer; */ import { PostMessage } from './message'; import { v4 as uuidv4 } from 'uuid'; import { IInitData, UserResponse, AccountInfo, UserResponseStatus, UserRejection, NetworkInfo, ChainData, } from './types'; // web and WebView /// #if BUILD_PLATFORM !== 'MINIPROGRAM' /// #endif import { getChain, getChainIdByName, isApproveTx, isLuffa, isLuffaMiniProgram, isLuffaMiniProgramWebview, normalizeMessageForDisplay } from './utils'; import { EvmSDKEvent, EvmSDKEventPayload, EvmSDKEventType, IRequestData } from './message/types'; export { isLuffa, isLuffaMiniProgram, isLuffaMiniProgramWebview } from './utils'; export interface Metadata { title: string; url: string; origin: string; icon: string; gameId: string; userId: string; walletAddress: string; } export { EvmSDKEvent } from './message/types'; export { UserResponseStatus } from './types'; export type { UserResponse, AccountInfo } from './types'; export enum MethodName { CONNECT = 'connect', GETACCOUNT = 'getAccount', DISCONNECT = 'disconnect', CURRENT_CHAIN = 'currentChain', NETWORK_CHANGE = 'luffa_switchChain', SIGN_MESSAGE = 'signMessage', SEND_TRANSACTION = 'sendTransaction', SIGN_AND_SUBMIT_TRANSACTION = 'signAndSubmitTransaction', SIGN_TRANSACTION = 'signTransaction', SIGN_BUILD_TRANSACTION = 'signBuildTransaction', EVM_APPROVE = 'evmApprove', ACCOUNT_CHANGE = 'accountChange', } export class LuffaEvmSdk { static readonly version: string = '1.0.7'; private static _instance: LuffaEvmSdk; private message: PostMessage | null = null; private _metadata: Metadata = {} as Metadata; private _initData: IInitData = {} as IInitData; private accountAddress: string | null = null; static getIninData = (): IInitData => { if (LuffaEvmSdk._instance) { return LuffaEvmSdk._instance._initData; } else { return {} as IInitData; } }; static getAccountAddress = () => { if (LuffaEvmSdk._instance) { return LuffaEvmSdk._instance.accountAddress; } else { return null; } }; static setAccountAddress = (accountAddress: string | null) => { if (LuffaEvmSdk._instance) { LuffaEvmSdk._instance.accountAddress = accountAddress; } }; constructor(initData: IInitData) { if (LuffaEvmSdk._instance) return LuffaEvmSdk._instance; this.message = new PostMessage(); this.getMetadata(); this.initConfig(initData); LuffaEvmSdk._instance = this; } private initConfig(initData: IInitData) { this._initData.callbackWalletName = 'evmWallet'; this._initData.network = initData.network; const evmProvider = Object.freeze({ isMetaMask: false, request: this.request, on: this.on, removeListener: this.off, }); Object.defineProperty(globalThis, 'ethereum', { value: evmProvider, writable: false, }); function announceProvider() { const info = { uuid: uuidv4(), name: 'LuffaEvmWallet', icon: '', rdns: 'org.luffa.wallet', }; window.dispatchEvent( new CustomEvent('eip6963:announceProvider', { detail: Object.freeze({ info, provider: evmProvider }), }) ); } window.addEventListener('eip6963:requestProvider', () => { announceProvider(); }); announceProvider(); } async sendTransaction(params: any) { if (!this.accountAddress) { const res = await this.connect(); this.accountAddress = res[0]; } console.log('sendTransaction params: ', params); const { isApprove, spender } = isApproveTx(params.params[0].data); console.log('isApprove: ', isApprove, spender); if (isApprove) { params.to = spender; return this.signAndSubmitTransaction(params, MethodName.EVM_APPROVE); } else { return this.signAndSubmitTransaction(params); } } private getMetadata() { if (!window) { return; } 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; } request = (data: IRequestData, callback?: (data: unknown) => void) => { switch (data.method) { case 'eth_requestAccounts': return this.connect(); case 'eth_accounts': return this.getAccount(); case 'eth_chainId': return this.currentChain(); case 'wallet_switchEthereumChain': return this.luffa_switchChain(data); case 'eth_sendTransaction': return this.sendTransaction(data); case 'personal_sign': return this.signMessage(data); case 'wallet_revokePermissions': return this.disconnect(); default: { throw new Error('Unsupported method: ' + data.method); } } }; luffa_switchChain = (data: IRequestData): Promise<string | { status: string }> => { return new Promise((resolve) => { const network = getChain(data?.params[0]?.chainId); this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.NETWORK_CHANGE, metadata: this._metadata, data: { targetNet: network }, }, (res) => { if (res?.network) { this._initData.network = res.network; } const result = { ...res, }; resolve(result); } ); }); }; currentChain = (): Promise<string | { status: string }> => { return new Promise((resolve) => { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.CURRENT_CHAIN, metadata: this._metadata, data: {}, }, (res) => { if (res?.network) { this._initData.network = res?.network; const result = `0x${getChainIdByName(res?.network)?.toString(16)}`; resolve(result); } else { const result = { status: 'Rejected' }; resolve(result); } } ); }); }; getAccount = (): Promise<string[]> => { return new Promise((resolve) => { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.GETACCOUNT, metadata: this._metadata, data: {}, }, (data) => { if (data?.account) { this.accountAddress = data.account; resolve([data.account]); } else { resolve([]); } } ); }); }; connect = (): Promise<[string]> => { return new Promise((resolve, reject) => { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.CONNECT, metadata: this._metadata, data: {}, }, (data) => { let res; if (data?.account) { this.accountAddress = data.account; res = [data.account] as [string]; resolve(res); } else { res = { code: 4001, message: 'User rejected the request.', }; reject(res); } } ); }); }; disconnect = (callback?: (data: unknown) => void): Promise<void> => { return new Promise((resolve, reject) => { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.DISCONNECT, metadata: this._metadata, data: {}, }, (data: unknown) => { this.accountAddress = null; if (callback) callback(data); resolve(); } ); }); }; on = <K extends EvmSDKEventType>(methodName: K, callback: (payload: EvmSDKEventPayload<K>) => void) => { if (this.message?.addListener) { this.message?.addListener(methodName, callback); } }; off = <K extends EvmSDKEventType>(methodName: K, callback?: (payload: EvmSDKEventPayload<K>) => void) => { if (this.message?.removeListener) { this.message?.removeListener(methodName, callback); } }; signAndSubmitTransaction = async ( data: IRequestData, methodName = MethodName.SIGN_AND_SUBMIT_TRANSACTION ): Promise<any> => { if (!this.accountAddress) { const res = await this.connect(); this.accountAddress = res[0]; } return new Promise(async (resolve) => { this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: methodName, metadata: this._metadata, data: data.params[0], }, (res) => { if (res?.hash) { resolve(res.hash); } else { resolve({ code: 4001, message: res?.message, }); } } ); }); }; signTransaction = (params: any, methodName = MethodName.SIGN_BUILD_TRANSACTION): Promise<any> => { return new Promise(async (resolve, reject) => { const data: { raw_data_hex: string; to?: string } = { raw_data_hex: params.raw_data_hex, }; if (params.to) { data.to = params.to; } this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName, metadata: this._metadata, data, }, (res) => { const signature = res?.signature?.split(',') || []; console.log('luffa evm signature: ', signature); if (Array.isArray(signature) && signature.length > 0) { const result = { ...params, signature, }; resolve(result); } else { reject('Confirmation declined by user'); } } ); }); }; signMessage = ( data: IRequestData, callback?: (data: unknown) => void ): Promise<UserResponse<any>> => { return new Promise(async (resolve) => { if (!this.accountAddress) { await this.connect(); } this.message?.sendMessage( { uuid: new Date().getTime().toString(), methodName: MethodName.SIGN_MESSAGE, metadata: this._metadata, data: { message: normalizeMessageForDisplay(data.params[0]) } }, (res) => { console.log('signMessage res: ', res); if (res?.signature) { const result: UserResponse<any> = { status: UserResponseStatus.APPROVED, args: res, }; resolve(result); } else { const result: UserRejection = { status: UserResponseStatus.REJECTED, message: res?.message }; resolve(result); } if (callback) callback(res); } ); }); }; onAccountChange = (callback: (data: AccountInfo) => void) => { this.on(EvmSDKEvent.ACCOUNT_CHANGE, callback); }; onNetworkChange = (callback: (data: NetworkInfo) => void) => { this.on(EvmSDKEvent.NETWORK_CHANGE, callback); }; }