diff --git a/example/nuxt.config.js b/example/nuxt.config.js index 20eaaf5..2327bf4 100644 --- a/example/nuxt.config.js +++ b/example/nuxt.config.js @@ -45,6 +45,8 @@ export default { '@cosmjs', '@walletconnect', '@web3modal', + 'ethers', + '@noble/curves', ], } } diff --git a/example/package.json b/example/package.json index 4d79312..af9f7d2 100644 --- a/example/package.json +++ b/example/package.json @@ -11,6 +11,7 @@ "dependencies": { "@cosmjs/stargate": "^0.28.10", "core-js": "^3.19.3", + "ethers": "^6.13.4", "nuxt": "^2.17.2", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/example/pages/eth/ethers.vue b/example/pages/eth/ethers.vue new file mode 100644 index 0000000..46e12ff --- /dev/null +++ b/example/pages/eth/ethers.vue @@ -0,0 +1,354 @@ +<template> + <v-app> + <v-app-bar app> + <template v-if="walletAddress"> + <v-chip label>{{ method }}</v-chip> + <v-toolbar-title class="ml-4">{{ formattedWalletAddress }}</v-toolbar-title> + <v-spacer /> + <v-btn + class="text-truncate" + outlined + style="max-width: 150px" + @click="logout" + >Logout</v-btn> + </template> + <template v-if="walletAddress" v-slot:extension> + <v-tabs + v-model="tab" + grow + > + <v-tab key="send">Send</v-tab> + <v-tab key="sign-arbitrary">Sign Arbitrary</v-tab> + </v-tabs> + </template> + </v-app-bar> + + <v-main> + <v-container v-if="!walletAddress" fill-height> + <v-row> + <v-col class="d-flex justify-center"> + <v-btn + :loading="isLoading" + elevation="2" + @click="connect" + >Connect</v-btn> + </v-col> + </v-row> + </v-container> + <v-container v-else fill-height> + <v-row> + <v-col> + <v-card + :loading="isSending || isSigningArbitrary" + class="mx-auto my-12" + max-width="480" + outlined + > + <v-tabs-items v-model="tab"> + + <v-tab-item key="send"> + <v-form + class="pa-4" + @submit.prevent="send" + > + <v-text-field + :value="walletAddress" + label="From address" + readonly + /> + <v-text-field + v-model="toAddress" + label="To address" + :disabled="isSending" + required + /> + <v-text-field + v-model="amount" + label="Amount" + type="number" + :disabled="isSending" + suffix="ETH" + required + /> + <div class="d-flex justify-end"> + <v-btn + type="submit" + :elevation="isSending ? 0 : 2" + :disabled="isSending" + :loading="isSending" + >Send</v-btn> + </div> + </v-form> + </v-tab-item> + + <v-tab-item key="sign-arbitrary"> + <v-form + class="pa-4" + @submit.prevent="signArbitrary" + > + <v-text-field + :value="signArbitraryMessage" + label="Message to sign" + required + /> + <div class="d-flex justify-end"> + <v-btn + type="submit" + :elevation="isSigningArbitrary ? 0 : 2" + :disabled="isSigningArbitrary" + :loading="isSigningArbitrary" + >Sign</v-btn> + </div> + </v-form> + <v-divider /> + <v-textarea + class="mt-4 mx-4" + :value="signArbitraryResult" + label="Signature" + background-color="grey lighten-4" + placeholder="Result" + persistent-placeholder + outlined + readonly + auto-grow + /> + </v-tab-item> + + </v-tabs-items> + </v-card> + <v-alert + v-model="isShowAlert" + class="mx-auto" + :type="error ? 'error' : 'success'" + elevation="2" + max-width="480" + prominent + dismissible + > + <div v-if="error">{{ error }}</div> + <v-row + v-else + align="center" + > + <v-col class="grow">The transaction is broadcasted.</v-col> + <v-col class="shrink"> + <v-btn + :href="txURL" + color="white" + target="_blank" + rel="noreferrer noopener" + small + outlined + >Details</v-btn> + </v-col> + </v-row> + </v-alert> + </v-col> + </v-row> + </v-container> + </v-main> + <v-footer app> + <v-container>Demo of ethers + @likecoin/wallet-connector</v-container> + </v-footer> + </v-app> +</template> + +<script> +import { JsonRpcProvider, formatEther, verifyMessage, parseEther } from 'ethers'; + +import { LikeCoinWalletConnector, LikeCoinWalletConnectorMethodType } from '../../../dist'; + +import { AuthcoreSigner } from '~/utils/ethers/AuthcoreSigner'; + +export default { + data() { + return { + tab: 'send', + isLoading: true, + + method: undefined, + walletAddress: '', + + toAddress: '0xCb3152aCb16a60325Abd5Ab0e0CD2174a5292414', + amount: 1, + + ethSigner: null, + ethProvider: null, + ethBalance: 0, + + signArbitraryMessage: 'Hello, @likecoin/wallet-connector!', + signArbitraryResult: '', + + txHash: '', + error: '', + isSending: false, + isSigningArbitrary: false, + isShowAlert: false, + }; + }, + computed: { + formattedWalletAddress() { + const address = this.walletAddress; + const length = address.length; + return `${address.substring(0, 8)}...${address.substring(length - 3, length)}`; + }, + txURL() { + return `https://sepolia-optimism.etherscan.io/tx/${this.txHash}` + }, + isEthSupported() { + return !!this.ethSigner; + }, + }, + async mounted() { + this.connector = new LikeCoinWalletConnector({ + chainId: 'likecoin-mainnet-2', + chainName: 'LikeCoin', + rpcURL: 'https://mainnet-node.like.co/rpc/', + restURL: 'https://mainnet-node.like.co', + coinType: 118, + coinDenom: 'LIKE', + coinMinimalDenom: 'nanolike', + coinDecimals: 9, + coinGeckoId: 'likecoin', + bech32PrefixAccAddr: 'like', + bech32PrefixAccPub: 'likepub', + bech32PrefixValAddr: 'likevaloper', + bech32PrefixValPub: 'likevaloperpub', + bech32PrefixConsAddr: 'likevalcons', + bech32PrefixConsPub: 'likevalconspub', + availableMethods: [ + LikeCoinWalletConnectorMethodType.LikerId, + // LikeCoinWalletConnectorMethodType.Keplr, + // LikeCoinWalletConnectorMethodType.Cosmostation, + ], + keplrSignOptions: { + disableBalanceCheck: true, + preferNoSetFee: true, + preferNoSetMemo: true, + }, + keplrInstallURLOverride: 'https://www.keplr.app/download', + keplrInstallCTAPreset: 'fancy-banner', + cosmostationDirectSignEnabled: true, + language: 'zh', + + authcoreClientId: 'likecoin-app-hidesocial', + authcoreApiHost: 'https://likecoin-integration-test.authcore.io', + authcoreRedirectUrl: `http://localhost:3000/in/register?method=liker-id&page=${encodeURIComponent('/eth/ethers')}`, + + onEvent: ({ type, ...payload}) => { + console.log('onEvent', type, payload); + }, + }); + const { code, method, ...query } = this.$route.query; + if (method && code) { + this.$router.replace({ query }) + const connection = await this.connector.handleRedirect(method, { code }); + if (connection) this.handleConnection(connection); + } else { + this.connector.restoreSession(); + const connection = await this.connector.initIfNecessary(); + this.handleConnection(connection); + } + this.isLoading = false; + }, + watch: { + txHash(value) { + if (value) { + this.isShowAlert = true; + } + }, + error(value) { + if (value) { + this.isShowAlert = true; + } + }, + }, + methods: { + reset() { + this.offlineSigner = undefined; + this.walletAddress = ''; + this.signArbitraryResult = ''; + + this.txHash = ''; + this.error = ''; + this.isSending = false; + this.isShowAlert = false; + }, + handleConnection(connection) { + if (!connection) return; + const { method, accounts: [account], offlineSigner, params } = connection; + this.method = method; + this.walletAddress = account.address; + this.offlineSigner = offlineSigner; + if (params.ethereumProvider) { + // Connect to the Ethereum network + const provider = new JsonRpcProvider("https://opt-sepolia.g.alchemy.com/v2/6-pw8XvhGc-oOc-xY3vkWKdrM8lzrtUc"); + this.ethSigner = new AuthcoreSigner(params.ethereumProvider, provider); + this.ethProvider = provider; + this.initEth() + } + this.connector.once('account_change', this.handleAccountChange); + }, + async initEth() { + const walletAddress = await this.ethSigner.getAddress(); + this.walletAddress = walletAddress; + this.ethBalance = formatEther(await this.ethProvider.getBalance(walletAddress)) + }, + async connect() { + const connection = await this.connector.openConnectWalletModal(); + if (connection) this.handleConnection(connection); + }, + logout() { + this.connector.disconnect(); + this.reset(); + }, + async handleAccountChange(method) { + const connection = await this.connector.init(method); + this.handleConnection(connection); + }, + + async send() { + try { + this.txHash = ''; + this.error = ''; + this.isShowAlert = false; + this.isSending = true; + const tx = await this.ethSigner.sendTransaction({ + to: this.toAddress, + value: parseEther(this.amount.toString()), + }); + const receipt = await tx.wait(); + if (receipt.status === 1) { + this.txHash = receipt.transactionHash; + } else { + this.error = receipt; + } + } catch (error) { + this.error = `${error.message || error.name || error}`; + console.error(error); + } finally { + this.isSending = false; + } + }, + + async signArbitrary() { + try { + this.error = ''; + this.isShowAlert = false; + this.signArbitraryResult = ''; + this.isSigningArbitrary = true; + + this.signArbitraryResult = await this.ethSigner.signMessage(this.signArbitraryMessage); + if (verifyMessage(this.signArbitraryMessage, this.signArbitraryResult) !== this.walletAddress) { + throw new Error('Invalid signature'); + } + } catch (error) { + this.error = `${error.message || error.name || error}`; + console.error(error); + } finally { + this.isSigningArbitrary = false; + } + }, + }, +}; +</script> diff --git a/example/pages/in/register/index.vue b/example/pages/in/register/index.vue index c4bfd79..a249d5e 100644 --- a/example/pages/in/register/index.vue +++ b/example/pages/in/register/index.vue @@ -1,8 +1,8 @@ <script> export default { fetch({ redirect, query }) { - const { code, method } = query; - redirect(`/?code=${code}&method=${method}`) + const { code, method, page = '/' } = query; + redirect(`${page}?code=${code}&method=${method}`) } } </script> \ No newline at end of file diff --git a/example/pages/index.vue b/example/pages/index.vue index 13024b1..bddef84 100644 --- a/example/pages/index.vue +++ b/example/pages/index.vue @@ -157,6 +157,14 @@ </v-main> <v-footer app> <v-container>Demo of @likecoin/wallet-connector</v-container> + <v-btn + class="text-truncate" + outlined + style="max-width: 150px" + @click="$router.push('/eth/ethers')" + > + Ethers + </v-btn> </v-footer> </v-app> </template> @@ -261,13 +269,15 @@ export default { console.log('onEvent', type, payload); }, }); - const session = this.connector.restoreSession(); - this.handleConnection(session); const { code, method, ...query } = this.$route.query; if (method && code) { this.$router.replace({ query }) const connection = await this.connector.handleRedirect(method, { code }); if (connection) this.handleConnection(connection); + } else { + this.connector.restoreSession(); + const connection = await this.connector.initIfNecessary(); + this.handleConnection(connection); } this.isLoading = false; }, @@ -296,7 +306,7 @@ export default { }, handleConnection(connection) { if (!connection) return; - const { method, accounts: [account], offlineSigner } = connection; + const { method, accounts: [account], offlineSigner, params } = connection; this.method = method; this.walletAddress = account.address; this.offlineSigner = offlineSigner; diff --git a/example/utils/ethers/AuthcoreSigner.js b/example/utils/ethers/AuthcoreSigner.js new file mode 100644 index 0000000..b9b9ade --- /dev/null +++ b/example/utils/ethers/AuthcoreSigner.js @@ -0,0 +1,77 @@ +import { + AbstractSigner, + Transaction, + hexlify, + copyRequest, + resolveAddress, + resolveProperties, + toUtf8Bytes, +} from 'ethers'; + +export class AuthcoreSigner extends AbstractSigner { + /** + * The signer address. + */ + address = ''; + AuthcoreEthereumProvider = null; + + /** + * Creates a new **AuthcoreSigner** with %%AuthcoreEthereumProvider%% attached to + * %%provider%%. + */ + constructor(AuthcoreEthereumProvider, provider) { + super(provider); + this.AuthcoreEthereumProvider = AuthcoreEthereumProvider; + } + + async getAddress() { + if (this.address) { + return this.address; + } + const addresses = await this.AuthcoreEthereumProvider.getAddresses(); + this.address = addresses[0]; + return this.address; + } + + connect(provider) { + return new AuthcoreSigner(this.AuthcoreEthereumProvider, provider); + } + + #throwUnsupported(suffix, operation) { + assert(false, `AuthcoreSigner cannot sign ${suffix}`, "UNSUPPORTED_OPERATION", { operation }); + } + + // Modified from https://github.com/ethers-io/ethers.js/blob/9e7e7f3e2f2d51019aaa782e6290e079c38332fb/src.ts/wallet/base-wallet.ts#L71 + async signTransaction(_tx) { + const tx = copyRequest(_tx); + + const { to, from } = await resolveProperties({ + to: (tx.to ? resolveAddress(tx.to, this.provider) : undefined), + from: (tx.from ? resolveAddress(tx.from, this.provider) : undefined) + }); + + if (to != null) { tx.to = to; } + if (from != null) { tx.from = from; } + + if (tx.from != null) { + assertArgument(getAddress((tx.from)) === this.address, + "transaction from address mismatch", "tx.from", tx.from); + delete tx.from; + } + + const btx = Transaction.from(tx); + btx.signature = await this.AuthcoreEthereumProvider.signTransaction(btx.unsignedSerialized, this.address.toLowerCase()); + return btx.serialized; + } + + async signMessage(_message) { + const message = ((typeof (_message) === "string") ? toUtf8Bytes(_message) : _message); + return await this.AuthcoreEthereumProvider.signMessage(hexlify(message), this.address.toLowerCase()); + } + + async signTypedData(domain, types, value) { + this.#throwUnsupported("typed-data", "signTypedData"); + } +} + +export default AuthcoreSigner; diff --git a/example/yarn.lock b/example/yarn.lock index 6996483..8c9f8f6 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@adraffy/ens-normalize@1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" + integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -1722,6 +1727,18 @@ resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.3.0.tgz#e5623885bb5e0c48c1151e4dae422fb03a5887a1" integrity sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw== +"@noble/curves@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/hashes@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + "@noble/hashes@^1", "@noble/hashes@^1.0.0": version "1.1.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" @@ -2190,6 +2207,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.0.tgz#67c7b724e1bcdd7a8821ce0d5ee184d3b4dd525a" integrity sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA== +"@types/node@22.7.5": + version "22.7.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.7.5.tgz#cfde981727a7ab3611a481510b473ae54442b92b" + integrity sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ== + dependencies: + undici-types "~6.19.2" + "@types/node@>=13.7.0": version "18.0.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.1.tgz#e91bd73239b338557a84d1f67f7b9e0f25643870" @@ -2528,6 +2552,11 @@ acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== +aes-js@4.0.0-beta.5: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" + integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -4230,6 +4259,19 @@ etag@^1.8.1, etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== +ethers@^6.13.4: + version "6.13.4" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.4.tgz#bd3e1c3dc1e7dc8ce10f9ffb4ee40967a651b53c" + integrity sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA== + dependencies: + "@adraffy/ens-normalize" "1.10.1" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@types/node" "22.7.5" + aes-js "4.0.0-beta.5" + tslib "2.7.0" + ws "8.17.1" + events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -8584,16 +8626,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tslib@2.7.0, tslib@^2.0.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== - tslib@^2.0.3, tslib@^2.3.1: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" @@ -8639,6 +8681,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + unfetch@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-5.0.0.tgz#8a5b6e5779ebe4dde0049f7d7a81d4a1af99d142" @@ -9134,6 +9181,11 @@ write-json-file@^2.3.0: sort-keys "^2.0.0" write-file-atomic "^2.0.0" +ws@8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + ws@^7, ws@^7.3.1: version "7.5.8" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.8.tgz#ac2729881ab9e7cbaf8787fe3469a48c5c7f636a"