import { Blockchain, BoraPortalConnectRequest, BoraPortalConnectStatusResponse, Env, FaceError, FaceLoginResponse, HomeOptions, JsonRpcMethod, JsonRpcRequestPayload, JsonRpcResponsePayload, LoginProviderType, LoginWithAccessTokenRequest, LoginWithIdTokenRequest, Network, ProviderRpcError, unsupportedChainError, } from '@haechi-labs/face-types'; import { IFRAME_URL, isEthlikeNetwork, isSupportedNetwork, networkToBlockchain, } from '@haechi-labs/shared'; import { PublicKey } from '@solana/web3.js'; import { BigNumber, ethers } from 'ethers'; import { getBundleId } from 'react-native-device-info'; import { v4 as uuidv4 } from 'uuid'; import { Face } from './Face'; import eventEmitter from './utils/Events'; import { getEthlikeChainIdFromNetwork, getValidNetwork } from './utils/network'; import { Webview } from './Webview'; import { Xhr } from './Xhr'; const DEFAULT_ETH_GAS_PRICE = BigNumber.from(100000).toHexString(); type InternalParams = { apiKey: string; face: Face; scheme: string; network: Network; iframeUrl?: string; env?: Env; }; export class Internal { private readonly face: Face; private network: Network; private blockchain: Blockchain; private currentUser: FaceLoginResponse | null = null; private apiKey: string; private serverHostUrl: string; public xhr: Xhr; public readonly webview: Webview; constructor({ apiKey, network, env, iframeUrl, face, scheme }: InternalParams) { this.face = face; this.network = network; this.blockchain = this.getBlockchainFromNetwork(network); const _env = env ?? Env.ProdMainnet; this.webview = new Webview( apiKey, network, this.blockchain, _env, this.getWebviewUrl(_env, iframeUrl), scheme ); this.apiKey = apiKey; this.serverHostUrl = this.getServerHostUrl(_env); this.xhr = new Xhr(this.serverHostUrl, { 'X-Face-Dapp-Api-Key': this.apiKey, 'X-Face-Dapp-Api-Hostname': getBundleId(), }); } async getAddresses(): Promise { if (!this.currentUser?.wallet?.address) { return []; } if (this.blockchain === Blockchain.NEAR) { return [this.currentUser.wallet.eddsaPublicKey?.substring(2) || '']; } else if (this.blockchain === Blockchain.SOLANA) { const publicKey = new PublicKey( Buffer.from(this.currentUser.wallet.eddsaPublicKey?.substring(2) || '', 'hex') ); return [publicKey.toBase58() || '']; } return [this.currentUser.wallet.address]; } // SDK에서 estimateGas를 호출하는 대부분의 경우는 트랜잭션을 보내는 상황에서 ethers가 호출하는 것 // 이 상황에서는 0으로 리턴하고 iframe 안에서 덮어씌우는 게 유저 경험이 더 좋음 async estimateGas() { return 0; } async getBalance(address: string, contractAddress?: string): Promise { if (contractAddress) { const callData = await this.encodeData( ['function balanceOf(address owner) view returns (uint256)'], 'balanceOf', [address] ); const result = await this.sendRpc({ method: JsonRpcMethod.eth_call, params: [ { to: contractAddress, data: callData, }, 'latest', ], }); return BigNumber.from(result); } return BigNumber.from( await this.sendRpc({ method: JsonRpcMethod.eth_getBalance, params: [address, 'latest'], }) ); } async ownerOf(contractAddress: string, tokenId: string): Promise { const callData = await this.encodeData( ['function ownerOf(uint256 tokenId) view returns (address)'], 'ownerOf', [tokenId] ); const result = await this.sendRpc({ method: JsonRpcMethod.eth_call, params: [ { to: contractAddress, data: callData, }, 'latest', ], }); return ('0x' + (result as string).substring(26)).toLowerCase(); } async logout(): Promise { await this.webview.openWebview({ method: JsonRpcMethod.face_logOut }); this.currentUser = null; } async getCurrentUser(): Promise { return this.currentUser; } async isLoggedIn(): Promise { return !!this.currentUser; } async loginWithCredential(): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_logInSignUp }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } async directSocialLogin(provider: LoginProviderType): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_directSocialLogin, params: [provider], }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } async loginWithIdToken( loginWithIdTokenRequest: LoginWithIdTokenRequest ): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_loginWithIdToken, params: [loginWithIdTokenRequest], }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } async loginWithAccessToken( loginWithIdTokenRequest: LoginWithAccessTokenRequest ): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_loginWithIdToken, params: [loginWithIdTokenRequest], }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } async boraLogin( boraRequest: BoraPortalConnectRequest, providers?: LoginProviderType[] ): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_logInSignUp, params: [boraRequest, providers], }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } async boraLoginWithIdToken( boraRequest: BoraPortalConnectRequest, loginWithIdTokenRequest: LoginWithIdTokenRequest ): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_loginWithIdToken, params: [loginWithIdTokenRequest, boraRequest], }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } async boraDirectSocialLogin( boraRequest: BoraPortalConnectRequest, provider: LoginProviderType ): Promise { const [response] = await this.webview.openWebview({ method: JsonRpcMethod.face_directSocialLogin, params: [provider, boraRequest], }); if (response === null) { return null; } this.currentUser = response.result; return response.result; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async openWalletConnect(name: string, url: string): Promise { // TODO // const [response] = await this.webview.openWebview({ // method: JsonRpcMethod.face_openWalletConnect, // params: [name, url], // }); } async openHome(options?: HomeOptions): Promise { await this.webview.openWebview({ method: JsonRpcMethod.face_openHome, params: [options], }); } async sendTrasction(rpcPayload: JsonRpcRequestPayload): Promise { const transactionRequestId = uuidv4(); rpcPayload.params[1] = transactionRequestId; await this.openWebview(rpcPayload); const response = await this.xhr.request( 'GET', '/transactions/requests/' + transactionRequestId ); if (response?.code === 'MD0011') { throw new Error('Transaction is canceled.'); } return response.transactionId; } async openWebview( rpcPayload: JsonRpcRequestPayload ): Promise<[JsonRpcResponsePayload | null, JsonRpcRequestPayload]> { return await this.webview.openWebview(rpcPayload); } async sendRpc( rpcPayload: JsonRpcRequestPayload ): Promise['result']> { if (!rpcPayload.id) { rpcPayload.id = parseInt(String(Math.random() * 100)); } rpcPayload.jsonrpc = '2.0'; rpcPayload.blockchainNetwork = this.network; const blockchain = this.network ? networkToBlockchain(this.network) : this.blockchain; // @ts-expect-error blockchain 하위호환 rpcPayload.blockchain = blockchain; return (await this.xhr.request('POST', '/rpc', { body: rpcPayload })).result; } getBlockchainFromNetwork(network?: Network): Blockchain { switch (network) { case Network.ETHEREUM: case Network.SEPOLIA: return Blockchain.ETHEREUM; case Network.POLYGON: case Network.MUMBAI: return Blockchain.POLYGON; case Network.BNB_SMART_CHAIN: case Network.BNB_SMART_CHAIN_TESTNET: return Blockchain.BNB_SMART_CHAIN; case Network.KLAYTN: case Network.BAOBAB: return Blockchain.KLAYTN; case Network.SOLANA: case Network.SOLANA_DEVNET: return Blockchain.SOLANA; case Network.BORA: case Network.BORA_TESTNET: return Blockchain.BORA; case Network.NEAR: case Network.NEAR_TESTNET: return Blockchain.NEAR; case Network.HEDERA: case Network.HEDERA_TESTNET: return Blockchain.HEDERA; case Network.KROMA: case Network.KROMA_SEPOLIA: return Blockchain.KROMA; case Network.LINEA: case Network.LINEA_GOERLI: return Blockchain.LINEA; default: return Blockchain.ETHEREUM; } } getWebviewUrl(env?: Env, iframeUrl?: string): string { if (iframeUrl != null) { return iframeUrl; } switch (env) { case Env.Local: return IFRAME_URL.Local; case Env.Dev: return IFRAME_URL.Dev; case Env.StageTest: return IFRAME_URL.StageTestnet; case Env.ProdTest: return IFRAME_URL.ProdTestnet; case Env.StageMainnet: return IFRAME_URL.StageMainnet; case Env.ProdMainnet: return IFRAME_URL.ProdMainnet; default: return IFRAME_URL.ProdMainnet; } } getServerHostUrl(env?: Env): string { switch (env) { case Env.Local: return 'http://localhost:8881/v1'; case Env.Dev: return 'https://api.dev.facewallet.xyz/v1'; case Env.StageTest: return 'https://api.stage-test.facewallet.xyz/v1'; case Env.StageMainnet: return 'https://api.stage.facewallet.xyz/v1'; case Env.ProdTest: return 'https://api.test.facewallet.xyz/v1'; case Env.ProdMainnet: return 'https://api.facewallet.xyz/v1'; } return 'https://api.test.facewallet.xyz/v1'; } async decodeData(serializedTx: string, abi: string[]) { const ethersInterface = new ethers.utils.Interface(abi); const { name, args } = ethersInterface.parseTransaction({ data: serializedTx }); return { name, args }; } async encodeData(abi: string[], functionFragment: string, args?: unknown[]) { const ethersInterface = new ethers.utils.Interface(abi); return ethersInterface.encodeFunctionData(functionFragment, args); } async switchNetwork(network: Network | number | string) { try { const _network = getValidNetwork(network); if (!isSupportedNetwork(_network, ['react-native'])) { throw unsupportedChainError(); } const blockchain = this.getBlockchainFromNetwork(_network); this.blockchain = blockchain; this.webview.setBlockchain(blockchain); this.webview.setNetwork(_network); this.network = _network; if (isEthlikeNetwork(this.network)) { const chainId = getEthlikeChainIdFromNetwork(_network); eventEmitter.emit('chainChanged', chainId); } } catch (e) { if (isEthlikeNetwork(this.network)) { const error: ProviderRpcError = { name: (e as FaceError).name, message: (e as FaceError).message, code: 4901, }; eventEmitter.emit('disconnect', error); } throw e; } } async boraConnect( request: BoraPortalConnectRequest ): Promise { this.webview.allowBlockchain([Blockchain.BORA]); const [response] = await this.openWebview({ method: JsonRpcMethod.bora_connect, params: [request], }); return response?.result ?? null; } getNetwork(): Network { return this.network; } }