From f0877f7295d771aa1b04a3485e8033dba6d42d98 Mon Sep 17 00:00:00 2001 From: Felipe Mendes Date: Thu, 31 Oct 2024 10:16:40 -0300 Subject: [PATCH] feat: siwx UI (#3171) --- .changeset/young-readers-develop.md | 24 +++++ apps/laboratory/package.json | 1 + .../src/pages/library/multichain-siwx.tsx | 43 +++++++++ apps/laboratory/src/utils/DataUtil.ts | 5 + packages/adapters/solana/src/client.ts | 2 +- .../src/controllers/ConnectionController.ts | 31 ++++++- .../core/src/controllers/RouterController.ts | 1 + packages/core/src/utils/SIWXUtil.ts | 4 +- packages/scaffold-ui/exports/index.ts | 2 + .../scaffold-ui/src/modal/w3m-router/index.ts | 2 + .../src/partials/w3m-header/index.ts | 3 +- .../w3m-siwx-sign-message-thumbnails/index.ts | 52 +++++++++++ .../styles.ts | 13 +++ .../views/w3m-siwx-sign-message-view/index.ts | 91 +++++++++++++++++++ packages/siwx/src/core/SIWXConfig.ts | 23 ++++- pnpm-lock.yaml | 3 + 16 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 .changeset/young-readers-develop.md create mode 100644 apps/laboratory/src/pages/library/multichain-siwx.tsx create mode 100644 packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/index.ts create mode 100644 packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/styles.ts create mode 100644 packages/scaffold-ui/src/views/w3m-siwx-sign-message-view/index.ts diff --git a/.changeset/young-readers-develop.md b/.changeset/young-readers-develop.md new file mode 100644 index 0000000000..efb5855727 --- /dev/null +++ b/.changeset/young-readers-develop.md @@ -0,0 +1,24 @@ +--- +'@reown/appkit-adapter-solana': patch +'@reown/appkit-scaffold-ui': patch +'@apps/laboratory': patch +'@reown/appkit-core': patch +'@reown/appkit-siwx': patch +'@apps/demo': patch +'@apps/gallery': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-polkadot': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'@reown/appkit-common': patch +'@reown/appkit-experimental': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-siwe': patch +'@reown/appkit-ui': patch +'@reown/appkit-wallet': patch +--- + +Adds UI components for SIWX diff --git a/apps/laboratory/package.json b/apps/laboratory/package.json index f28d167161..9067b8f293 100644 --- a/apps/laboratory/package.json +++ b/apps/laboratory/package.json @@ -82,6 +82,7 @@ "@reown/appkit-adapter-wagmi": "workspace:*", "@reown/appkit-experimental": "workspace:*", "@reown/appkit-siwe": "workspace:*", + "@reown/appkit-siwx": "workspace:*", "@reown/appkit-wallet": "workspace:*", "@sentry/browser": "7.119.1", "@sentry/react": "7.92.0", diff --git a/apps/laboratory/src/pages/library/multichain-siwx.tsx b/apps/laboratory/src/pages/library/multichain-siwx.tsx new file mode 100644 index 0000000000..9c9a5e53cb --- /dev/null +++ b/apps/laboratory/src/pages/library/multichain-siwx.tsx @@ -0,0 +1,43 @@ +import { createAppKit } from '@reown/appkit/react' +import { EthersAdapter } from '@reown/appkit-adapter-ethers' +import { SolanaAdapter } from '@reown/appkit-adapter-solana' +import { ThemeStore } from '../../utils/StoreUtil' +import { ConstantsUtil } from '../../utils/ConstantsUtil' + +import { AppKitButtons } from '../../components/AppKitButtons' +import { HuobiWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets' +import { MultiChainTestsEthersSolana } from '../../components/MultiChainTestsEthersSolana' +import { mainnet } from '@reown/appkit/networks' +import { DefaultSIWX } from '@reown/appkit-siwx' + +const networks = ConstantsUtil.AllNetworks + +const etherAdapter = new EthersAdapter() + +const solanaWeb3JsAdapter = new SolanaAdapter({ + wallets: [new HuobiWalletAdapter(), new SolflareWalletAdapter()] +}) + +const modal = createAppKit({ + adapters: [etherAdapter, solanaWeb3JsAdapter], + projectId: ConstantsUtil.ProjectId, + networks, + defaultNetwork: mainnet, + features: { + analytics: true + }, + termsConditionsUrl: 'https://reown.com/terms-of-service', + privacyPolicyUrl: 'https://reown.com/privacy-policy', + siwx: new DefaultSIWX() +}) + +ThemeStore.setModal(modal) + +export default function MultiChainEthersSolana() { + return ( + <> + + + + ) +} diff --git a/apps/laboratory/src/utils/DataUtil.ts b/apps/laboratory/src/utils/DataUtil.ts index 396573d4be..3b9f8714b4 100644 --- a/apps/laboratory/src/utils/DataUtil.ts +++ b/apps/laboratory/src/utils/DataUtil.ts @@ -137,6 +137,11 @@ export const multichainSdkOptions: SdkOption[] = [ title: 'Basic', link: '/library/multichain-basic', description: 'Configuration with no adapters enabled for AppKit' + }, + { + title: '[Experimental] SIWX', + link: '/library/multichain-siwx', + description: 'Configuration with SIWX adapters enabled for AppKit' } ] diff --git a/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts index 8087b65ec0..085ef21ba4 100644 --- a/packages/adapters/solana/src/client.ts +++ b/packages/adapters/solana/src/client.ts @@ -179,7 +179,7 @@ export class SolanaAdapter implements ChainAdapter { // If it's not the auth provider, we should auto connect the provider if (chainNamespace === this.chainNamespace || !isAuthProvider) { - this.setProvider(externalProvider) + await this.setProvider(externalProvider) } }, diff --git a/packages/core/src/controllers/ConnectionController.ts b/packages/core/src/controllers/ConnectionController.ts index c44b2aa301..a7316cfb05 100644 --- a/packages/core/src/controllers/ConnectionController.ts +++ b/packages/core/src/controllers/ConnectionController.ts @@ -252,6 +252,16 @@ export const ConnectionController = { async disconnect() { const connectionControllerClient = this._getClient() + const siwx = OptionsController.state.siwx + if (siwx) { + const activeCaipNetwork = ChainController.getActiveCaipNetwork() + const address = ChainController.getActiveCaipAddress()?.split(':')[2] || '' + + if (activeCaipNetwork && address) { + siwx.revokeSession(activeCaipNetwork.caipNetworkId, address) + } + } + try { await connectionControllerClient?.disconnect() this.resetWcConnection() @@ -266,8 +276,8 @@ export const ConnectionController = { * This is not yet considering One Click Auth. */ async initializeSWIXIfAvailable() { - const swix = OptionsController.state.siwx - if (!swix) { + const siwx = OptionsController.state.siwx + if (!siwx) { return } @@ -285,17 +295,28 @@ export const ConnectionController = { throw new Error('No active chain') } - const message = await swix.createMessage({ + const address = ChainController.getActiveCaipAddress()?.split(':')[2] || '' + + const sessions = await siwx.getSessions(activeCaipNetwork.caipNetworkId, address) + if (sessions.length) { + return + } + + ModalController.open({ view: 'SIWXSignMessage' }) + + const message = await siwx.createMessage({ chainId: activeCaipNetwork.caipNetworkId, - accountAddress: ChainController.getActiveCaipAddress()?.split(':')[2] || '' + accountAddress: address }) const signature = await client.signMessage(message.toString()) - await swix.addSession({ + await siwx.addSession({ message, signature }) + + ModalController.close() } catch (error) { // eslint-disable-next-line no-console console.error('Failed to initialize SIWX', error) diff --git a/packages/core/src/controllers/RouterController.ts b/packages/core/src/controllers/RouterController.ts index 31b3732a33..f6c7021f4f 100644 --- a/packages/core/src/controllers/RouterController.ts +++ b/packages/core/src/controllers/RouterController.ts @@ -81,6 +81,7 @@ export interface RouterControllerState { | 'SwitchActiveChain' | 'SmartSessionCreated' | 'SmartSessionList' + | 'SIWXSignMessage' history: RouterControllerState['view'][] data?: { connector?: Connector diff --git a/packages/core/src/utils/SIWXUtil.ts b/packages/core/src/utils/SIWXUtil.ts index 0a1613b4e1..92d2847bbd 100644 --- a/packages/core/src/utils/SIWXUtil.ts +++ b/packages/core/src/utils/SIWXUtil.ts @@ -6,9 +6,9 @@ import type { CaipNetworkId } from '@reown/appkit-common' export interface SIWXConfig { createMessage: (input: SIWXMessageInput) => Promise addSession: (session: SIWXSession) => Promise - revokeSession: (chainId: string, address: string) => Promise + revokeSession: (chainId: CaipNetworkId, address: string) => Promise setSessions: (sessions: SIWXSession[]) => Promise - getSessions: (chainId: CaipNetworkId) => Promise + getSessions: (chainId: CaipNetworkId, address: string) => Promise } /** diff --git a/packages/scaffold-ui/exports/index.ts b/packages/scaffold-ui/exports/index.ts index b1264352c1..3dde4beae2 100644 --- a/packages/scaffold-ui/exports/index.ts +++ b/packages/scaffold-ui/exports/index.ts @@ -62,6 +62,7 @@ export * from '../src/views/w3m-connecting-social-view/index.js' export * from '../src/views/w3m-profile-view/index.js' export * from '../src/views/w3m-switch-address-view/index.js' export * from '../src/views/w3m-connecting-farcaster-view/index.js' +export * from '../src/views/w3m-siwx-sign-message-view/index.js' // -- Partials ------------------------------------------ // export * from '../src/partials/w3m-all-wallets-list/index.js' @@ -115,3 +116,4 @@ export * from '../src/partials/w3m-connector-list/index.js' export * from '../src/partials/w3m-all-wallets-widget/index.js' export * from '../src/partials/w3m-account-auth-button/index.js' export * from '../src/partials/w3m-wallet-guide/index.js' +export * from '../src/partials/w3m-siwx-sign-message-thumbnails/index.js' diff --git a/packages/scaffold-ui/src/modal/w3m-router/index.ts b/packages/scaffold-ui/src/modal/w3m-router/index.ts index 3903351a8c..581c25df6c 100644 --- a/packages/scaffold-ui/src/modal/w3m-router/index.ts +++ b/packages/scaffold-ui/src/modal/w3m-router/index.ts @@ -163,6 +163,8 @@ export class W3mRouter extends LitElement { return html`` case 'SmartSessionList': return html`` + case 'SIWXSignMessage': + return html`` default: return html`` } diff --git a/packages/scaffold-ui/src/partials/w3m-header/index.ts b/packages/scaffold-ui/src/partials/w3m-header/index.ts index 29b04c2530..05c3c28bd9 100644 --- a/packages/scaffold-ui/src/partials/w3m-header/index.ts +++ b/packages/scaffold-ui/src/partials/w3m-header/index.ts @@ -84,7 +84,8 @@ function headings() { ConnectingFarcaster: 'Farcaster', SwitchActiveChain: 'Switch chain', SmartSessionCreated: undefined, - SmartSessionList: 'Smart Sessions' + SmartSessionList: 'Smart Sessions', + SIWXSignMessage: 'Sign In' } } diff --git a/packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/index.ts b/packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/index.ts new file mode 100644 index 0000000000..d6a19f6e1f --- /dev/null +++ b/packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/index.ts @@ -0,0 +1,52 @@ +import { customElement } from '@reown/appkit-ui' +import { LitElement, html } from 'lit' +import styles from './styles.js' +import { AccountController, OptionsController } from '@reown/appkit-core' + +@customElement('w3m-siwx-sign-message-thumbnails') +export class W3mSIWXSignMessageThumbnails extends LitElement { + public static override styles = styles + + // -- Members ------------------------------------------- // + private readonly dappImageUrl = OptionsController.state.metadata?.icons + + private readonly walletImageUrl = AccountController.state.connectedWalletInfo?.icon + + public override firstUpdated() { + const visuals = this.shadowRoot?.querySelectorAll('wui-visual-thumbnail') + + if (visuals?.[0]) { + this.createAnimation(visuals[0], 'translate(18px)') + } + if (visuals?.[1]) { + this.createAnimation(visuals[1], 'translate(-18px)') + } + } + + // -- Render -------------------------------------------- // + public override render() { + return html` + + + ` + } + + // -- Private ------------------------------------------- // + private createAnimation(element: HTMLElement, translation: string) { + element.animate([{ transform: 'translateX(0px)' }, { transform: translation }], { + duration: 1600, + easing: 'cubic-bezier(0.56, 0, 0.48, 1)', + direction: 'alternate', + iterations: Infinity + }) + } +} + +declare global { + interface HTMLElementTagNameMap { + 'w3m-siwx-sign-message-thumbnails': W3mSIWXSignMessageThumbnails + } +} diff --git a/packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/styles.ts b/packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/styles.ts new file mode 100644 index 0000000000..f02285e6de --- /dev/null +++ b/packages/scaffold-ui/src/partials/w3m-siwx-sign-message-thumbnails/styles.ts @@ -0,0 +1,13 @@ +import { css } from 'lit' + +export default css` + :host { + display: flex; + justify-content: center; + gap: var(--wui-spacing-2xl); + } + + wui-visual-thumbnail:nth-child(1) { + z-index: 1; + } +` diff --git a/packages/scaffold-ui/src/views/w3m-siwx-sign-message-view/index.ts b/packages/scaffold-ui/src/views/w3m-siwx-sign-message-view/index.ts new file mode 100644 index 0000000000..2187113545 --- /dev/null +++ b/packages/scaffold-ui/src/views/w3m-siwx-sign-message-view/index.ts @@ -0,0 +1,91 @@ +import { + AccountController, + ChainController, + ConnectionController, + EventsController, + ModalController, + OptionsController, + RouterController +} from '@reown/appkit-core' +import { customElement } from '@reown/appkit-ui' +import { LitElement, html } from 'lit' +import { state } from 'lit/decorators.js' +import { W3mFrameRpcConstants } from '@reown/appkit-wallet' + +@customElement('w3m-siwx-sign-message-view') +export class W3mSIWXSignMessageView extends LitElement { + // -- Members ------------------------------------------- // + private readonly dappName = OptionsController.state.metadata?.name + + @state() private isCancelling = false + + // -- Render -------------------------------------------- // + public override render() { + return html` + + + + + ${this.dappName ?? 'Dapp'} needs to connect to your wallet + + + Sign this message to prove you own this wallet and proceed. Canceling will disconnect + you. + + + + Cancel + + + ` + } + + // -- Private ------------------------------------------- // + private async onCancel() { + this.isCancelling = true + const caipAddress = ChainController.state.activeCaipAddress + if (caipAddress) { + await ConnectionController.disconnect() + ModalController.close() + } else { + RouterController.push('Connect') + } + this.isCancelling = false + EventsController.sendEvent({ + event: 'CLICK_CANCEL_SIWE', + type: 'track', + properties: { + network: ChainController.state.activeCaipNetwork?.caipNetworkId || '', + isSmartAccount: + AccountController.state.preferredAccountType === + W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT + } + }) + } +} + +declare global { + interface HTMLElementTagNameMap { + 'w3m-siwx-sign-message-view': W3mSIWXSignMessageView + } +} diff --git a/packages/siwx/src/core/SIWXConfig.ts b/packages/siwx/src/core/SIWXConfig.ts index e465fb7517..ee1d49c959 100644 --- a/packages/siwx/src/core/SIWXConfig.ts +++ b/packages/siwx/src/core/SIWXConfig.ts @@ -44,8 +44,25 @@ export abstract class SIWXConfig implements SIWXConfigInterface { return this.storage.set(sessions) } - public async getSessions(chainId: CaipNetworkId): Promise { - return this.storage.get(chainId) + public async getSessions(chainId: CaipNetworkId, address: string): Promise { + const allSessions = await this.storage.get(chainId) + + return allSessions.filter(session => { + const isSameChain = session.message.chainId === chainId + const isSameAddress = session.message.accountAddress === address + + const startsAt = session.message.notBefore || session.message.issuedAt + if (!startsAt || Date.parse(startsAt) > Date.now()) { + return false + } + + const endsAt = session.message.expirationTime + if (endsAt && Date.now() > Date.parse(endsAt)) { + return false + } + + return isSameChain && isSameAddress + }) } public async revokeSession(chainId: string, address: string): Promise { @@ -58,7 +75,7 @@ export abstract class SIWXConfig implements SIWXConfigInterface { chainVerifiers.map(verifier => verifier.verify(session)) ) - return verifications.every(result => result) + return verifications.length > 0 && verifications.every(result => result) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 738f072c34..8b28ace07d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,6 +219,9 @@ importers: '@reown/appkit-siwe': specifier: workspace:* version: link:../../packages/siwe + '@reown/appkit-siwx': + specifier: workspace:* + version: link:../../packages/siwx '@reown/appkit-wallet': specifier: workspace:* version: link:../../packages/wallet