diff --git a/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts b/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts index f2ab6bd9f..8424e78a3 100644 --- a/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts +++ b/packages/0xsequence/tests/browser/mock-wallet/mock-wallet.test.ts @@ -1,5 +1,5 @@ import { ethers } from 'ethers' -import { WalletRequestHandler, WindowMessageHandler } from '@0xsequence/provider' +import { WalletRequestHandler, WindowMessageHandler, UrlMessageHandler } from '@0xsequence/provider' import { Wallet, Account } from '@0xsequence/wallet' import { NetworkConfig } from '@0xsequence/network' import { LocalRelayer } from '@0xsequence/relayer' @@ -11,7 +11,7 @@ import { test, assert } from '../../utils/assert' configureLogger({ logLevel: 'DEBUG', silence: false }) // -// Wallet, a test wallet +// Wallet, a test wallet, just like a super lightweight wallet-webapp // const main = async () => { @@ -88,12 +88,17 @@ const main = async () => { // fake/force an async wallet initialization for the wallet-request handler. This is the behaviour // of the wallet-webapp, so lets ensure the mock wallet does the same thing too. setTimeout(() => { - walletRequestHandler.signIn(account) + console.log('mock wallet signing into account...', account) + walletRequestHandler.signIn(account, { connect: true }) // TODO: review how we use this connect param in practice..? }, 1000) // setup and register window message transport const windowHandler = new WindowMessageHandler(walletRequestHandler) windowHandler.register() + + // setup and register url message transport + const urlMessageHandler = new UrlMessageHandler(walletRequestHandler, '/mock-wallet/mock-wallet.test.html') + urlMessageHandler.register() } main() diff --git a/packages/0xsequence/tests/browser/mux-transport/mux.test.ts b/packages/0xsequence/tests/browser/mux-transport/mux.test.ts index 26a78a2b8..ad05a089a 100644 --- a/packages/0xsequence/tests/browser/mux-transport/mux.test.ts +++ b/packages/0xsequence/tests/browser/mux-transport/mux.test.ts @@ -1,3 +1,6 @@ +// NOTE: run `pnpm test:server` and open browser at http://localhost:9999/mux-transport/mux.test.html +// to run tests from this file directly from your Web Browser. + import { ProxyMessageProvider, ProviderMessageTransport, diff --git a/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts b/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts index 5510bc5ac..007a74e1e 100644 --- a/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts +++ b/packages/0xsequence/tests/browser/proxy-transport/channel.test.ts @@ -1,3 +1,6 @@ +// NOTE: run `pnpm test:server` and open browser at http://localhost:9999/proxy-transport/channel.test.html +// to run tests from this file directly from your Web Browser. + import { Web3Provider, ProxyMessageProvider, diff --git a/packages/0xsequence/tests/browser/testutils/index.ts b/packages/0xsequence/tests/browser/testutils/index.ts index 7880169dd..4dd3f93ab 100644 --- a/packages/0xsequence/tests/browser/testutils/index.ts +++ b/packages/0xsequence/tests/browser/testutils/index.ts @@ -1,3 +1,4 @@ export * from './accounts' export * from './deploy-wallet-context' export * from './wallet' +export * from './utils' diff --git a/packages/0xsequence/tests/browser/testutils/utils.ts b/packages/0xsequence/tests/browser/testutils/utils.ts new file mode 100644 index 000000000..a6b90fd1d --- /dev/null +++ b/packages/0xsequence/tests/browser/testutils/utils.ts @@ -0,0 +1,20 @@ + +export const delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +const redirectRunKey = 'redirect-run' + +export const redirectRun = (state: string) => { + localStorage.setItem(redirectRunKey, state) +} + +export const redirectRunState = () => { + return localStorage.getItem(redirectRunKey) +} + +// export const isRedirectRunCallback = () => { +// return redirectRunState() === 'callback' +// } + +export const redirectRunClear = () => { + localStorage.removeItem(redirectRunKey) +} \ No newline at end of file diff --git a/packages/0xsequence/tests/browser/url-transport/dapp.test.ts b/packages/0xsequence/tests/browser/url-transport/dapp.test.ts new file mode 100644 index 000000000..b97a7d6fa --- /dev/null +++ b/packages/0xsequence/tests/browser/url-transport/dapp.test.ts @@ -0,0 +1,272 @@ +// NOTE: run `pnpm test:server` and open browser at http://localhost:9999/url-transport/dapp.test.html +// to run tests from this file directly from your Web Browser. + +import { test, assert } from '../../utils/assert' +import { initWallet, getWallet, Wallet, DefaultProviderConfig, BrowserRedirectMessageHooks, ProviderMessage, ConnectDetails } from '@0xsequence/provider' +import { configureLogger, TypedDataDomain, TypedDataField, base64DecodeObject } from '@0xsequence/utils' +import { testWalletContext, delay, redirectRun, redirectRunState, redirectRunClear } from '../testutils' + +configureLogger({ logLevel: 'DEBUG', silence: false }) + +export const tests = async () => { + // + // Deploy Sequence WalletContext (deterministic). We skip deployment + // as we rely on mock-wallet to deploy it. + // + const deployedWalletContext = testWalletContext + console.log('walletContext:', deployedWalletContext) + + // + // Setup + // + const providerConfig = { ...DefaultProviderConfig } + providerConfig.walletAppURL = 'http://localhost:9999/mock-wallet/mock-wallet.test.html' + providerConfig.transports = { + windowTransport: { + enabled: false // TODO: should be fine to keep this true too.. just .connect() will select the one it wants to use.. + }, + urlTransport: { + enabled: true, + // TODO: I think maybe we should move this to wallet.connect({ redirectUrl: '' }) which would be optional cuz default + // if redirectMode: true would just use window.location + redirectUrl: 'http://localhost:9999/url-transport/dapp.test.html', + hooks: new BrowserRedirectMessageHooks() // TODO: rename the hooks stuff.. + // instead just use wallet.on('xxxx', () => { ... }) .. of course .on('connect', x) will work.. + // and just need to think about the sendTxn and signMessage stuff.. + } + } + + // + // Wallet init + // + const wallet = await Wallet.load('hardhat', providerConfig) + + // provider + signer, by default if a chainId is not specified it will direct + // requests to the defaultChain + const provider = wallet.getProvider() + const signer = wallet.getSigner() + + // + // Tests starting point -- before connect is called, and before our redirect to wallet + // + const testsConnect = async () => { + console.log('==> testsConnect()') + + if (wallet.isConnected()) { + const address = await wallet.getAddress() + console.log('wallet connected with address:', address) + } else { + redirectRun('connect-callback') + wallet.connectWithRedirect() + } + } + + // + // Tests after the wallet connects and redirects + // + const testsConnectCallback = async () => { + console.log('==> testsConnectCallback()') + + let connectDetails: ConnectDetails | undefined + wallet.on('connect', _connectDetails => { + connectDetails = _connectDetails + }) + + + // let the events above trigger + await delay(1500) + + await test('isConnected', async () => { + assert.true(connectDetails!.connected, 'on connect event captured') + assert.true(wallet.isConnected(), 'is connected') + }) + + await test('getChainId', async () => { + const chainId = await wallet.getChainId() + assert.equal(chainId, 31337, 'chainId is correct') + }) + + // TODO: maybe .getSigners() can be cached too..? + // await test('getSigners', async () => { + // const signers = await signer.getSigners() + // assert.true(signers.length === 1, 'signers, single owner') + // assert.true(signers[0] === '0x4e37E14f5d5AAC4DF1151C6E8DF78B7541680853', 'signers, check address') + // }) + + await redirectRunExec('sign-message') + } + + // + // Tests after connected, and now performing a sign message request + // + const testsSignMessage = async () => { + console.log('==> testsSignMessage()') + + const message = 'hihi' + + // Upon call to signMessage, the page will redirect to make the request to the wallet + // TODO: maybe we should have getSignerRedirect() or something..? or getSignerAsync() ..? + redirectRun('sign-message-callback') + signer.signMessage(message) + } + + const testsSignMessageCallback = async () => { + console.log('==> testsSignMessageCallback()') + + redirectRunExec('done') + } + + const testsReset = () => { + console.log('==> testsReset()') + redirectRunClear() + } + + const redirectRunExec = async (state?: string) => { + if (state !== undefined) { + redirectRun(state) + } + + switch (redirectRunState()) { + case null: + case 'connect': + await testsConnect() + break + + case 'connect-callback': + await testsConnectCallback() + break + + case 'sign-message': + await testsSignMessage() + break + + case 'sign-message-callback': + await testsSignMessageCallback() + break + + case 'done': + testsReset() + break + + default: + testsReset() + console.error('unknown redirect run state') + } + } + + await redirectRunExec() + + + // const windowURL = new URL(window.location.href) + // const response = windowURL.searchParams.get('response') + // const continueTest = windowURL.searchParams.get('continue') + // const decodedResponse = base64DecodeObject(response) as ProviderMessage + + // const session = localStorage.getItem('@sequence.session') // todo remove + + // If we have a session, continue with tests + // if ((session && continueTest) || (session && response)) { + // if (session && response) { + // const sessionObj = JSON.parse(session) + // const connectDetails: ConnectDetails = { + // connected: true, + // chainId: sessionObj.networks.find(n => n.isDefaultChain).chainId, + // session: sessionObj + // } + + // TODO: remove finalizeConnect() and instead put it on the .register() method + // along with another hook.. + // wallet.finalizeConnect(connectDetails) + + // // signTypedData on defaultChain test prep + // console.log('... signTypedData on defaultChain ... prep step') + + // const address = await wallet.getAddress() + // console.log('... signTypedData on defaultChain ... getAddress', address) + // const chainId = await wallet.getChainId() + // console.log('... signTypedData on defaultChain ... getChainId', chainId) + + // const domain: TypedDataDomain = { + // name: 'Ether Mail', + // version: '1', + // chainId: chainId, + // verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' + // } + + // const types: { [key: string]: TypedDataField[] } = { + // Person: [ + // { name: 'name', type: 'string' }, + // { name: 'wallet', type: 'address' } + // ] + // } + + // const message = { + // name: 'Bob', + // wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' + // } + + // if (!response) { + // console.log('... signTypedData on defaultChain ... sig step') + // await signer.signTypedData(domain, types, message, undefined, undefined, address) + // } + + // const sig = decodedResponse.data.result + + // if (sig) { + // await test('signTypedData on defaultChain', async () => { + // assert.equal( + // sig, + // '0x00010001c25b59035ea662350e08f41b5087fc49a98b94936826b61a226f97e400c6ce290b8dfa09e3b0df82288fbc599d5b1a023a864bbd876bc67ec1f94c5f2fc4e6101b02', + // 'signature match typed-data' + // ) + + // // // Verify typed data + // // const isValid = await wallet.utils.isValidTypedDataSignature(address, { domain, types, message }, sig, chainId) + // // assert.true(isValid, 'signature is valid - 3') + + // // // Recover config / address + // // const walletConfig = await wallet.utils.recoverWalletConfigFromTypedData(address, { domain, types, message }, sig, chainId) + // // assert.true(walletConfig.address === address, 'recover address - 3') + + // // const singleSignerAddress = '0x4e37E14f5d5AAC4DF1151C6E8DF78B7541680853' // expected from mock-wallet owner + // // assert.true(singleSignerAddress === walletConfig.signers[0].address, 'owner address check') + // }) + // } + + // return + // } + + // if (!session) { + // // Finalize connect if there is already response + // if (response) { + // console.log('... we have a response...', decodedResponse) + // console.log('... data', JSON.stringify(decodedResponse.data, null, 2)) + + // if (decodedResponse.type === 'connect') { + // wallet.finalizeConnect(decodedResponse.data as ConnectDetails) + + // await test('isConnected', async () => { + // assert.true(wallet.isConnected(), 'is connected') + // }) + // } + + // // reset url, start with next test + // window.location.href = windowURL.href.split(/[?#]/)[0] + '?continue=true' + + // return + // } else { + // // Start connect + // await wallet.connect({ keepWalletOpened: true }) + // } + // } else { + // console.log('we have the session:', session) + // const address = await wallet.getAddress() + // console.log('wallet address is:', address) + // } +} + +// await test('getWalletConfig', async () => { +// console.log('... getWalletConfig test') +// const allWalletConfigs = await wallet.getWalletConfig() +// console.log('allWalletConfigs', allWalletConfigs) +// }) diff --git a/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts b/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts index 8a2bc0401..2b5c28d0c 100644 --- a/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts +++ b/packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts @@ -1,3 +1,6 @@ +// NOTE: run `pnpm test:server` and open browser at http://localhost:9999/wallet-provider/dapp.test.html +// to run tests from this file directly from your Web Browser. + import { test, assert } from '../../utils/assert' import { ethers, TypedDataDomain, TypedDataField } from 'ethers' import { Wallet, DefaultProviderConfig, isValidMessageSignature } from '@0xsequence/provider' @@ -51,7 +54,6 @@ export const tests = async () => { await test('connect', async () => { const { connected } = await wallet.connect({ keepWalletOpened: true, - redirectMode: true, }) assert.true(connected, 'is connected') }) diff --git a/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts b/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts index bf4e12cf3..907e8303a 100644 --- a/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts +++ b/packages/0xsequence/tests/browser/wallet-provider/dapp2.test.ts @@ -1,3 +1,6 @@ +// NOTE: run `pnpm test:server` and open browser at http://localhost:9999/wallet-provider/dapp2.test.html +// to run tests from this file directly from your Web Browser. + import { test, assert } from '../../utils/assert' import { TypedDataDomain, TypedDataField } from 'ethers' import { Wallet, DefaultProviderConfig } from '@0xsequence/provider' diff --git a/packages/0xsequence/tests/browser/window-transport/dapp.test.ts b/packages/0xsequence/tests/browser/window-transport/dapp.test.ts index 1c82c7405..92f92f2fd 100644 --- a/packages/0xsequence/tests/browser/window-transport/dapp.test.ts +++ b/packages/0xsequence/tests/browser/window-transport/dapp.test.ts @@ -1,3 +1,6 @@ +// NOTE: run `pnpm test:server` and open browser at http://localhost:9999/window-transport/dapp.test.html +// to run tests from this file directly from your Web Browser. + import { prefixEIP191Message, WindowMessageProvider } from '@0xsequence/provider' import { ethers } from 'ethers' import { test, assert } from '../../utils/assert' diff --git a/packages/0xsequence/tests/url-transport.spec.ts b/packages/0xsequence/tests/url-transport.spec.ts new file mode 100644 index 000000000..209eec080 --- /dev/null +++ b/packages/0xsequence/tests/url-transport.spec.ts @@ -0,0 +1,3 @@ +import { runBrowserTests } from './utils/browser-test-runner' + +runBrowserTests('url-transport/dapp', 'url-transport/dapp.test.html') diff --git a/packages/0xsequence/tests/webpack.config.js b/packages/0xsequence/tests/webpack.config.js index 0a9b7ccbd..174d783d4 100644 --- a/packages/0xsequence/tests/webpack.config.js +++ b/packages/0xsequence/tests/webpack.config.js @@ -6,7 +6,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin') const port = process.env['PORT'] || 9999 const appDirectory = fs.realpathSync(process.cwd()) -const resolveCwd = (relativePath) => path.resolve(appDirectory, relativePath) +const resolveCwd = relativePath => path.resolve(appDirectory, relativePath) const resolvePackages = () => { const pkgs = path.resolve(fs.realpathSync(process.cwd()), '..') @@ -21,7 +21,7 @@ const resolvePackages = () => { // Include extra sources for compilation. // -// NOTE: if you experience an error in your webpack builder such as, +// NOTE: if you experience an error in your webpack builder such as, // Module parse failed: Unexpected token (11:20) // You may need an appropriate loader to handle this file type, currently no loaders are // configured to process this file. See https://webpack.js.org/concepts#loaders @@ -34,13 +34,13 @@ const resolveExtras = [ resolveCwd('../../node_modules/@0xsequence/wallet-contracts/gen') ] -const resolveTestEntries = (location) => { +const resolveTestEntries = location => { return fs.readdirSync(location).reduce((list, f) => { const n = path.join(location, f) if (fs.lstatSync(n).isDirectory()) { list.push(...resolveTestEntries(n)) } else { - if (n.endsWith(".test.ts") > 0) list.push(n) + if (n.endsWith('.test.ts') > 0) list.push(n) } return list }, []) @@ -48,43 +48,69 @@ const resolveTestEntries = (location) => { const resolveEntry = () => { const browserTestRoot = fs.realpathSync(path.join(process.cwd(), 'tests', 'browser')) - const entry = { 'lib': './src/index.ts' } + const entry = { lib: './src/index.ts' } const testEntries = resolveTestEntries(browserTestRoot) - testEntries.forEach(v => entry[v.slice(browserTestRoot.length+1, v.length-3)] = v) + testEntries.forEach(v => (entry[v.slice(browserTestRoot.length + 1, v.length - 3)] = v)) return entry } -const resolveHtmlPlugins = (entry) => { +const resolveHtmlPlugins = entry => { const plugins = [] for (let k in entry) { if (k === 'lib') continue - plugins.push(new HtmlWebpackPlugin({ - inject: false, - filename: `${k}.html`, - templateContent: htmlTemplate(k) - })) + plugins.push( + new HtmlWebpackPlugin({ + inject: false, + filename: `${k}.html`, + templateContent: htmlTemplate(k) + }) + ) } return plugins } -const htmlTemplate = (k) => ` +const htmlTemplate = k => ` test +

