diff --git a/.env.dev b/.env.dev index 32a7b57..c8d3fe2 100644 --- a/.env.dev +++ b/.env.dev @@ -1,4 +1,9 @@ VUE_APP_BASE_API='https://market.cebg.games' VUE_APP_BASE_API2='https://invitation.counterfire.games' //VUE_APP_BASE_API2='http://192.168.100.83:3000/' -VUE_APP_GPAL_API='https://game2006api.cebggame.com/' \ No newline at end of file +VUE_APP_GPAL_API='https://game2006api.cebggame.com/' +VUE_APP_PASSPORT_PUBLISHABLE_KEY=pk_imapik-test-8c2FAlWxWAoRITk1v9rH +VUE_APP_PASSPORT_REDIRECT_URI=http://localhost:4000/redirect +VUE_APP_PASSPORT_LOGOUT_URI=http://localhost:4000/ +VUE_APP_PASSPORT_CLIENT_ID=eTmUah69p7ZdRhRYzBta6lZRKXXeXDYj +VUE_APP_PASSPORT_MARKET_ADDRESS=0x7d117aA8BD6D31c4fa91722f246388f38ab1942c \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f268596 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +node_modules +artifacts +cache +coverage* +gasReporterOutput.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..32f2e64 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": false, + "explicitTypes": "always" +} diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..64fea22 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "node", + "allowJs": true, + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json index 4cfb846..274a5b6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "ant-design-vue": "^4.2.3", "axios": "^1.3.3", "buffer": "^6.0.3", + "ethers": "^5.7.2", "gsap": "^3.11.5", "js-cookie": "^3.0.1", "normalize.css": "^8.0.1", diff --git a/src/components/layout/NavBar.vue b/src/components/layout/NavBar.vue index 01c17e0..c4c6eaa 100644 --- a/src/components/layout/NavBar.vue +++ b/src/components/layout/NavBar.vue @@ -66,6 +66,7 @@ +
@@ -78,7 +79,7 @@ import { hasMetamask } from "@/utils/chain.util"; import { useRouter, useRoute } from "vue-router"; import ChainModel from "@/components/home/ChainModel.vue"; import { useCopyToClipboard } from "./../../hooks/useCopyToClipboard"; -import { getConnect } from "@/wallet/passPort" +import { PassportWallet } from "@/wallet/passPort" const AppModule = useAppStore(); const router = useRouter(); @@ -226,8 +227,13 @@ watchEffect(() => { const immuTableLogin = async () => { // console.log('----') try{ - const walletLogin = await getConnect() + const walletLogin = await new PassportWallet().connect() console.log(walletLogin) + // const list = await new PassportWallet().beginSellERC721({ + // contractAddress: '', + // tokenId: '' + // }); + // console.log(list) } catch (e) { console.log(e); } diff --git a/src/utils/singleton.js b/src/utils/singleton.js new file mode 100644 index 0000000..21d2cd9 --- /dev/null +++ b/src/utils/singleton.js @@ -0,0 +1,16 @@ +export const createSingleton = (Class) => { + let instance; + + return new Proxy(Class, { + construct(target, args, newTarget) { + // Skip proxy for children + if (target.prototype !== newTarget.prototype) { + return Reflect.construct(target, argumentsList, newTarget) + } + if (!instance) { + instance = Reflect.construct(target, args, newTarget); + } + return instance; + }, + }); +}; diff --git a/src/wallet/passPort.js b/src/wallet/passPort.js index 98fd987..0f1853a 100644 --- a/src/wallet/passPort.js +++ b/src/wallet/passPort.js @@ -1,28 +1,274 @@ -import { config, passport } from '@imtbl/sdk'; +import { config, passport, orderbook, checkout } from '@imtbl/sdk'; +import { createSingleton } from '@/utils/singleton'; +import { providers } from 'ethers'; -const passportInstance = new passport.Passport({ - baseConfig: { - environment: config.Environment.SANDBOX, // or Environment.PRODUCTION - publishableKey: 'wc:b348768c53d32b69062adcc7f67b263f771564c39edc06acc4cb3e9ee561d415@2?relay-protocol=irn&symKey=b8302aed25de3f9e3485eb8601d27c52779ab28cd8b1398312aa18be01cebbe8', // replace with your publishable API key from Hub - }, - clientId: '', // replace with your client ID from Hub - redirectUri: 'http://192.168.100.216:4000/', // replace with one of your redirect URIs from Hub - logoutRedirectUri: 'http://192.168.100.216:4000/', // replace with one of your logout URIs from Hub - audience: 'platform_api', - scope: 'openid offline_access email transact', - popupOverlayOptions: { - disableGenericPopupOverlay: false, // Set to true to disable the generic pop-up overlay - disableBlockedPopupOverlay: false, // Set to true to disable the blocked pop-up overlay +const environment = process.env.NODE_ENV === 'production' ? config.Environment.PRODUCTION : config.Environment.SANDBOX; +const publishableKey = import.meta.env.VUE_APP_PASSPORT_PUBLISHABLE_KEY +const clientId = import.meta.env.VUE_APP_PASSPORT_CLIENT_ID +const redirectUri = import.meta.env.VUE_APP_PASSPORT_REDIRECT_URI +const logoutRedirectUri = import.meta.env.VUE_APP_PASSPORT_LOGOUT_URI +const marketAddress = import.meta.env.VUE_APP_PASSPORT_MARKET_ADDRESS + +const NATIVE = 'NATIVE' +const ERC20 = 'ERC20' + +const baseConfig = { environment, publishableKey } +class LPassportWallet { + constructor() { + this.passportInstance = new passport.Passport({ + baseConfig, + clientId, // replace with your client ID from Hub + redirectUri, // replace with one of your redirect URIs from Hub + logoutRedirectUri, // replace with one of your logout URIs from Hub + audience: 'platform_api', + scope: 'openid offline_access email transact', + popupOverlayOptions: { + disableGenericPopupOverlay: false, // Set to true to disable the generic pop-up overlay + disableBlockedPopupOverlay: false, // Set to true to disable the blocked pop-up overlay + } + }); + this.passportInstance.loginCallback(); + this.client = new orderbook.Orderbook({ baseConfig }); } -}); + async initWidget() { + const checkoutSDK = new checkout.Checkout({ + baseConfig, + passport: this.passportInstance, + bridge: { enable: true }, + swap: { enable: true }, + onRamp: { enable: true } + }); + + const widgets = await checkoutSDK.widgets({ + config: { theme: checkout.WidgetTheme.DARK }, + }); -// 链接Wallet Connect钱包 -export async function getConnect() { - // let result = await passportInstance.loginCallback() - const profile = await passportInstance.login(); - console.log(profile,'-----------------------------------------------------------------') - return - // let result = profile.passport.UserProfile | null = await passportInstance.login() - // let result = await passportInstance.request({ method: 'eth_requestAccounts' }) - return result -} \ No newline at end of file + // RECOMMENDED - create all of the widgets once at the start of your application + // use the created widgets throughout your application to mount and unmount in specific parts of your application + const connect = widgets.create(checkout.WidgetType.CONNECT); + const wallet = widgets.create(checkout.WidgetType.WALLET); // you can optionally pass in additional config per widget + const swap = widgets.create(checkout.WidgetType.SWAP); + const bridge = widgets.create(checkout.WidgetType.BRIDGE); + const onramp = widgets.create(checkout.WidgetType.ONRAMP); + + // Mount the wallet widget passing the element id of where to mount the widget + connect.mount('wallet'); + } + async connect() { + const profile = await this.passportInstance.login(); + console.log(profile,'-----------------------------------------------------------------') + const passportProvider = this.passportInstance.connectEvm(); + this.web3Provider = new providers.Web3Provider(passportProvider); + this.signer = this.web3Provider.getSigner(); + const accounts = await passportProvider.request({ method: "eth_requestAccounts" }) + const accessToken = await this.passportInstance.getAccessToken(); + console.log('accessToken:', accessToken) + // const idToken = await passportInstance.getIdToken(); + // console.log('idToken:', idToken) + return {profile, accounts, accessToken} + } + + async logout() { + await this.passportInstance.logout(); + } + /** + * 查询某个nft的所有挂单 + * @param {*} contractAddress + * @returns + */ + async listListings(contractAddress){ + const listOfListings = await this.client.listListings({ + sellItemContractAddress: contractAddress, + status: orderbook.OrderStatusName.ACTIVE, + pageSize: 50, + }); + return listOfListings; + }; + + /** + * 准备一个ERC721的挂单 + * @returns + */ + async _prepareERC721Listing({ contractAddress, tokenId, type = 'ERC721', currencyAddress, currencyAmount}){ + const offerer = await this.signer.getAddress(); + const buyData = { + amount: currencyAmount, + } + if (currencyAddress == NATIVE) { + buyData.type = NATIVE + } else { + buyData.type = ERC20 + buyData.contractAddress = currencyAddress + } + const preparedListing = await this.client.prepareListing({ + makerAddress: offerer, + buy: buyData, + sell: { + contractAddress, + tokenId, + type, + }, + }); + + let orderSignature = '' + for (const action of preparedListing.actions) { + // If the user hasn't yet approved the Immutable Seaport contract to transfer assets from this + // collection on their behalf they'll need to do so before they create an order + if (action.type === orderbook.ActionType.TRANSACTION) { + const builtTx = await action.buildTransaction() + console.log(`Submitting ${action.purpose} transaction`) + await signer.sendTransaction(builtTx); + } + + // For an order to be created (and subsequently filled), Immutable needs a valid signature for the order data. + // This signature is stored off-chain and is later provided to any user wishing to fulfil the open order. + // The signature only allows the order to be fulfilled if it meets the conditions specified by the user that created the listing. + if (action.type === orderbook.ActionType.SIGNABLE) { + orderSignature = await signer._signTypedData( + action.message.domain, + action.message.types, + action.message.value, + ) + } + } + + return { preparedListing, orderSignature } + } + /** + * 创建一个挂单 + * @param {*} preparedListing + * @param {*} orderSignature + */ + async _createListing( + preparedListing, + orderSignature, + currencyAmount + ){ + const amount = (BigInt(currencyAmount) * 2n / 100n).toString() + const order = await this.client.createListing({ + orderComponents: preparedListing.orderComponents, + orderHash: preparedListing.orderHash, + orderSignature, + // Optional maker marketplace fee + makerFees: [{ + amount, + recipientAddress: marketAddress, // Replace address with your own marketplace address + }], + }); + console.log('order:', order); + return order + }; + + /** + * 出售一个ERC721的NFT + * doc: https://docs.immutable.com/docs/zkEVM/products/orderbook/create-listing + * @param {string} contractAddress NFT的合约地址 + * @param {string} tokenId NFT的tokenId + * @param {string} currencyAddress NATIVE 或者 ERC20的合约地址 + * @param {string} currencyAmount 出售价格, 单位 wei + */ + async beginSellERC721({contractAddress, tokenId, currencyAddress, currencyAmount}) { + const { preparedListing, orderSignature } = + await this._prepareERC721Listing({contractAddress, tokenId, currencyAddress, currencyAmount}); + const order = await this._createListing(preparedListing, orderSignature, currencyAmount); + return order + } + /** + * 开始购买 + * doc: https://docs.immutable.com/docs/zkEVM/products/orderbook/fill + * @param {*} listingId + */ + async beginBuy(listingId) { + const fulfiller = await this.signer.getAddress(); + const { actions, expiration, order } = await this.client.fulfillOrder( + listingId, + fulfiller, + [] + ); + console.log(`Fulfilling listing ${order}, transaction expiry ${expiration}`); + for (const action of actions) { + if (action.type === orderbook.ActionType.TRANSACTION) { + const builtTx = await action.buildTransaction(); + console.log(`Submitting ${action.purpose} transaction`); + await signer.sendTransaction(builtTx); + } + } + } + /** + * 批量购买 + * doc: https://docs.immutable.com/docs/zkEVM/products/orderbook/fill-bulk + * @param { string[] } listingIds: listingId列表 + */ + async batchBuy(listingIds) { + const fulfiller = await this.signer.getAddress(); + try { + const fulfillResponse = await this.client.fulfillBulkOrders( + listingIds.map((listingId) => ({ + listingId, + // you could have up to 2 marketplace fees + takerFees: [], + })), + fulfiller + ); + + if (fulfillResponse.sufficientBalance) { + const { actions, expiration, fulfillableOrders, unfulfillableOrders } = fulfillResponse; + // depending on the application, we can either throw an error if some orders are not fulfillable + // or we can ignore these unfulfillable orders and proceed with fulfillment + if (unfulfillableOrders.length > 0) { + throw new Error( + `Not all orders are fulfillable - unfulfillable orders: ${unfulfillableOrders}` + ); + } + for (const action of actions) { + if (action.type === orderbook.ActionType.TRANSACTION) { + const builtTx = await action.buildTransaction(); + console.log(`Submitting ${action.purpose} transaction`); + await signer.sendTransaction(builtTx); + } + } + console.log( + `Fulfilling listings ${fulfillableOrders}, transaction expiry ${expiration}` + ); + } + } catch (e) { + console.error(`Fulfill bulk orders request failed with ${e}`); + throw e; + } + } + /** + * 取消交易 + * doc: https://docs.immutable.com/docs/zkEVM/products/orderbook/cancel + * @param {*} listingIds + * @returns + */ + async cancelOrder(listingIds) { + const account = await this.signer.getAddress(); + const { signableAction } = await this.client.prepareOrderCancellations(listingIds); + const cancellationSignature = await this.signer._signTypedData( + signableAction.message.domain, + signableAction.message.types, + signableAction.message.value, + ) + return this.client.cancelOrders(listingIds, account, cancellationSignature) + } + /** + * 取消交易, onChain + * doc: https://docs.immutable.com/docs/zkEVM/products/orderbook/cancel + * @param {*} listingIds + * @returns + */ + async cancelOrdersOnChain(listingIds) { + const offerer = await this.signer.getAddress(); + const { cancellationAction } = await this.client.cancelOrdersOnChain( + listingIds, + offerer + ); + + const unsignedCancelOrderTransaction = await cancellationAction.buildTransaction(); + const receipt = await this.signer.sendTransaction(unsignedCancelOrderTransaction); + return receipt; + } +} + +export const PassportWallet = createSingleton(LPassportWallet) \ No newline at end of file