Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multi blockchain wallet overhaul #699

Open
wants to merge 7 commits into
base: staging
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/client/auth/signedUNChallenge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent
): Promise<AuthenticationResult> {
let web3WalletProvider = await this.client.getWalletProvider()

// TODO: Update once Flow & Solana signing support is added
let connection = web3WalletProvider.getSingleSignatureCompatibleConnection()
let connection =
web3WalletProvider.getConnectionByAddress(_tokens[0].walletAddress) ?? web3WalletProvider.getSingleSignatureCompatibleConnection()
if (!connection) {
throw new Error('WALLET_REQUIRED')
}
Expand All @@ -32,7 +32,7 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent
if (currentProof) {
let unChallenge = currentProof?.data as UNInterface

if (unChallenge.expiration < Date.now() || !UN.verifySignature(unChallenge)) {
if (unChallenge.expiration < Date.now() || !(await UN.verifySignature(unChallenge))) {
this.deleteProof(address)
currentProof = null
}
Expand Down
71 changes: 45 additions & 26 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { SignedUNChallenge } from './auth/signedUNChallenge'
import { TicketZKProof } from './auth/ticketZKProof'
import { AuthenticationMethod } from './auth/abstractAuthentication'
import { isUserAgentSupported, validateBlockchain } from '../utils/support/isSupported'
import Web3WalletProvider from '../wallet/Web3WalletProvider'
import Web3WalletProvider, { SupportedWalletProviders } from '../wallet/Web3WalletProvider'
import { LocalOutlet } from '../outlet/localOutlet'
import { Outlet, OutletInterface } from '../outlet'
import { shouldUseRedirectMode } from '../utils/support/getBrowserData'
Expand Down Expand Up @@ -86,7 +86,7 @@ export const defaultConfig: NegotiationInterface = {
export const enum UIUpdateEventType {
ISSUERS_LOADING,
ISSUERS_LOADED,
WALLET_DISCONNECTED,
WALLET_CHANGE,
}

export enum ClientError {
Expand All @@ -110,7 +110,7 @@ export class Client {
private uiUpdateCallbacks: { [type in UIUpdateEventType] } = {
[UIUpdateEventType.ISSUERS_LOADING]: undefined,
[UIUpdateEventType.ISSUERS_LOADED]: undefined,
[UIUpdateEventType.WALLET_DISCONNECTED]: undefined,
[UIUpdateEventType.WALLET_CHANGE]: undefined,
}

private urlParams: URLSearchParams
Expand Down Expand Up @@ -255,13 +255,30 @@ export class Client {
return this.web3WalletProvider
}

public async disconnectWallet() {
public async disconnectWallet(walletAddress?: string, providerType?: string) {
let wp = await this.getWalletProvider()
wp.deleteConnections()
this.tokenStore.clearCachedTokens()

if (walletAddress) {
const deleted = wp.deleteConnection(walletAddress, providerType as SupportedWalletProviders)

if (!deleted) return

this.tokenStore.clearCachedTokens(true, walletAddress)
} else {
wp.deleteConnections()
this.tokenStore.clearCachedTokens()
}

// TODO: Deprecate use of connected-wallet events for disconnecting wallet
this.eventSender('connected-wallet', null)

// Emit disconnected wallet details
this.eventSender('disconnected-wallet', null)
this.triggerUiUpdateCallback(UIUpdateEventType.WALLET_DISCONNECTED)
this.triggerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE)

this.eventSender('tokens-selected', {
selectedTokens: this.tokenStore.getSelectedTokens(),
})
}

async negotiatorConnectToWallet(walletType: string) {
Expand Down Expand Up @@ -335,10 +352,6 @@ export class Client {
return this.config.type === 'active'
}

private createCurrentUrlWithoutHash(): string {
return window.location.origin + window.location.pathname + window.location.search ?? '?' + window.location.search
}

public getNoTokenMsg(collectionID: string) {
const store = this.getTokenStore().getCurrentIssuers()
const collectionNoTokenMsg = store[collectionID]?.noTokenMsg
Expand Down Expand Up @@ -648,26 +661,32 @@ export class Client {
private async loadOnChainTokens(issuer: OnChainIssuer): Promise<any[]> {
let walletProvider = await this.getWalletProvider()

// TODO: Collect tokens from all addresses for this blockchain
const walletAddress = walletProvider.getConnectedWalletAddresses(issuer.blockchain)?.[0]
const walletAddresses = walletProvider.getConnectedWalletAddresses(issuer.blockchain)

if (!walletAddresses.length) {
throw new Error('WALLET_REQUIRED')
}

requiredParams(walletAddress, 'wallet address is missing.')
const combinedTokens = []

// TODO: Allow API to return tokens for multiple addresses
let tokens
for (const walletAddress of walletAddresses) {
let tokens

if (issuer.fungible) {
tokens = await getFungibleTokenBalances(issuer, walletAddress)
} else {
tokens = await getNftTokens(issuer, walletAddress)
}
if (issuer.fungible) {
tokens = await getFungibleTokenBalances(issuer, walletAddress)
} else {
tokens = await getNftTokens(issuer, walletAddress)
}

tokens.map((token) => {
token.walletAddress = walletAddress
return token
})
tokens.map((token) => {
token.walletAddress = walletAddress
return token
})

return tokens
combinedTokens.push(...tokens)
}

return combinedTokens
}

private async loadRemoteOutletTokens(issuer: OffChainTokenConfig): Promise<unknown[] | void> {
Expand Down
12 changes: 9 additions & 3 deletions src/client/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { ViewInterface, ViewComponent, ViewFactory, ViewConstructor } from './vi
import { TokenStore } from './tokenStore'
import { SelectIssuers } from './views/select-issuers'
import { SelectWallet } from './views/select-wallet'
import { ManageWallets } from './views/manage-wallets'

export type UIType = 'popup' | 'inline' // TODO: implement modal too
export type PopupPosition = 'bottom-right' | 'bottom-left' | 'top-left' | 'top-right'
export type UItheme = 'light' | 'dark'
export type ViewType = 'start' | 'main' | 'wallet' | string
export type ViewType = 'start' | 'main' | 'wallet' | 'manage-wallets' | string

export interface UIOptionsInterface {
uiType?: UIType
Expand Down Expand Up @@ -136,6 +137,10 @@ export class Ui implements UiInterface {
return SelectIssuers
case 'wallet':
return SelectWallet
case 'manage-wallets':
return ManageWallets
default:
throw new Error("Default view '" + type + "' not found")
}
}

Expand Down Expand Up @@ -226,7 +231,7 @@ export class Ui implements UiInterface {
}
}

updateUI(viewFactory: ViewComponent | ViewType, data?: any, options?: any) {
updateUI(viewFactory: ViewComponent | ViewType, data?: any, viewOpts?: any) {
let viewOptions: any = {}
let viewName = 'unknown'

Expand All @@ -243,7 +248,8 @@ export class Ui implements UiInterface {
if (data?.viewName) viewName = data.viewName
}

if (options) viewOptions = { ...viewOptions, ...options }
// Manually specified view options can override ones set in the viewOverrides config
if (viewOpts) viewOptions = { ...viewOptions, ...viewOpts }

if (!this.viewContainer) {
logger(3, 'Element .view-content-tn not found: popup not initialized')
Expand Down
146 changes: 143 additions & 3 deletions src/client/views/manage-wallets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,149 @@
import { AbstractView } from './view-interface'
import { SupportedBlockchainsParam } from '../interface'
import { UIUpdateEventType } from '../index'
import { getWalletInfo } from './utils/wallet-info'

class ManageWallets extends AbstractView {
render() {
const DisconnectButtonSVG = `
<svg width="12px" height="100%" viewBox="0 0 384 384" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="Layer1">
<path d="M194.449,-0.378L194.449,29.622L29.577,29.622C29.577,95.909 30.577,354.191 30.577,354.191L194.449,354.191L194.449,384.191L16.077,384.191C7.517,384.191 0.577,377.251 0.577,368.691L0.577,15.122C0.577,6.562 7.517,-0.378 16.077,-0.378L194.449,-0.378Z"/>
<g transform="matrix(1.39537,0,0,2.43013,-54.9803,-262.053)">
<path d="M99.772,200.171L99.772,165.725L228.493,165.725L228.493,133.741L314.191,182.948L228.493,232.156L228.493,200.171L99.772,200.171Z"/>
</g>
</g>
</svg>
`

export class ManageWallets extends AbstractView {
init() {
this.client.registerUiUpdateCallback(UIUpdateEventType.WALLET_CHANGE, async () => {
const wallets = await this.client.getWalletProvider()

if (wallets.getConnectionCount() === 0) {
this.ui.updateUI('wallet', { viewName: 'wallet' }, { viewTransition: 'slide-in-left' })
} else {
await this.render()
}
})
}

async render() {
this.viewContainer.innerHTML = `
<h1>Wallet Connections Here!</h1>
<div class="inner-content-tn" style="flex-direction: column; width: 100% !important;">
<div class="scroll-tn">
<div class="headline-container-tn">
<div>
<div class="brand-tn">
<button aria-label="back to token issuer menu" class="back-to-menu-tn">
<svg style="position: relative; top: 1px;" width="20" height="20" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="m10.2 15.8 7.173 7.56c.55.587 1.453.587 2.01 0a1.554 1.554 0 0 0 0-2.12l-5.158-5.44 5.157-5.44a1.554 1.554 0 0 0 0-2.12 1.367 1.367 0 0 0-2.009 0L10.2 15.8z" fill="#000" fill-rule="nonzero"/>
</g>
</svg>
</button>
</div>
<div style="flex-grow: 1;">
<p class="headline-tn">My Wallets</p>
</div>
<div class="toolbar-tn">
<button class="btn-tn add-wallet-tn" aria-label="Add another wallet" title="Add another wallet">
<strong style="font-size: 20px; line-height: 12px;">+</strong>
</button>
<button class="btn-tn dis-wallet-tn" aria-label="Disconnect all wallets" title="Disconnect all wallets">
${DisconnectButtonSVG}
</button>
</div>
</div>
</div>
<div class="wallet-list-tn" style="width: 100%;">
${await this.renderCurrentWalletConnections()}
</div>
</div>
</div>
`

this.setupWalletButtons()
this.setupBackButton()
}

private setupWalletButtons() {
this.viewContainer.querySelectorAll('.dis-wallet-tn').forEach((elem) =>
elem.addEventListener('click', (e) => {
const elem = e.currentTarget
this.client.disconnectWallet(
elem.hasAttribute('data-address') ? elem.getAttribute('data-address') : null,
elem.hasAttribute('data-providertype') ? elem.getAttribute('data-providertype') : null,
)
}),
)

const addWalletBtn = this.viewContainer.querySelector('.add-wallet-tn')
addWalletBtn.addEventListener('click', () => {
this.ui.updateUI('wallet', null, { viewTransition: 'slide-in-right', backButtonView: 'manage-wallets' })
})
}

private setupBackButton() {
const backBtn = this.viewContainer.querySelector('.back-to-menu-tn')
backBtn.addEventListener('click', () => {
this.ui.updateUI('main', null, { viewTransition: 'slide-in-left' })
})
}

async renderCurrentWalletConnections() {
const wallets = await this.client.getWalletProvider()

let html = ''

for (const blockchain of ['evm', 'solana', 'flow'] as SupportedBlockchainsParam[]) {
// If TN has connected wallets of this type, render the header and each wallet connection as a row
if (wallets.hasAnyConnection([blockchain])) {
let typeLabel = ''

switch (blockchain) {
case 'evm':
typeLabel = 'Ethereum EVM'
break
case 'solana':
typeLabel = 'Solana'
break
case 'flow':
typeLabel = 'Flow'
break
default:
typeLabel = blockchain
}

html += `<h3 class="wallets-header headline-tn">${typeLabel}</h3>`

for (const connection of wallets.getConnectedWalletData(blockchain)) {
const address = connection.address

html += `
<div class="wallet-connection-tn">
<div class="wallet-icon-tn">
${getWalletInfo(connection.providerType)?.imgBig}
</div>
<div class="wallet-info-tn">
<small class="wallet-address-tn" title="${address}">
<span class="ellipsis">${address.substring(0, address.length - 5)}</span>
${address.substring(address.length - 5, address.length)}
</small>
<div class="wallet-name-tn">${connection.providerType}</div>
</div>
<div class="wallet-disconnect-tn">
<button class="btn-tn dis-wallet-tn" aria-label="Disconnect Wallet" title="Disconnect Wallet" data-address="${address}" data-providertype="${
connection.providerType
}">
${DisconnectButtonSVG}
</button>
</div>
</div>
`
}
}
}

return html
}
}
Loading