${k}

+
` @@ -118,12 +151,10 @@ module.exports = { loader: require.resolve('babel-loader'), options: { presets: ['@babel/preset-typescript'], - plugins: [ - [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }] - ], + plugins: [[require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }]], cacheCompression: false, - compact: false, - }, + compact: false + } }, { test: /\.(jpe?g|png|gif|svg)$/i, diff --git a/packages/provider/src/provider.ts b/packages/provider/src/provider.ts index c53eb03cc..0bdb3d17c 100644 --- a/packages/provider/src/provider.ts +++ b/packages/provider/src/provider.ts @@ -242,7 +242,8 @@ export class Web3Signer extends Signer implements TypedDataSigner { types: Record>, message: Record, chainId?: ChainIdLike, - allSigners?: boolean + allSigners?: boolean, + address?: string ): Promise { // Populate any ENS names (in-place) // const populated = await ethers.utils._TypedDataEncoder.resolveNames(domain, types, message, (name: string) => { @@ -251,7 +252,7 @@ export class Web3Signer extends Signer implements TypedDataSigner { return await this.provider.send( 'eth_signTypedData_v4', - [await this.getAddress(), ethers.utils._TypedDataEncoder.getPayload(domain, types, message)], + [address ?? (await this.getAddress()), ethers.utils._TypedDataEncoder.getPayload(domain, types, message)], maybeChainId(chainId) || this.defaultChainId ) } diff --git a/packages/provider/src/transports/base-provider-transport.ts b/packages/provider/src/transports/base-provider-transport.ts index c8749ba90..ff44d55a9 100644 --- a/packages/provider/src/transports/base-provider-transport.ts +++ b/packages/provider/src/transports/base-provider-transport.ts @@ -125,6 +125,14 @@ export abstract class BaseProviderTransport implements ProviderTransport { // handleMessage will handle message received from the remote wallet handleMessage(message: ProviderMessage) { + console.log('yesssss...............................', message) + console.log('yesssss...............................', message) + console.log('yesssss...............................', message) + console.log('yesssss...............................', message) + console.log('yesssss...............................', message) + console.log('yesssss...............................', message) + console.log('handleMessage', message.type) + // init incoming for initial handshake with transport. // always respond to INIT messages, e.g. on popup window reload if (message.type === EventType.INIT) { @@ -211,7 +219,7 @@ export abstract class BaseProviderTransport implements ProviderTransport { // NOTE: this would occur if 'idx' isn't set, which should never happen // or when we register two handler, or duplicate messages with the same idx are sent, // all of which should be prevented prior to getting to this point - throw new Error('impossible state') + throw new Error('impossible state, no response callback for message') } // Callback to original caller @@ -266,7 +274,9 @@ export abstract class BaseProviderTransport implements ProviderTransport { // NOTIFY CONNECT -- when wallet instructs we've connected if (message.type === EventType.CONNECT) { + console.log('message.type === EventType.CONNECT') this.connectPayload = message.data + console.log('..???? paload??????????????? connnect...???', this.connectPayload) this.events.emit('connect', this.connectPayload!) } diff --git a/packages/provider/src/transports/base-wallet-transport.ts b/packages/provider/src/transports/base-wallet-transport.ts index 7bb8b6456..fc45ea182 100644 --- a/packages/provider/src/transports/base-wallet-transport.ts +++ b/packages/provider/src/transports/base-wallet-transport.ts @@ -98,6 +98,8 @@ export abstract class BaseWalletTransport implements WalletTransport { } handleMessage = async (message: ProviderMessage) => { + console.log('... base-wallet-transport ... handleMessage ...', message) + const request = message // ensure initial handshake is complete before accepting @@ -161,6 +163,7 @@ export abstract class BaseWalletTransport implements WalletTransport { // sendMessageRequest sends a ProviderMessageRequest to the wallet post-message transport sendMessageRequest = async (message: ProviderMessageRequest): Promise => { + console.log('... base-wallet-transport ... sendMessageRequest ...', message) return this.walletRequestHandler.sendMessageRequest(message) } @@ -170,6 +173,7 @@ export abstract class BaseWalletTransport implements WalletTransport { notifyOpen(openInfo: { chainId?: string; sessionId?: string; session?: WalletSession; error?: string }) { const { chainId, sessionId, session, error } = openInfo + console.log('... base-wallet-transport ... notifyOpen ... error?', openInfo.error) this.sendMessage({ idx: -1, type: EventType.OPEN, @@ -328,6 +332,8 @@ export abstract class BaseWalletTransport implements WalletTransport { // origin host of the dapp. await this.init() + console.log('waaa?', intent) + // Prepare connect options from intent if (intent && intent.type === 'connect' && intent.options) { const connectOptions = intent.options @@ -370,8 +376,11 @@ export abstract class BaseWalletTransport implements WalletTransport { // ensure signer is ready await this.walletRequestHandler.getSigner() + console.log('wazzzzzzzzzzup???????????????????????????????????????') + // Notify open and proceed to prompt for connection if intended if (!(await this.walletRequestHandler.isSignedIn())) { + console.log('... not signed in???') // open wallet without a specific connected chainId, as the user is not signed in this.notifyOpen({ sessionId: this._sessionId @@ -400,6 +409,8 @@ export abstract class BaseWalletTransport implements WalletTransport { console.log('Failed to set default network on open') } + console.log('intent connect ????') + // notify wallet is opened, without session details this.notifyOpen({ sessionId: this._sessionId @@ -443,6 +454,7 @@ export abstract class BaseWalletTransport implements WalletTransport { // user is already connected, notify session details. // TODO: in future, keep list if 'connected' dapps / sessions in the session // controller, and only sync with allowed apps + console.log('... already connected') this.notifyOpen({ sessionId: this._sessionId, chainId: `${chainId}`, diff --git a/packages/provider/src/transports/extension-transport/base-injected-transport.ts b/packages/provider/src/transports/extension-transport/base-injected-transport.ts index 94d734dbb..8e9c7e545 100644 --- a/packages/provider/src/transports/extension-transport/base-injected-transport.ts +++ b/packages/provider/src/transports/extension-transport/base-injected-transport.ts @@ -50,7 +50,7 @@ export abstract class BaseInjectedTransport extends EventEmitter { // NOTE: this would occur if 'idx' isn't set, which should never happen // or when we register two handler, or duplicate messages with the same idx are sent, // all of which should be prevented prior to getting to this point - throw new Error('impossible state') + throw new Error('impossible state, no response callback for message') } break case EventType.DISCONNECT: diff --git a/packages/provider/src/transports/index.ts b/packages/provider/src/transports/index.ts index 35f06a3af..bd2c4657d 100644 --- a/packages/provider/src/transports/index.ts +++ b/packages/provider/src/transports/index.ts @@ -6,3 +6,4 @@ export * from './window-transport' export * from './wallet-request-handler' export * from './extension-transport' export * from './unreal-transport' +export * from './url-transport' diff --git a/packages/provider/src/transports/url-transport/browser-redirect-hooks.ts b/packages/provider/src/transports/url-transport/browser-redirect-hooks.ts new file mode 100644 index 000000000..7b960baae --- /dev/null +++ b/packages/provider/src/transports/url-transport/browser-redirect-hooks.ts @@ -0,0 +1,16 @@ +import { OpenWalletIntent, ProviderRpcError, ProviderMessageResponse, ProviderMessage } from '../../types' +import { UrlMessageProviderHooks } from './url-message-provider' + +export class BrowserRedirectMessageHooks implements UrlMessageProviderHooks { + openWallet = (walletBaseUrl: string): void => { + console.log('BrowserRedirectMessageHooks ....???... here...?') + console.log('BrowserRedirectMessageHooks openWallet', walletBaseUrl) + window.location.href = walletBaseUrl + + // on 0xsequence/react-native, we have to implement UrlMessageHooks + // which will call InAppBrowser.open() etc......... + } + + // responseFromRedirectUrl = (callback: (response: string) => void): void => {} + responseFromRedirectUrl = (response: string) => {} +} diff --git a/packages/provider/src/transports/url-transport/index.ts b/packages/provider/src/transports/url-transport/index.ts new file mode 100644 index 000000000..b471be7ed --- /dev/null +++ b/packages/provider/src/transports/url-transport/index.ts @@ -0,0 +1,3 @@ +export * from './url-message-provider' +export * from './url-message-handler' +export * from './browser-redirect-hooks' diff --git a/packages/provider/src/transports/url-transport/url-message-handler.ts b/packages/provider/src/transports/url-transport/url-message-handler.ts new file mode 100644 index 000000000..79bc1c87e --- /dev/null +++ b/packages/provider/src/transports/url-transport/url-message-handler.ts @@ -0,0 +1,153 @@ +import { BaseWalletTransport } from '../base-wallet-transport' +import { WalletRequestHandler } from '../wallet-request-handler' +import { + EventType, + InitState, + OpenWalletIntent, + ProviderMessage, + ProviderRpcError, + TransportSession, + WindowSessionParams +} from '../../types' +import { base64DecodeObject, base64EncodeObject, logger } from '@0xsequence/utils' + +export class UrlMessageHandler extends BaseWalletTransport { + private _pathname: string + private _redirectUrl: string = '' + private _redirecting: boolean = false + private _messages: ProviderMessage[] = [] + + private _lastMessageAt: number = 0 + + constructor(walletRequestHandler: WalletRequestHandler, pathname: string) { + super(walletRequestHandler) + this._init = InitState.OK + this._pathname = pathname + } + + async register() { + const { pathname, search: rawParams } = new URL(window.location.href) + // TODO: do we need this? + // if (pathname !== this._pathname) { + // return + // } + + console.log('... url message handler register ...') + + const params = new URLSearchParams(rawParams) + const redirectUrl = params.get('redirectUrl') + const intent = base64DecodeObject(params.get('intent')) as OpenWalletIntent + const request = base64DecodeObject(params.get('request')) as ProviderMessage + this._redirectUrl = redirectUrl! + console.log('intent', intent) + console.log('request', request) + console.log('request data', JSON.stringify(request?.data, null, 2)) + + // TODO: ensure we have both of these, otherwise just return and skip, + // maybe though console.warn + + let session: TransportSession | null = this.getWindowTransportSession(rawParams) + + // provider should always include sid when opening a new window + const isNewWindowSession = !!session.sessionId + + // attempt to restore previous session in the case of a redirect or window reload + if (!isNewWindowSession) { + session = await this.getCachedTransportSession() + } + + if (!session) { + logger.error('window session is undefined') //... + return + } + + this._registered = true + this.open(session).catch(e => { + const err = `failed to open to network ${session?.networkId}, due to: ${e}` + logger.error(err) + // this.notifyClose({ message: err } as ProviderRpcError) + // redirect?sup....??? lalala + // window.close() + }) + + if (request) { + console.log('requesting..............', request) + const response = await this.sendMessageRequest(request) + this.sendMessage(response) + console.log('... sendMessageRequest response ...', response) + console.log('... sendMessageRequest response data ...', JSON.stringify(response, null, 2)) + } + } + + unregister() { + this._registered = false + } + + sendMessage(message: ProviderMessage) { + if (this._redirecting) { + return + } + + console.log('...URL-HANDLER (wallet) sendMessage in url-message-handler ...', message) + console.log('...URL-HANDLER (wallet) sendMessage data ...', message.data) + + if (message.type === EventType.OPEN) { + console.log('open message, but well skip it..') + return + } + + // TODO: remove this.. we want to keep close.. + if (message.type === EventType.CLOSE) { + console.log('close message, but well skip it..') + return + } + + // NOTE: maybe we check the 'connect' object, and we shorten it..? it will be quite long + // .. we can just rely on the lib to have all of the networks listed..? if anything + // we can just return a list like networks: [1, 137, etc..] + + // respond with the first message, no waiting around.. take the first message + this._redirecting = true + const redirectUrl = new URL(this._redirectUrl) + redirectUrl.hash = 'response='+base64EncodeObject(message) + window.location.href = redirectUrl.href + + + // this._messages.push(message) + // this._lastMessageAt = Date.now() + + // // temporary, just to send back connect so that we don't navigate for INIT etc. + // //.. + // setTimeout(() => { + // // if (this._lastMessageAt == 0 || this._lastMessageAt + 1000 > Date.now()) { + // // return + // // } else { + // // this._lastMessageAt = 0 + // const connectMessage = this._messages.find(m => m.type === EventType.CONNECT) + // if (connectMessage) { + // this._redirecting = true + // const redirectUrl = new URL(this._redirectUrl) + // redirectUrl.hash = 'response='+base64EncodeObject(message) + // window.location.href = redirectUrl.href + // } + // // const lastMessage = this._messages[this._messages.length - 1] + // // const redirectUrl = new URL(this._redirectUrl) + + // // redirectUrl.hash = 'response='+base64EncodeObject(connectMessage ?? lastMessage) + + // // // redirectUrl.searchParams.set('response', base64EncodeObject(connectMessage ?? lastMessage)) + + // // window.location.href = redirectUrl.href + // // } + // }, 3000) + } + + private getWindowTransportSession = (windowParams: string | undefined): TransportSession => { + const params = new WindowSessionParams(windowParams) + return { + sessionId: params.get('sid'), + networkId: params.get('net'), + intent: base64DecodeObject(params.get('intent')) + } + } +} diff --git a/packages/provider/src/transports/url-transport/url-message-provider.ts b/packages/provider/src/transports/url-transport/url-message-provider.ts new file mode 100644 index 000000000..9f1363632 --- /dev/null +++ b/packages/provider/src/transports/url-transport/url-message-provider.ts @@ -0,0 +1,166 @@ +import { BaseProviderTransport, nextMessageIdx } from '../base-provider-transport' +import { Wallet } from '../../wallet' +import { ProviderMessage, OpenWalletIntent, EventType, WalletSession, InitState, ConnectDetails, ProviderEventTypes } from '../../types' +import { base64DecodeObject, base64EncodeObject } from '@0xsequence/utils' +import { JsonRpcRequest, JsonRpcResponseCallback } from '@0xsequence/network' + +export interface UrlMessageProviderHooks { + openWallet(walletUrl: string): void + + // responseFromRedirectUrl(callback: (response: string) => void): void + responseFromRedirectUrl(response: string): void +} + +export class UrlMessageProvider extends BaseProviderTransport { + private _wallet: Wallet + private _walletBaseUrl: string + private _redirectUrl: string + private _hooks: UrlMessageProviderHooks + private _connectDetails: ConnectDetails | undefined + + constructor(wallet: Wallet, walletBaseUrl: string, redirectUrl: string, hooks: UrlMessageProviderHooks) { + super() + console.log('UrlMessageProvider walletBaseUrl:', walletBaseUrl) + this._init = InitState.OK + this._wallet = wallet + this._walletBaseUrl = walletBaseUrl + this._redirectUrl = redirectUrl + this._hooks = hooks + } + + on(event: K, fn: ProviderEventTypes[K]) { + this.events.on(event, fn as any) + if (event === 'connect' && this._connectDetails) { + this.events.emit('connect', this._connectDetails) + } + } + + once(event: K, fn: ProviderEventTypes[K]) { + this.events.once(event, fn as any) + if (event === 'connect' && this._connectDetails) { + this.events.emit('connect', this._connectDetails) + } + } + + register = async () => { + console.log('... URL MESSAGE PROVIDER ... register...????') + + this.events.on('connect', connectDetails => { + console.log('url messagep provider got connect, connectDetails..', connectDetails) + this._connectDetails = connectDetails + + if (connectDetails.connected) { + if (!!connectDetails.session) { + this._wallet.useSession(connectDetails.session, true) + // this.addConnectedSite(options?.origin) + } else { + throw new Error('impossible state, connect response is missing session') + } + } + }) + + this._hooks.responseFromRedirectUrl = (response: string) => { + const decodedResponse = base64DecodeObject(response) as ProviderMessage + console.log('... we have a response...zzzzzz', decodedResponse) + this.handleMessage(decodedResponse) + } + + this._registered = true + + // const windowURL = new URL(window.location.href) + const prefix = '#response=' + console.log('sup....??? lalala', window.location.hash) + if (window.location.hash.startsWith(prefix)) { + const response = window.location.hash.substring(prefix.length) + console.log('=> response', response) + window.location.hash = '' + this._hooks.responseFromRedirectUrl(response) + } + } + + unregister = () => { + this._registered = false + // this._hooks ..? + } + + openWallet = (path?: string, intent?: OpenWalletIntent, networkId?: string | number): void => { + console.log('url message provider......... openWallet', path, intent) + + this._sessionId = `${performance.now()}` + + const openUrl = this.buildWalletOpenUrl(this._sessionId, path, intent, networkId) + + // const walletRequestUrl = this._walletBaseUrl + '?request=XX&redirectUrl=' + this._redirectUrl + this._hooks.openWallet(openUrl.toString()) + } + + closeWallet() { + // this._hooks.closeWallet() + } + + sendMessage(message: ProviderMessage) { + if (!message.idx) { + throw new Error('message idx is empty') + } + + console.log('url message provider......... sendMessage', message) + + const encodedRequest = base64EncodeObject(message) + const walletUrl = new URL(this._walletBaseUrl) + walletUrl.searchParams.set('request', encodedRequest) + walletUrl.searchParams.set('redirectUrl', this._redirectUrl) + console.log('.... walletURL ..', walletUrl) + + this._hooks.openWallet(walletUrl.toString()) + } + + private buildWalletOpenUrl( + sessionId: string, + path?: string, + intent?: OpenWalletIntent, + networkId?: string | number, + request?: string + ): URL { + const walletURL = new URL(this._walletBaseUrl) + if (path && path !== '') { + walletURL.pathname = path.toLowerCase() + } + + // Make sure at least the app name is forced on Mobile SDK and intent is never undefined + walletURL.searchParams.set('sid', sessionId) + if (intent) { + walletURL.searchParams.set('intent', base64EncodeObject(intent)) + } + walletURL.searchParams.set('redirectUrl', this._redirectUrl) + if (request) { + walletURL.searchParams.set('request', request) + } + + if (networkId) { + walletURL.searchParams.set('net', `${networkId}`) + } + + console.log('.... walletURL ..', walletURL.toString()) + + return walletURL + } + + sendAsync = async (request: JsonRpcRequest, callback: JsonRpcResponseCallback, chainId?: number) => { + console.log('... url message provider......... sendAsync', request) + const encodedRequest = base64EncodeObject({ + idx: nextMessageIdx(), + type: EventType.MESSAGE, + data: request, + chainId: chainId + }) + + const openUrl = this.buildWalletOpenUrl(this._sessionId!, undefined, undefined, chainId, encodedRequest) + this._hooks.openWallet(openUrl.href) + } + + waitUntilOpened = async (openTimeout = 0): Promise => { + // noop + return undefined + } + +} diff --git a/packages/provider/src/transports/wallet-request-handler.ts b/packages/provider/src/transports/wallet-request-handler.ts index ea7c1b6ec..53d8555e7 100644 --- a/packages/provider/src/transports/wallet-request-handler.ts +++ b/packages/provider/src/transports/wallet-request-handler.ts @@ -103,6 +103,7 @@ export class WalletRequestHandler implements ExternalProvider, JsonRpcHandler, P // for this dapp, so its safe to authorize in the connect() method without the prompt. // // NOTE: signIn can optionally connect and notify dapp at this time for new signIn flows + console.log('...? sup...? connect?', connect) if (connect) { const connectOptions = this._connectOptions @@ -715,6 +716,7 @@ export class WalletRequestHandler implements ExternalProvider, JsonRpcHandler, P } notifyConnect(connectDetails: ConnectDetails, origin?: string) { + console.log('hess........ notifyCOnnect from wallet-request-handler', connectDetails) this.events.emit('connect', connectDetails) if (connectDetails.session?.accountAddress) { this.events.emit('accountsChanged', [connectDetails.session?.accountAddress], origin) diff --git a/packages/provider/src/types.ts b/packages/provider/src/types.ts index ce9b9bbff..31f5751cd 100644 --- a/packages/provider/src/types.ts +++ b/packages/provider/src/types.ts @@ -166,6 +166,9 @@ export interface ConnectOptions { * is to automatically close the wallet after connecting. */ keepWalletOpened?: boolean + /** */ + redirectMode?: boolean + /** Options to further customize the wallet experience. */ settings?: Settings } diff --git a/packages/provider/src/utils.ts b/packages/provider/src/utils.ts index 707d57108..7b49f1d56 100644 --- a/packages/provider/src/utils.ts +++ b/packages/provider/src/utils.ts @@ -160,7 +160,7 @@ export class LocalStorage { } } -// window.localstorage helper +// window.localStorage helper export class LocalStore { readonly key: string diff --git a/packages/provider/src/wallet.ts b/packages/provider/src/wallet.ts index 90b0f3a88..5cd7a9abe 100644 --- a/packages/provider/src/wallet.ts +++ b/packages/provider/src/wallet.ts @@ -26,7 +26,9 @@ import { WindowMessageProvider, ProxyMessageProvider, ProxyMessageChannelPort, - UnrealMessageProvider + UnrealMessageProvider, + UrlMessageProvider, + UrlMessageProviderHooks } from './transports' import { WalletSession, ProviderEventTypes, ConnectOptions, OpenWalletIntent, ConnectDetails } from './types' import { ethers, providers } from 'ethers' @@ -36,8 +38,11 @@ import { WalletUtils } from './utils/index' import { Runtime } from 'webextension-polyfill-ts' + export interface WalletProvider { + // connect(options?: M): M extends RedirectMode ? void : Promise connect(options?: ConnectOptions): Promise + connectWithRedirect(options?: ConnectOptions): void disconnect(): void isConnected(): boolean @@ -89,6 +94,7 @@ export class Wallet implements WalletProvider { // message communication messageProvider?: MuxMessageProvider windowMessageProvider?: WindowMessageProvider + urlMessageProvider?: UrlMessageProvider proxyMessageProvider?: ProxyMessageProvider extensionMessageProvider?: ExtensionMessageProvider unrealMessageProvider?: UnrealMessageProvider @@ -137,6 +143,14 @@ export class Wallet implements WalletProvider { this.transport.windowMessageProvider = new WindowMessageProvider(this.config.walletAppURL) this.transport.messageProvider.add(this.transport.windowMessageProvider) } + if (this.config.transports?.urlTransport?.enabled) { + const redirectUrl = this.config.transports?.urlTransport?.redirectUrl + const hooks = this.config.transports?.urlTransport?.hooks + if (redirectUrl && hooks) { + this.transport.urlMessageProvider = new UrlMessageProvider(this, this.config.walletAppURL, redirectUrl, hooks) + this.transport.messageProvider.add(this.transport.urlMessageProvider) + } + } if (this.config.transports?.proxyTransport?.enabled) { this.transport.proxyMessageProvider = new ProxyMessageProvider(this.config.transports.proxyTransport.appPort!) this.transport.messageProvider.add(this.transport.proxyMessageProvider) @@ -158,6 +172,7 @@ export class Wallet implements WalletProvider { this.transport.unrealMessageProvider = new UnrealMessageProvider(this.config.walletAppURL) this.transport.messageProvider.add(this.transport.unrealMessageProvider) } + this.transport.messageProvider.register() // ..... @@ -182,6 +197,7 @@ export class Wallet implements WalletProvider { // Provider proxy to support middleware stack of logging, caching and read-only rpc calls this.transport.cachedProvider = new CachedProvider() + console.log('cachdproviderrrrr:', this.transport.cachedProvider) this.transport.cachedProvider.onUpdate(() => { if (!this.session) this.session = { providerCache: {} } this.session.providerCache = this.transport.cachedProvider!.getCache() @@ -281,6 +297,19 @@ export class Wallet implements WalletProvider { } } + // connect(options?: M): M extends RedirectMode ? void : Promise + // connect(options?: ConnectOptions): void | Promise { + // if (options?.refresh === true) { + // this.disconnect() + // } + // if (options?.redirectMode) { + // this._connectWithRedirect(options) + // return + // } else { + // return this._connect(options) + // } + // } + connect = async (options?: ConnectOptions): Promise => { if (options?.refresh === true) { this.disconnect() @@ -288,7 +317,7 @@ export class Wallet implements WalletProvider { if ( this.isConnected() && - (await this.isSiteConnected(options?.origin)) && + // (await this.isSiteConnected(options?.origin)) && !!this.session && !options?.authorize && !options?.askForEmail @@ -306,7 +335,12 @@ export class Wallet implements WalletProvider { } } - await this.openWallet(undefined, { type: 'connect', options }) + if (options?.redirectMode) { + this.openWallet(undefined, { type: 'connect', options }) + return { connected: false } + } else { + await this.openWallet(undefined, { type: 'connect', options }) + } const connectDetails = await this.transport.messageProvider!.waitUntilConnected().catch((error): ConnectDetails => { if (error instanceof Error) { @@ -329,6 +363,10 @@ export class Wallet implements WalletProvider { return connectDetails } + connectWithRedirect = (options?: ConnectOptions): void => { + this.connect({ ...options, redirectMode: true }) + } + async addConnectedSite(origin: string | undefined) { origin = origin || window.location.origin @@ -385,6 +423,9 @@ export class Wallet implements WalletProvider { } isConnected(): boolean { + // log all checks below + console.log('this.session', this.session) + console.log('this.networks', this.networks) return ( this.session !== undefined && this.session.networks !== undefined && @@ -594,7 +635,7 @@ export class Wallet implements WalletProvider { await LocalStorage.getInstance().setItem('@sequence.session', data) } - private useSession = async (session: WalletSession, autoSave: boolean = true) => { + useSession = async (session: WalletSession, autoSave: boolean = true) => { if (!this.session) this.session = {} // setup wallet context @@ -615,10 +656,18 @@ export class Wallet implements WalletProvider { } // setup provider cache - if (session.providerCache) { - this.transport.cachedProvider!.setCache(session.providerCache) + console.log('???? session.providerCache', session.providerCache) + console.log('???? this.transport.cachedProvider', this.transport.cachedProvider) + + if (this.transport.cachedProvider) { + if (session.providerCache) { + this.transport.cachedProvider.setCache(session.providerCache) + } else { + console.log('therooo...........', this.transport.cachedProvider!.clearCache) + this.transport.cachedProvider.clearCache() + } } else { - this.transport.cachedProvider!.clearCache() + console.log('noooo???????????????????????? CACHE???????????????????') } // persist @@ -681,11 +730,22 @@ export class Wallet implements WalletProvider { this.providers = {} this.transport.cachedProvider?.clearCache() } + + static load = async (network?: string | number, config?: Partial) => { + if (walletInstance) { + return walletInstance + } else { + walletInstance = new Wallet(network, config) + await walletInstance.loadSession(network) + return walletInstance + } + } } export interface ProviderConfig { // The local storage dependency for the wallet provider, defaults to window.localStorage. // For example, this option should be used when using React Native since window.localStorage is not available. + // TODO: rename localStorag to like cacheStore or sessionStore localStorage?: ItemStore // Sequence Wallet App URL, default: https://sequence.app @@ -714,6 +774,12 @@ export interface ProviderConfig { enabled: boolean } + urlTransport?: { + enabled: boolean + redirectUrl?: string + hooks?: UrlMessageProviderHooks + } + // ProxyMessage transport (optional) proxyTransport?: { enabled: boolean @@ -746,7 +812,8 @@ export const DefaultProviderConfig: ProviderConfig = { transports: { windowTransport: { enabled: true }, - proxyTransport: { enabled: false } + proxyTransport: { enabled: false }, + urlTransport: { enabled: false } } }