@luffalab/luffa-evm-sdk
Version:
luffa evm ts sdk
400 lines (377 loc) • 11.9 kB
text/typescript
/* 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);
};
}