diff --git a/src/components/chain/BlockChain.js b/src/components/chain/BlockChain.js index 646778a..8f79608 100644 --- a/src/components/chain/BlockChain.js +++ b/src/components/chain/BlockChain.js @@ -4,10 +4,12 @@ import { OkxWallet } from '@/components/chain/wallet/OkxWallet'; import {walletStore} from "@/store/wallet"; import WalletSelectModel from "@/components/chain/WalletSelectModel.vue"; import {createModal} from "@/utils/model.util"; +import {isTokenExpired, genRefreshToken, cfgChainId, switchEthereumChain} from "@/components/chain/utils" import {ImtblMarket} from "@/components/chain/Market"; import { ALL_PROVIDERS } from "@/configs/configchain"; import {Locker} from "@/components/chain/contract/Locker"; + export const allProviders = { 1: MetaMaskWallet, 2: OkxWallet, @@ -43,16 +45,26 @@ export class BlockChain { new PassportWallet(); } - async updateInfo({provider, accounts, token}) { - this.provider = provider - if (!token && !this.store.token) { - token = await this.wallet.getAccessToken(); - } else if (!token && this.store.token){ - token = this.store.token; + async updateInfo({provider, accounts}) { + this.web3Provider = provider + if (!this.store.token) { + const {token, refreshToken}= await this.wallet.getAccessToken(); + this.store.token = token + this.store.refreshToken = refreshToken + } else { + if (isTokenExpired(3600, this.store.token)) { + if (this.store.refreshToken && !isTokenExpired(300, this.store.refreshToken)) { + const {token, refreshToken} = await genRefreshToken(this.store.refreshToken); + this.store.token = token; + this.store.refreshToken = refreshToken; + } else { + const {token, refreshToken}= await this.wallet.getAccessToken(); + this.store.token = token + this.store.refreshToken = refreshToken + } + } } this.store.address = accounts[0]; - this.token = token - this.store.token = token; this.store.$persist(); this.market.updateProvider(provider); return provider; @@ -60,8 +72,8 @@ export class BlockChain { async restoreWallet(walletType) { this.wallet = new allProviders[walletType](); - let { provider, accounts, token } = await this.wallet.web3Provider(); - await this.updateInfo({provider, accounts, token}) + const { provider, accounts } = await this.wallet.web3Provider(); + await this.updateInfo({provider, accounts }) return provider; } @@ -71,8 +83,8 @@ export class BlockChain { const walletType = ALL_PROVIDERS[0].id this.wallet = new allProviders[walletType](); this.store.walletType = walletType; - const { provider, accounts, token } = await this.wallet.web3Provider(); - await this.updateInfo({ provider, accounts, token }) + const { provider, accounts } = await this.wallet.web3Provider(); + await this.updateInfo({ provider, accounts }) return provider; } const rewardModal = createModal(WalletSelectModel, {}); @@ -88,10 +100,30 @@ export class BlockChain { } } + get token() { + const suffix = (this.store.walletType == 2 || this.store.walletType == 1) ? '.cf' : '' + return this.store.token+suffix + } + async logout() { - this.token = ''; this.store.reset(); this.store.$persist(); await this.wallet.logout(); } + + async getChainId() { + return this.wallet.getChainId(); + } + /** + * 检查并切换到目标链, 各上链前须调用该方法 + */ + async checkAndChangeChain() { + let chainId = await this.getChainId(); + if (chainId !== cfgChainId) { + console.log(`current chain: ${chainId}, want: ${cfgChainId}`) + chainId = await switchEthereumChain(this.web3Provider.provider, cfgChainId); + } + } + + } diff --git a/src/components/chain/Market.js b/src/components/chain/Market.js index 65c6c3c..03b79f9 100644 --- a/src/components/chain/Market.js +++ b/src/components/chain/Market.js @@ -6,8 +6,9 @@ const NATIVE = 'NATIVE' const ERC20 = 'ERC20' export class ImtblMarket { - constructor() { + constructor(_chainInstance) { this.client = new orderbook.Orderbook({ baseConfig }); + this.bc = _chainInstance } updateProvider(provider) { @@ -21,6 +22,7 @@ export class ImtblMarket { * @returns */ async listListings(contractAddress){ + await this.bc.checkAndChangeChain(); const listOfListings = await this.client.listListings({ sellItemContractAddress: contractAddress, status: orderbook.OrderStatusName.ACTIVE, @@ -114,6 +116,7 @@ export class ImtblMarket { * @param {string} currencyAmount 出售价格, 单位 wei */ async beginSellERC721({contractAddress, tokenId, currencyAddress, currencyAmount, orderExpiry}) { + await this.bc.checkAndChangeChain(); const { preparedListing, orderSignature } = await this._prepareERC721Listing({contractAddress, tokenId, currencyAddress, currencyAmount, orderExpiry}); const order = await this._createListing(preparedListing, orderSignature, currencyAmount); @@ -125,6 +128,7 @@ export class ImtblMarket { * @param {*} listingId */ async beginBuy(listingId) { + await this.bc.checkAndChangeChain(); const fulfiller = await this.signer.getAddress(); // const fulfiller = marketAddress console.log(listingId,fulfiller) @@ -149,6 +153,7 @@ export class ImtblMarket { * @param { string[] } listingIds: listingId列表 */ async batchBuy(listingIds) { + await this.bc.checkAndChangeChain(); const fulfiller = await this.signer.getAddress(); // console.log(listingIds, marketAddress,'---') // return @@ -211,6 +216,7 @@ export class ImtblMarket { * @returns */ async cancelOrdersOnChain(listingIds) { + await this.bc.checkAndChangeChain(); const offerer = await this.signer.getAddress(); const { cancellationAction } = await this.client.cancelOrdersOnChain( listingIds, diff --git a/src/components/chain/WalletSelectModel.vue b/src/components/chain/WalletSelectModel.vue index b2983d1..483fef5 100644 --- a/src/components/chain/WalletSelectModel.vue +++ b/src/components/chain/WalletSelectModel.vue @@ -35,10 +35,6 @@ import { computed } from "vue"; import { ALL_PROVIDERS } from "@/configs/configchain"; import { allProviders } from "@/components/chain/BlockChain" -import {walletStore} from "@/store/wallet"; - -const localWalletStore = walletStore() - const props = defineProps({ visible: Boolean, close: Function, diff --git a/src/components/chain/contract/Locker.js b/src/components/chain/contract/Locker.js index e15ff38..962b78a 100644 --- a/src/components/chain/contract/Locker.js +++ b/src/components/chain/contract/Locker.js @@ -21,18 +21,19 @@ export class Locker { async lock(nft, tokenIds) { // call single method with abi and address console.log('lock nft', nft, tokenIds) - const nftContract = new ethers.Contract(nft, erc721Abi, this.bc.provider.getSigner()) + await this.bc.checkAndChangeChain(); + const nftContract = new ethers.Contract(nft, erc721Abi, this.bc.web3Provider.getSigner()) for (let tokenId of tokenIds) { const addressApproval = await nftContract.getApproved(tokenId) if ((addressApproval || "").toLowerCase() != lockAddress.toLowerCase()) { const resApproval = await nftContract.approve(lockAddress, tokenId); - await this.bc.provider.waitForTransaction(resApproval.hash) + await this.bc.web3Provider.waitForTransaction(resApproval.hash) console.debug('approve', resApproval.hash) } } - const contract = new ethers.Contract(lockAddress, lockAbi, this.bc.provider.getSigner()) + const contract = new ethers.Contract(lockAddress, lockAbi, this.bc.web3Provider.getSigner()) const res = await contract.lock(nft, tokenIds) - await this.bc.provider.waitForTransaction(res.hash) + await this.bc.web3Provider.waitForTransaction(res.hash) return res.hash } } \ No newline at end of file diff --git a/src/components/chain/utils.js b/src/components/chain/utils.js index 7146039..789fbbb 100644 --- a/src/components/chain/utils.js +++ b/src/components/chain/utils.js @@ -3,14 +3,27 @@ export const WALLET_API_HOST_RELEASE = 'https://wallet.cebggame.com'; const apiBase = process.env.NODE_ENV === 'production' ? WALLET_API_HOST_RELEASE : WALLET_API_HOST_TEST; import { SiweMessage } from './common/SiweMessage'; import { ethers } from 'ethers'; +import assert from 'assert' +import { AllChains } from "@/configs/allchain"; +import { Deferred } from '@/utils/promise.util'; -const loginWithSignature = async(message, signature) => { - const url = `${apiBase}/wallet/login/general`; - const data = { - channel: 13, - code: signature, - message +export const cfgChainId = parseInt(import.meta.env.VUE_APP_NET_ID); + +assert(cfgChainId, 'VUE_APP_NET_ID not configured'); + +let chainCfg; +for (const d of AllChains) { + if (d.id === cfgChainId) { + chainCfg = d; + break; } +} +assert(chainCfg, 'chain config not found'); + +export const currentChainCfg = chainCfg; + + +const request = async(url, data, method = 'POST') => { let headers = { 'Content-Type': 'application/json', 'api_version': 2, @@ -18,12 +31,33 @@ const loginWithSignature = async(message, signature) => { 'api_env': process.env.NODE_ENV === 'production' ? 'release' : 'dev' }; return fetch(url, { - method: "POST", + method, body: JSON.stringify(data), headers }).then(res => res.json()); } +const loginWithSignature = async(message, signature) => { + const url = `${apiBase}/wallet/login/general`; + const data = { + channel: 13, + code: signature, + message, + nb: 1 + } + return request(url, data); +} + +export const genRefreshToken = async(refreshToken) => { + const url = `${apiBase}/wallet/refresh_token`; + const data = { refreshToken } + const res = await request(url, data); + if (res.errcode) { + throw new Error(res.errmsg); + } + return res.data; +} + const utf8ToHex = (str) => { return '0x' + Buffer.from(str).toString('hex'); } @@ -55,5 +89,100 @@ export const signLogin = async (provider, address) => { if (res.errcode) { throw new Error(res.errmsg); } - return res.data?.token; + return res.data; +} + + +export function parseTokenData(token) { + if (!token) { + return {}; + } + let datas = token.split("."); + if (datas.length < 2) { + return {}; + } + try { + return JSON.parse(window.atob(datas[1])); + } catch (err) { + return {}; + } +} + +/** + * check if token expired + * @param token jwt token string + * @param fixed fixed seconds + * @returns + */ +export function isTokenExpired(fixed, token) { + if (!token) { + return true; + } + let data = parseTokenData(token); + if (!data.exp) { + return true; + } + let now = Date.now() / 1000 | 0; + return data.exp < now - fixed; +} + +/** + * number to hex string + * @param {number} chainId + * @return {string} + */ +export function toHexChainId(chainId) { + return '0x' + chainId.toString(16) +} + +export const switchEthereumChain = async (provider, targetChainId) => { + const hexChainId = toHexChainId(targetChainId) + const deferred = new Deferred(); + const onChainChange = (chainId) => { + const chainIdNum = parseInt(chainId) + console.log('switchEthereumChain: ', chainIdNum) + provider.removeListener('chainChanged', onChainChange) + if (chainIdNum !== targetChainId) { + deferred.reject(new Error('switch chain failed')) + return + } + deferred.resolve(chainIdNum) + } + provider.on('chainChanged', onChainChange) + try { + await provider.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: hexChainId }] + }) + console.log('success send switch chain request') + } catch (e) { + console.log('error switch chain: ', e) + if (e.code === 4902 || e.message.indexOf('Unrecognized chain ID') >= 0) { + try { + const data = chainCfg + await provider.request({ + method: 'wallet_addEthereumChain', + params: [ + { + chainId: hexChainId, + chainName: data.name, + nativeCurrency: { + name: data.symbol, + symbol: data.symbol, + decimals: data.decimals || 18 + }, + blockExplorerUrls: [data.explorerurl], + rpcUrls: [data.rpc] + } + ] + }) + console.log('success send add chain request') + } catch (addError) { + console.error('error add chain: ', addError) + provider.removeListener('chainChanged', onChainChange) + deferred.reject(addError) + } + } + } + return deferred.promise } \ No newline at end of file diff --git a/src/components/chain/wallet/MetaMaskWallet.js b/src/components/chain/wallet/MetaMaskWallet.js index e03cedd..b995759 100644 --- a/src/components/chain/wallet/MetaMaskWallet.js +++ b/src/components/chain/wallet/MetaMaskWallet.js @@ -21,8 +21,8 @@ export class MetaMaskWallet{ async getAccessToken() { const accounts = await this.nativeProvider.request({ method: "eth_requestAccounts" }); - const token = await signLogin(this.nativeProvider, accounts[0]); - return token + const { token, refreshToken } = await signLogin(this.nativeProvider, accounts[0]); + return { token, refreshToken } } async logout() { @@ -35,4 +35,9 @@ export class MetaMaskWallet{ ] }); } + + async getChainId() { + const chainId = await this.nativeProvider.request({ method: "eth_chainId" }); + return parseInt(chainId); + } } \ No newline at end of file diff --git a/src/components/chain/wallet/OkxWallet.js b/src/components/chain/wallet/OkxWallet.js index e390c8c..fdc18e0 100644 --- a/src/components/chain/wallet/OkxWallet.js +++ b/src/components/chain/wallet/OkxWallet.js @@ -21,11 +21,16 @@ export class OkxWallet{ async getAccessToken() { const accounts = await this.nativeProvider.request({ method: "eth_requestAccounts" }); - const token = await signLogin(this.nativeProvider, accounts[0]); - return token + const { token, refreshToken } = await signLogin(this.nativeProvider, accounts[0]); + return { token, refreshToken } } async logout() { await this.nativeProvider.request({ method: 'wallet_disconnect' }); } + + async getChainId() { + const chainId = await this.nativeProvider.request({ method: "eth_chainId" }); + return parseInt(chainId); + } } \ No newline at end of file diff --git a/src/components/chain/wallet/PassportWallet.js b/src/components/chain/wallet/PassportWallet.js index 29f3b4e..cd75308 100644 --- a/src/components/chain/wallet/PassportWallet.js +++ b/src/components/chain/wallet/PassportWallet.js @@ -1,5 +1,6 @@ import { config, passport, orderbook, checkout } from '@imtbl/sdk'; import { providers } from 'ethers'; +import { cfgChainId } from '@/components/chain/utils.js'; const environment = process.env.NODE_ENV === 'production' ? config.Environment.PRODUCTION : config.Environment.SANDBOX; const publishableKey = import.meta.env.VUE_APP_PASSPORT_PUBLISHABLE_KEY @@ -69,12 +70,16 @@ export class PassportWallet { async getAccessToken() { - return await this.passportInstance.getAccessToken(); + const token = await this.passportInstance.getAccessToken(); + return { token } } async logout() { await this.passportInstance.logout(); await this.passportInstance.logoutSilentCallback(logoutRedirectUri); } - + + async getChainId() { + return Promise.resolve(cfgChainId) + } } diff --git a/src/configs/allchain.ts b/src/configs/allchain.ts index a543128..08cad38 100644 --- a/src/configs/allchain.ts +++ b/src/configs/allchain.ts @@ -282,5 +282,21 @@ export const AllChains = [ id: 1666700000, symbol: 'ONE', explorerurl: 'https://explorer.harmony.one' + }, + { + name: 'Immutable zkEVM', + type: 'Mainnet', + rpc: 'https://rpc.immutable.com', + id: 13371, + symbol: 'IMX', + explorerurl: 'https://explorer.immutable.com' + }, + { + name: 'Immutable zkEVM Testnet', + type: 'Testnet', + rpc: 'https://rpc.testnet.immutable.com', + id: 13473, + symbol: 'tIMX', + explorerurl: 'https://explorer.testnet.immutable.com' } ] diff --git a/src/store/wallet.js b/src/store/wallet.js index 9eec881..d7428db 100644 --- a/src/store/wallet.js +++ b/src/store/wallet.js @@ -8,6 +8,7 @@ export const walletStore = defineStore( const address = ref(); const chainId = ref(); const token = ref(); + const refreshToken = ref(); const showAddress = computed(() => { if (address.value.length > 10) { @@ -21,12 +22,14 @@ export const walletStore = defineStore( address.value = ''; chainId.value = ''; token.value = ''; + refreshToken.value = ''; } return { walletType, address, chainId, token, + refreshToken, showAddress, reset, };