diff --git a/package.json b/package.json index f00aeea09..86aa46819 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,5 @@ "./packages/*/*" ], "dependencies": { - "assert-never": "^1.2.1" } } diff --git a/packages/batcher/address-validator/package.json b/packages/batcher/address-validator/package.json index b37299146..032b5e7da 100644 --- a/packages/batcher/address-validator/package.json +++ b/packages/batcher/address-validator/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "pg": "^8.7.3", - "web3": "1.10.0" + "web3": "1.10.0", + "assert-never": "^1.2.1" }, "devDependencies": { "@types/pg": "^8.6.5" diff --git a/packages/batcher/utils/package.json b/packages/batcher/utils/package.json index 6df26a2c2..afab2fb5f 100644 --- a/packages/batcher/utils/package.json +++ b/packages/batcher/utils/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@truffle/hdwallet-provider": "^2.1.15", - "web3": "1.10.0" + "web3": "1.10.0", + "assert-never": "^1.2.1" } } diff --git a/packages/engine/paima-funnel/package.json b/packages/engine/paima-funnel/package.json index bdce22a7c..394c97860 100644 --- a/packages/engine/paima-funnel/package.json +++ b/packages/engine/paima-funnel/package.json @@ -17,5 +17,6 @@ "typescript": "^5.2.2" }, "dependencies": { + "assert-never": "^1.2.1" } } diff --git a/packages/engine/paima-funnel/src/cde/erc20Deposit.ts b/packages/engine/paima-funnel/src/cde/erc20Deposit.ts index 775f0246c..9b7d7855e 100644 --- a/packages/engine/paima-funnel/src/cde/erc20Deposit.ts +++ b/packages/engine/paima-funnel/src/cde/erc20Deposit.ts @@ -12,7 +12,7 @@ export default async function getCdeData( // https://github.com/dethcrypto/TypeChain/issues/767 const events = (await timeout( extension.contract.getPastEvents('Transfer', { - filter: { to: extension.depositAddress.toLocaleLowerCase() }, + filter: { to: extension.depositAddress.toLowerCase() }, fromBlock: fromBlock, toBlock: toBlock, }), diff --git a/packages/engine/paima-runtime/package.json b/packages/engine/paima-runtime/package.json index 979635e1a..9bcfcef78 100644 --- a/packages/engine/paima-runtime/package.json +++ b/packages/engine/paima-runtime/package.json @@ -17,7 +17,8 @@ "express": "^4.18.1", "fnv-plus": "^1.3.1", "json-stable-stringify": "^1.0.2", - "yaml": "^2.3.1" + "yaml": "^2.3.1", + "assert-never": "^1.2.1" }, "devDependencies": { "@types/cors": "^2.8.12", diff --git a/packages/engine/paima-sm/package.json b/packages/engine/paima-sm/package.json index 12d05ea07..fe82ccf6b 100644 --- a/packages/engine/paima-sm/package.json +++ b/packages/engine/paima-sm/package.json @@ -14,5 +14,6 @@ }, "author": "Paima Studios", "dependencies": { + "assert-never": "^1.2.1" } } diff --git a/packages/paima-sdk/paima-mw-core/package.json b/packages/paima-sdk/paima-mw-core/package.json index 53517593d..5bfe9fd76 100644 --- a/packages/paima-sdk/paima-mw-core/package.json +++ b/packages/paima-sdk/paima-mw-core/package.json @@ -40,6 +40,7 @@ "@paima/utils": "1.1.5", "@paima/providers": "1.1.5", "@paima/concise": "1.1.5", - "@paima/prando": "1.1.5" + "@paima/prando": "1.1.5", + "assert-never": "^1.2.1" } } diff --git a/packages/paima-sdk/paima-mw-core/src/endpoints/accounts.ts b/packages/paima-sdk/paima-mw-core/src/endpoints/accounts.ts index 5da8f980c..bbe328c70 100644 --- a/packages/paima-sdk/paima-mw-core/src/endpoints/accounts.ts +++ b/packages/paima-sdk/paima-mw-core/src/endpoints/accounts.ts @@ -5,7 +5,7 @@ import { } from '../helpers/auxiliary-queries'; import { checkCardanoWalletStatus } from '../wallets/cardano'; import { checkEthWalletStatus } from '../wallets/evm'; -import { specificWalletLogin, stringToWalletMode } from '../wallets/wallets'; +import { specificWalletLogin } from '../wallets/wallets'; import { getEmulatedBlocksActive, getPostingMode, @@ -14,6 +14,7 @@ import { setEmulatedBlocksInactive, } from '../state'; import type { Result, OldResult, Wallet } from '../types'; +import type { LoginInfo } from '../wallets/wallet-modes'; /** * Wrapper function for all wallet status checking functions @@ -39,15 +40,10 @@ async function checkWalletStatus(): Promise { * thus allowing the game to get past the login screen. * @param preferBatchedMode - If true (or truthy value) even EVM wallet inputs will be batched. */ -async function userWalletLogin( - loginType: string, - preferBatchedMode: boolean = false -): Promise> { +async function userWalletLogin(loginInfo: LoginInfo): Promise> { const errorFxn = buildEndpointErrorFxn('userWalletLogin'); - const walletMode = stringToWalletMode(loginType); - // Unity bridge uses 0|1 instead of booleans - const response = await specificWalletLogin(walletMode, !!preferBatchedMode); + const response = await specificWalletLogin(loginInfo); if (!response.success) { return response; } diff --git a/packages/paima-sdk/paima-mw-core/src/endpoints/internal.ts b/packages/paima-sdk/paima-mw-core/src/endpoints/internal.ts index b1664c05c..9ca821eeb 100644 --- a/packages/paima-sdk/paima-mw-core/src/endpoints/internal.ts +++ b/packages/paima-sdk/paima-mw-core/src/endpoints/internal.ts @@ -3,7 +3,6 @@ import { buildEndpointErrorFxn, PaimaMiddlewareErrorCode } from '../errors'; import { getActiveAddress, getChainUri, - getGameName, getPostingInfo, setAutomaticMode, setBackendUri, @@ -15,35 +14,14 @@ import { setUnbatchedMode, } from '../state'; import type { PostingInfo, PostingModeSwitchResult, Result, Wallet } from '../types'; -import { specificWalletLogin, stringToWalletMode } from '../wallets/wallets'; +import { specificWalletLogin } from '../wallets/wallets'; import { emulatedBlocksActiveOnBackend } from '../helpers/auxiliary-queries'; -import { CardanoConnector, TruffleConnector } from '@paima/providers'; +import { TruffleConnector } from '@paima/providers'; import HDWalletProvider from '@truffle/hdwallet-provider'; +import type { LoginInfo } from '../wallets/wallet-modes'; -export async function userWalletLoginWithoutChecks( - loginType: string, - preferBatchedMode = false -): Promise> { - const walletMode = stringToWalletMode(loginType); - return await specificWalletLogin(walletMode, preferBatchedMode); -} - -export async function cardanoWalletLoginEndpoint(): Promise> { - const errorFxn = buildEndpointErrorFxn('cardanoWalletLoginEndpoint'); - try { - const provider = await CardanoConnector.instance().connectSimple({ - gameName: getGameName(), - gameChainId: undefined, - }); - return { - success: true, - result: { - walletAddress: provider.getAddress(), - }, - }; - } catch (err) { - return errorFxn(PaimaMiddlewareErrorCode.CARDANO_LOGIN, err); - } +export async function userWalletLoginWithoutChecks(loginInfo: LoginInfo): Promise> { + return await specificWalletLogin(loginInfo); } export async function automaticWalletLogin(privateKey: string): Promise> { diff --git a/packages/paima-sdk/paima-mw-core/src/index.ts b/packages/paima-sdk/paima-mw-core/src/index.ts index aa4330196..4f2b77a0f 100644 --- a/packages/paima-sdk/paima-mw-core/src/index.ts +++ b/packages/paima-sdk/paima-mw-core/src/index.ts @@ -5,7 +5,6 @@ import { queryEndpoints } from './endpoints/queries'; import { utilityEndpoints } from './endpoints/utility'; import { - cardanoWalletLoginEndpoint, retrievePostingInfo, switchToBatchedCardanoMode, switchToBatchedEthMode, @@ -59,6 +58,7 @@ export type * from './errors'; // Only for use in game-specific middleware: export * from './types'; export type * from './types'; +export { WalletMode } from './wallets/wallet-modes'; export { paimaEndpoints, getBlockNumber, @@ -83,7 +83,6 @@ export { // NOT FOR USE IN PRODUCTION, just internal endpoints and helper functions for easier testing and debugging: export { - cardanoWalletLoginEndpoint, retrievePostingInfo, switchToBatchedCardanoMode, switchToBatchedEthMode, diff --git a/packages/paima-sdk/paima-mw-core/src/types.ts b/packages/paima-sdk/paima-mw-core/src/types.ts index d8ea3fe38..9d05a4145 100644 --- a/packages/paima-sdk/paima-mw-core/src/types.ts +++ b/packages/paima-sdk/paima-mw-core/src/types.ts @@ -1,4 +1,5 @@ import type { Hash, WalletAddress, UserSignature } from '@paima/utils'; +export type * from './wallets/wallet-modes'; export interface PostingInfo { address: WalletAddress; diff --git a/packages/paima-sdk/paima-mw-core/src/wallets/algorand.ts b/packages/paima-sdk/paima-mw-core/src/wallets/algorand.ts index 1bc552a42..3a01bf1f0 100644 --- a/packages/paima-sdk/paima-mw-core/src/wallets/algorand.ts +++ b/packages/paima-sdk/paima-mw-core/src/wallets/algorand.ts @@ -1,23 +1,34 @@ -import type { Result, Wallet } from '../types'; +import type { LoginInfoMap, Result, Wallet } from '../types'; import { PaimaMiddlewareErrorCode, buildEndpointErrorFxn } from '../errors'; import { AlgorandConnector } from '@paima/providers'; import { getGameName } from '../state'; +import type { WalletMode } from './wallet-modes'; +import { connectWallet } from './wallet-modes'; -export async function algorandLoginWrapper(): Promise> { +export async function algorandLoginWrapper( + loginInfo: LoginInfoMap[WalletMode.ALGORAND] +): Promise> { const errorFxn = buildEndpointErrorFxn('algorandLoginWrapper'); - try { - const provider = await AlgorandConnector.instance().connectSimple({ - gameName: getGameName(), - gameChainId: undefined, - }); - return { - success: true, - result: { - walletAddress: provider.getAddress(), - }, - }; - } catch (err) { - return errorFxn(PaimaMiddlewareErrorCode.ALGORAND_LOGIN, err); + const gameInfo = { + gameName: getGameName(), + gameChainId: undefined, // Not needed because of batcher + }; + const loginResult = await connectWallet( + 'algorandLoginWrapper', + errorFxn, + PaimaMiddlewareErrorCode.ALGORAND_LOGIN, + loginInfo, + AlgorandConnector.instance(), + gameInfo + ); + if (loginResult.success === false) { + return loginResult; } + return { + success: true, + result: { + walletAddress: loginResult.result.getAddress(), + }, + }; } diff --git a/packages/paima-sdk/paima-mw-core/src/wallets/cardano.ts b/packages/paima-sdk/paima-mw-core/src/wallets/cardano.ts index 50fd6238b..c22d6d0d5 100644 --- a/packages/paima-sdk/paima-mw-core/src/wallets/cardano.ts +++ b/packages/paima-sdk/paima-mw-core/src/wallets/cardano.ts @@ -1,11 +1,8 @@ -import type { OldResult, Result, Wallet } from '../types'; -import { - buildEndpointErrorFxn, - PaimaMiddlewareErrorCode, - FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED, -} from '../errors'; -import { WalletMode } from './wallet-modes'; -import { CardanoConnector, UnsupportedWallet, WalletNotFound } from '@paima/providers'; +import type { LoginInfoMap, OldResult, Result, Wallet } from '../types'; +import { buildEndpointErrorFxn, PaimaMiddlewareErrorCode } from '../errors'; +import type { WalletMode } from './wallet-modes'; +import { connectWallet } from './wallet-modes'; +import { CardanoConnector } from '@paima/providers'; import { getGameName } from '../state'; export async function checkCardanoWalletStatus(): Promise { @@ -21,63 +18,30 @@ export async function checkCardanoWalletStatus(): Promise { return { success: true, message: '' }; } -function cardanoWalletModeToName(walletMode: WalletMode): string { - switch (walletMode) { - case WalletMode.CARDANO_FLINT: - return 'flint'; - case WalletMode.CARDANO_NUFI: - return 'nufi'; - case WalletMode.CARDANO_NAMI: - return 'nami'; - case WalletMode.CARDANO_ETERNL: - return 'eternl'; - default: - return ''; - } -} - -export async function cardanoLoginWrapper(walletMode: WalletMode): Promise> { +export async function cardanoLoginWrapper( + loginInfo: LoginInfoMap[WalletMode.CARDANO] +): Promise> { const errorFxn = buildEndpointErrorFxn('cardanoLoginWrapper'); - console.log('[cardanoLoginWrapper] window.cardano:', (window as any).cardano); - - let specificWalletName: string | undefined = undefined; - if (walletMode !== WalletMode.CARDANO) { - console.log(`[cardanoLoginWrapper] Attempting to log into ${specificWalletName}`); - specificWalletName = cardanoWalletModeToName(walletMode); - if (!specificWalletName) { - return errorFxn(PaimaMiddlewareErrorCode.CARDANO_WALLET_NOT_INSTALLED); - } - } else { - console.log(`[cardanoLoginWrapper] Attempting to log into any Cardano wallet`); - } - try { - const gameInfo = { - gameName: getGameName(), - gameChainId: undefined, - }; - const provider = - specificWalletName == null - ? await CardanoConnector.instance().connectSimple(gameInfo) - : await CardanoConnector.instance().connectNamed(gameInfo, specificWalletName); - return { - success: true, - result: { - walletAddress: provider.getAddress().toLocaleLowerCase(), - }, - }; - } catch (err) { - if (err instanceof WalletNotFound || err instanceof UnsupportedWallet) { - return errorFxn( - PaimaMiddlewareErrorCode.CARDANO_WALLET_NOT_INSTALLED, - undefined, - FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED - ); - } - console.log( - `[cardanoLoginWrapper] Error while logging into wallet ${specificWalletName ?? 'Cardano'}` - ); - return errorFxn(PaimaMiddlewareErrorCode.CARDANO_LOGIN, err); - // TODO: improve error differentiation + const gameInfo = { + gameName: getGameName(), + gameChainId: undefined, // Not needed because of batcher + }; + const loginResult = await connectWallet( + 'cardanoLoginWrapper', + errorFxn, + PaimaMiddlewareErrorCode.CARDANO_LOGIN, + loginInfo, + CardanoConnector.instance(), + gameInfo + ); + if (loginResult.success === false) { + return loginResult; } + return { + success: true, + result: { + walletAddress: loginResult.result.getAddress(), + }, + }; } diff --git a/packages/paima-sdk/paima-mw-core/src/wallets/evm.ts b/packages/paima-sdk/paima-mw-core/src/wallets/evm.ts index 68e7725f2..755b37abf 100644 --- a/packages/paima-sdk/paima-mw-core/src/wallets/evm.ts +++ b/packages/paima-sdk/paima-mw-core/src/wallets/evm.ts @@ -1,8 +1,4 @@ -import { - buildEndpointErrorFxn, - PaimaMiddlewareErrorCode, - FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED, -} from '../errors'; +import { buildEndpointErrorFxn, PaimaMiddlewareErrorCode } from '../errors'; import { getChainCurrencyDecimals, getChainCurrencyName, @@ -13,11 +9,12 @@ import { getChainUri, getGameName, } from '../state'; -import type { OldResult, Result, Wallet } from '../types'; +import type { LoginInfoMap, OldResult, Result, Wallet } from '../types'; import { updateFee } from '../helpers/posting'; -import { WalletMode } from './wallet-modes'; -import { EvmConnector, UnsupportedWallet, WalletNotFound } from '@paima/providers'; +import { connectWallet } from './wallet-modes'; +import type { WalletMode } from './wallet-modes'; +import { EvmConnector } from '@paima/providers'; interface SwitchError { code: number; @@ -86,45 +83,25 @@ export async function checkEthWalletStatus(): Promise { return { success: true, message: '' }; } -function evmWalletModeToName(walletMode: WalletMode): string { - switch (walletMode) { - case WalletMode.METAMASK: - return 'metamask'; - case WalletMode.EVM_FLINT: - return 'flint'; - default: - return ''; - } -} - -export async function evmLoginWrapper(walletMode: WalletMode): Promise> { +export async function evmLoginWrapper( + loginInfo: LoginInfoMap[WalletMode.EVM] +): Promise> { const errorFxn = buildEndpointErrorFxn('evmLoginWrapper'); - const walletName = evmWalletModeToName(walletMode); - if (!walletName) { - return errorFxn(PaimaMiddlewareErrorCode.WALLET_NOT_SUPPORTED); - } - console.log(`[evmLoginWrapper] Attempting to log into ${walletName}`); - - try { - await EvmConnector.instance().connectNamed( - { - gameName: getGameName(), - gameChainId: '0x' + getChainId().toString(16), - }, - walletName - ); - } catch (err) { - if (err instanceof WalletNotFound || err instanceof UnsupportedWallet) { - return errorFxn( - PaimaMiddlewareErrorCode.CARDANO_WALLET_NOT_INSTALLED, - undefined, - FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED - ); - } - console.log(`[evmLoginWrapper] Error while logging into wallet ${walletName}`); - - return errorFxn(PaimaMiddlewareErrorCode.EVM_LOGIN, err); + const gameInfo = { + gameName: getGameName(), + gameChainId: '0x' + getChainId().toString(16), + }; + const loginResult = await connectWallet( + 'evmLoginWrapper', + errorFxn, + PaimaMiddlewareErrorCode.EVM_LOGIN, + loginInfo, + EvmConnector.instance(), + gameInfo + ); + if (loginResult.success === false) { + return loginResult; } try { @@ -147,7 +124,7 @@ export async function evmLoginWrapper(walletMode: WalletMode): Promise> { +export async function polkadotLoginWrapper( + loginInfo: LoginInfoMap[WalletMode.POLKADOT] +): Promise> { const errorFxn = buildEndpointErrorFxn('polkadotLoginWrapper'); - try { - const provider = await PolkadotConnector.instance().connectSimple({ - gameName: getGameName(), - gameChainId: undefined, - }); - return { - success: true, - result: { - walletAddress: provider.getAddress(), - }, - }; - } catch (err) { - if (err instanceof WalletNotFound || err instanceof UnsupportedWallet) { - return errorFxn( - PaimaMiddlewareErrorCode.POLKADOT_WALLET_NOT_INSTALLED, - undefined, - FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED - ); - } - console.log(`[polkadotLoginWrapper] Error while logging into wallet`); - return errorFxn(PaimaMiddlewareErrorCode.POLKADOT_LOGIN, err); + const gameInfo = { + gameName: getGameName(), + gameChainId: undefined, // Not needed because of batcher + }; + const loginResult = await connectWallet( + 'polkadotLoginWrapper', + errorFxn, + PaimaMiddlewareErrorCode.POLKADOT_LOGIN, + loginInfo, + PolkadotConnector.instance(), + gameInfo + ); + if (loginResult.success === false) { + return loginResult; } + return { + success: true, + result: { + walletAddress: loginResult.result.getAddress(), + }, + }; } diff --git a/packages/paima-sdk/paima-mw-core/src/wallets/wallet-modes.ts b/packages/paima-sdk/paima-mw-core/src/wallets/wallet-modes.ts index 0e3a587f7..01bfcadf3 100644 --- a/packages/paima-sdk/paima-mw-core/src/wallets/wallet-modes.ts +++ b/packages/paima-sdk/paima-mw-core/src/wallets/wallet-modes.ts @@ -1,12 +1,103 @@ -export enum WalletMode { +import type { + ActiveConnection, + EvmApi, + CardanoApi, + AlgorandApi, + PolkadotApi, + IConnector, + IProvider, + GameInfo, +} from '@paima/providers'; +import { WalletNotFound, UnsupportedWallet } from '@paima/providers'; +import type { EndpointErrorFxn } from '../errors'; +import type { Result } from '../types'; +import type { PaimaMiddlewareErrorCode } from '../errors'; +import { FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED } from '../errors'; +import assertNever from 'assert-never'; + +export const enum WalletMode { NO_WALLET, - METAMASK, - EVM_FLINT, + EVM, CARDANO, - CARDANO_FLINT, - CARDANO_NUFI, - CARDANO_NAMI, - CARDANO_ETERNL, POLKADOT, - ALGORAND_PERA, + ALGORAND, +} + +export type Preference = + | { + name: string; + } + | { + connection: ActiveConnection; + }; + +export type BaseLoginInfo = { + preference?: Preference; +}; +export type LoginInfoMap = { + [WalletMode.EVM]: BaseLoginInfo & { preferBatchedMode: boolean }; + [WalletMode.CARDANO]: BaseLoginInfo; + [WalletMode.POLKADOT]: BaseLoginInfo; + [WalletMode.ALGORAND]: BaseLoginInfo; + [WalletMode.NO_WALLET]: void; +}; + +type ToUnion = { + [K in keyof T]: { mode: K } & T[K]; +}[keyof T]; + +export type LoginInfo = ToUnion; + +function getWalletName(info: BaseLoginInfo): undefined | string { + if (info.preference == null) return undefined; + if ('name' in info.preference) { + return info.preference.name; + } + return info.preference.connection.metadata.name; +} +export async function connectWallet( + typeName: string, + errorFxn: EndpointErrorFxn, + errorCode: PaimaMiddlewareErrorCode, + loginInfo: BaseLoginInfo, + connector: IConnector, + gameInfo: GameInfo +): Promise>> { + try { + if (loginInfo.preference == null) { + console.log(`${typeName} Attempting simple login`); + const provider = await connector.connectSimple(gameInfo); + return { + success: true, + result: provider, + }; + } else if ('name' in loginInfo.preference) { + const walletName = loginInfo.preference.name; + console.log(`${typeName} Attempting to log into ${walletName}`); + const provider = await connector.connectNamed(gameInfo, walletName); + return { + success: true, + result: provider, + }; + } else if ('connection' in loginInfo.preference) { + const walletName = loginInfo.preference.connection.metadata.name; + console.log(`${typeName} Attempting to log into ${walletName}`); + const provider = await connector.connectExternal(gameInfo, loginInfo.preference.connection); + return { + success: true, + result: provider, + }; + } else { + assertNever(loginInfo.preference); + } + } catch (err) { + if (err instanceof WalletNotFound || err instanceof UnsupportedWallet) { + return errorFxn(errorCode, undefined, FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED); + } + console.log( + `${typeName} Error while logging into wallet ${getWalletName(loginInfo) ?? 'simple'}` + ); + + return errorFxn(errorCode, err); + } } diff --git a/packages/paima-sdk/paima-mw-core/src/wallets/wallets.ts b/packages/paima-sdk/paima-mw-core/src/wallets/wallets.ts index 5cc0b8f62..1fb5989e7 100644 --- a/packages/paima-sdk/paima-mw-core/src/wallets/wallets.ts +++ b/packages/paima-sdk/paima-mw-core/src/wallets/wallets.ts @@ -7,72 +7,37 @@ import { setBatchedPolkadotMode, setUnbatchedMode, } from '../state'; -import type { Result, Wallet } from '../types'; +import type { LoginInfo, Result, Wallet } from '../types'; import { algorandLoginWrapper } from './algorand'; import { cardanoLoginWrapper } from './cardano'; import { evmLoginWrapper } from './evm'; import { polkadotLoginWrapper } from './polkadot'; import { WalletMode } from './wallet-modes'; -export function stringToWalletMode(loginType: string): WalletMode { - // TODO: this function has a bunch of magic strings in it which is not great - // some of these are also repeated in other places (ex: evmWalletModeToName) - switch (loginType) { - case 'metamask': - return WalletMode.METAMASK; - case 'evm-flint': - return WalletMode.EVM_FLINT; - case 'cardano': - return WalletMode.CARDANO; - case 'flint': - return WalletMode.CARDANO_FLINT; - case 'nufi': - return WalletMode.CARDANO_NUFI; - case 'nami': - return WalletMode.CARDANO_NAMI; - case 'eternl': - return WalletMode.CARDANO_ETERNL; - case 'polkadot': - return WalletMode.POLKADOT; - case 'pera': - return WalletMode.ALGORAND_PERA; - default: - return WalletMode.NO_WALLET; - } -} - -export async function specificWalletLogin( - walletMode: WalletMode, - preferBatchedMode: boolean -): Promise> { +export async function specificWalletLogin(loginInfo: LoginInfo): Promise> { const errorFxn = buildEndpointErrorFxn('specificWalletLogin'); - switch (walletMode) { - case WalletMode.METAMASK: - case WalletMode.EVM_FLINT: - if (preferBatchedMode) { + switch (loginInfo.mode) { + case WalletMode.EVM: + if (loginInfo.preferBatchedMode) { setBatchedEthMode(); } else { setUnbatchedMode(); } - return await evmLoginWrapper(walletMode); + return await evmLoginWrapper(loginInfo); case WalletMode.CARDANO: - case WalletMode.CARDANO_FLINT: - case WalletMode.CARDANO_NUFI: - case WalletMode.CARDANO_NAMI: - case WalletMode.CARDANO_ETERNL: setBatchedCardanoMode(); - return await cardanoLoginWrapper(walletMode); + return await cardanoLoginWrapper(loginInfo); case WalletMode.POLKADOT: setBatchedPolkadotMode(); - return await polkadotLoginWrapper(); - case WalletMode.ALGORAND_PERA: + return await polkadotLoginWrapper(loginInfo); + case WalletMode.ALGORAND: setBatchedAlgorandMode(); - return await algorandLoginWrapper(); + return await algorandLoginWrapper(loginInfo); case WalletMode.NO_WALLET: return errorFxn(FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED); default: - assertNever(walletMode, true); + assertNever(loginInfo, true); return errorFxn(FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED); } } diff --git a/packages/paima-sdk/paima-providers/src/IProvider.ts b/packages/paima-sdk/paima-providers/src/IProvider.ts index 531c9a4db..3c0c0e405 100644 --- a/packages/paima-sdk/paima-providers/src/IProvider.ts +++ b/packages/paima-sdk/paima-providers/src/IProvider.ts @@ -1,12 +1,35 @@ export type UserSignature = string; +export type WalletOption = { + name: string; // name of the wallet used in APIs (as opposed to a human-friendly string) + displayName: string; + /** + * URI-encoded image + * DANGER: SVGs can contain Javascript, so these should only be rendered with tags + * + * Note: not every wallet type has an image (ex: locally generated keypairs) + * Example values: + * data:image/svg+xml,... + * data:image/png;base64,... + */ + icon?: undefined | string; +}; + export type ActiveConnection = { - metadata: { - // TODO: should also expose the icon for the wallet - name: string; - }; + metadata: WalletOption; api: T; }; +export type ConnectionOption = { + metadata: WalletOption; + api: () => Promise; +}; +export async function optionToActive(option: ConnectionOption): Promise> { + const connection = { + metadata: option.metadata, + api: await option.api(), + }; + return connection; +} export type GameInfo = { gameName: string; diff --git a/packages/paima-sdk/paima-providers/src/algorand.ts b/packages/paima-sdk/paima-providers/src/algorand.ts index da1f66d55..0464bbe22 100644 --- a/packages/paima-sdk/paima-providers/src/algorand.ts +++ b/packages/paima-sdk/paima-providers/src/algorand.ts @@ -1,21 +1,54 @@ import type { PeraWalletConnect } from '@perawallet/connect'; -import type { ActiveConnection, GameInfo, IConnector, IProvider, UserSignature } from './IProvider'; +import { + optionToActive, + type ActiveConnection, + type ConnectionOption, + type GameInfo, + type IConnector, + type IProvider, + type UserSignature, +} from './IProvider'; import { CryptoManager } from '@paima/crypto'; import { uint8ArrayToHexString } from '@paima/utils'; -import { ProviderApiError, ProviderNotInitialized, UnsupportedWallet } from './errors'; +import { + ProviderApiError, + ProviderNotInitialized, + UnsupportedWallet, + WalletNotFound, +} from './errors'; export type AlgorandApi = PeraWalletConnect; export type AlgorandAddress = string; -// TODO: this should probably be dynamically detected -enum SupportedAlgorandWallets { - PERA = 'pera', -} - export class AlgorandConnector implements IConnector { private provider: AlgorandProvider | undefined; private static INSTANCE: undefined | AlgorandConnector = undefined; + static getWalletOptions(): ConnectionOption[] { + // Algorand has no standard for wallet discovery + // The closest that exists is ARC11 (https://arc.algorand.foundation/ARCs/arc-0011) + // but it doesn't give any information about which wallet is injected + // and, similar to window.ethereum, has wallets overriding each other + // and Pera wallet doesn't even use this standard + // instead, the best we can do is check if Pera injected its UI component in the window + if (window.customElements.get('pera-wallet-connect-modal') == null) { + return []; + } + return [ + { + metadata: { + name: 'pera', + displayName: 'Pera Wallet', + }, + api: async (): Promise => { + const { PeraWalletConnect } = await import('@perawallet/connect'); + const peraWallet = new PeraWalletConnect(); + return peraWallet; + }, + }, + ]; + } + static instance(): AlgorandConnector { if (AlgorandConnector.INSTANCE == null) { const newInstance = new AlgorandConnector(); @@ -27,7 +60,11 @@ export class AlgorandConnector implements IConnector { if (this.provider != null) { return this.provider; } - return await this.connectNamed(gameInfo, SupportedAlgorandWallets.PERA); + const options = AlgorandConnector.getWalletOptions(); + if (options.length === 0) { + throw new WalletNotFound(`No Algorand wallet found`); + } + return await this.connectExternal(gameInfo, await optionToActive(options[0])); }; connectExternal = async ( gameInfo: GameInfo, @@ -43,18 +80,13 @@ export class AlgorandConnector implements IConnector { if (this.provider?.getConnection().metadata?.name === name) { return this.provider; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (name !== SupportedAlgorandWallets.PERA) { - throw new UnsupportedWallet(`AlgorandProvider: unknown connection type ${name}`); + const provider = AlgorandConnector.getWalletOptions().find( + entry => entry.metadata.name === name + ); + if (provider == null) { + throw new UnsupportedWallet(`AlgorandProvider: unsupported connection type ${name}`); } - const { PeraWalletConnect } = await import('@perawallet/connect'); - const peraWallet = new PeraWalletConnect(); - return await this.connectExternal(gameInfo, { - metadata: { - name: SupportedAlgorandWallets.PERA, - }, - api: peraWallet, - }); + return await this.connectExternal(gameInfo, await optionToActive(provider)); }; getProvider = (): undefined | AlgorandProvider => { return this.provider; diff --git a/packages/paima-sdk/paima-providers/src/cardano.ts b/packages/paima-sdk/paima-providers/src/cardano.ts index 55dc92d29..d1ccbe966 100644 --- a/packages/paima-sdk/paima-providers/src/cardano.ts +++ b/packages/paima-sdk/paima-providers/src/cardano.ts @@ -1,13 +1,15 @@ import { hexStringToUint8Array, type UserSignature } from '@paima/utils'; import { utf8ToHex } from 'web3-utils'; -import type { ActiveConnection, GameInfo, IConnector, IProvider } from './IProvider'; -import { bech32 } from 'bech32'; import { - ProviderApiError, - ProviderNotInitialized, - UnsupportedWallet, - WalletNotFound, -} from './errors'; + optionToActive, + type ActiveConnection, + type ConnectionOption, + type GameInfo, + type IConnector, + type IProvider, +} from './IProvider'; +import { bech32 } from 'bech32'; +import { ProviderApiError, ProviderNotInitialized, WalletNotFound } from './errors'; // TODO: proper type definitions for CIP30 export type CardanoApi = any; @@ -27,6 +29,28 @@ export class CardanoConnector implements IConnector { private provider: CardanoProvider | undefined; private static INSTANCE: undefined | CardanoConnector = undefined; + static getWalletOptions(): ConnectionOption[] { + const cardanoApi: Record Promise }> = ( + window as any + ).cardano; + if (cardanoApi == null) return []; + + const options = Object.entries(cardanoApi).reduce((options, [key, info]) => { + if (info.name != null && info.enable != null) { + options.push({ + metadata: { + name: key, + displayName: info.name, + icon: 'icon' in info ? (info.icon as string) : undefined, + }, + api: info.enable, + }); + } + return options; + }, [] as ConnectionOption[]); + return options; + } + static instance(): CardanoConnector { if (CardanoConnector.INSTANCE == null) { const newInstance = new CardanoConnector(); @@ -38,40 +62,29 @@ export class CardanoConnector implements IConnector { if (this.provider != null) { return this.provider; } - const cardanoApi: Record Promise }> = ( - window as any - ).cardano; - if (cardanoApi == null) { - throw new WalletNotFound(`[cardano] no wallet detected`); + const options = CardanoConnector.getWalletOptions(); + if (options.length === 0) { + throw new WalletNotFound(`No Cardano wallet found`); } - const pontentialWallets = Object.keys(cardanoApi); // flint has some custom support for Paima, so best to return this one first if it exists - if (pontentialWallets.includes('flint')) { - return await this.connectNamed(gameInfo, 'flint'); + const flintWallet = options.find(option => option.metadata.name === 'flint'); + if (flintWallet != null) { + return await this.connectExternal(gameInfo, await optionToActive(flintWallet)); } - for (const key of pontentialWallets) { - if (cardanoApi[key]?.name == null || cardanoApi[key]?.enable == null) { - continue; - } - return await this.connectNamed(gameInfo, key); - } - throw new UnsupportedWallet('[cardanoLogin] Unable to connect to any supported Cardano wallet'); + return await this.connectExternal(gameInfo, await optionToActive(options[0])); }; connectNamed = async (gameInfo: GameInfo, name: string): Promise => { if (this.provider?.getConnection().metadata?.name === name) { return this.provider; } - if (typeof (window as any).cardano?.[name] === 'undefined') { - throw new WalletNotFound(`[cardanoLoginSpecific] Wallet ${name} not injected in the browser`); + const provider = CardanoConnector.getWalletOptions().find( + entry => entry.metadata.name === name + ); + if (provider == null) { + throw new WalletNotFound(`Cardano wallet ${name} not found`); } - const api: CardanoApi = await (window as any).cardano[name].enable(); - return await this.connectExternal(gameInfo, { - api, - metadata: { - name, - }, - }); + return await this.connectExternal(gameInfo, await optionToActive(provider)); }; connectExternal = async ( gameInfo: GameInfo, diff --git a/packages/paima-sdk/paima-providers/src/evm-truffle.ts b/packages/paima-sdk/paima-providers/src/evm-truffle.ts index d002d7521..48fc01a50 100644 --- a/packages/paima-sdk/paima-providers/src/evm-truffle.ts +++ b/packages/paima-sdk/paima-providers/src/evm-truffle.ts @@ -55,6 +55,7 @@ export class TruffleEvmProvider implements IProvider { api: hdWalletProvider, metadata: { name: 'truffle', + displayName: 'truffle', }, }; return new TruffleEvmProvider(conn, web3, address); diff --git a/packages/paima-sdk/paima-providers/src/evm.ts b/packages/paima-sdk/paima-providers/src/evm.ts index dd3852bf6..8563c4a81 100644 --- a/packages/paima-sdk/paima-providers/src/evm.ts +++ b/packages/paima-sdk/paima-providers/src/evm.ts @@ -1,13 +1,62 @@ import type { MetaMaskInpageProvider } from '@metamask/providers'; -import type { ActiveConnection, GameInfo, IConnector, IProvider, UserSignature } from './IProvider'; +import { + optionToActive, + type ActiveConnection, + type ConnectionOption, + type GameInfo, + type IConnector, + type IProvider, + type UserSignature, +} from './IProvider'; import { utf8ToHex } from 'web3-utils'; import { ProviderApiError, ProviderNotInitialized, WalletNotFound } from './errors'; +type EIP1193Provider = MetaMaskInpageProvider; + +interface EIP5749ProviderInfo { + uuid: string; + name: string; + icon: `data:image/svg+xml;base64,${string}`; + description: string; +} +interface EIP5749ProviderWithInfo extends EIP1193Provider { + info: EIP5749ProviderInfo; +} +interface EIP5749EVMProviders { + /** + * The key is RECOMMENDED to be the name of the extension in snake_case. It MUST contain only lowercase letters, numbers, and underscores. + */ + [index: string]: EIP5749ProviderWithInfo; +} + +interface EIP6963ProviderInfo { + uuid: string; + name: string; + icon: string; + rdns: string; +} +interface EIP6963ProviderDetail { + info: EIP6963ProviderInfo; + provider: EIP1193Provider; +} +interface EIP6963AnnounceProviderEvent extends CustomEvent { + type: 'eip6963:announceProvider'; + detail: EIP6963ProviderDetail; +} + +const eip5953Providers: EIP6963ProviderDetail[] = []; + +window?.addEventListener('eip6963:announceProvider', (event: EIP6963AnnounceProviderEvent) => { + eip5953Providers.push(event.detail); +}); +window?.dispatchEvent(new Event('eip6963:requestProvider')); declare global { interface Window { - ethereum: MetaMaskInpageProvider; - // API should be the same as MetaMask - evmproviders?: Record; + ethereum?: EIP1193Provider; + evmproviders?: EIP5749EVMProviders; + } + interface WindowEventMap { + 'eip6963:announceProvider': EIP6963AnnounceProviderEvent; } } @@ -28,36 +77,68 @@ interface AddEthereumChainParameter { rpcUrls?: string[]; } -export type EvmApi = MetaMaskInpageProvider; +export type EvmApi = EIP1193Provider; export type EvmAddress = string; -/** - * NOTE: https://eips.ethereum.org/EIPS/eip-5749 - */ +export class EvmConnector implements IConnector { + private provider: EvmProvider | undefined; + private static INSTANCE: undefined | EvmConnector = undefined; -// TODO: this should probably be dynamically detected -enum SupportedEvmWallets { - Metamask = 'metamask', - Flint = 'flint', -} + static getWalletOptions(): ConnectionOption[] { + const seenNames: Set = new Set(); + const allWallets: ConnectionOption[] = []; -const getProvider = (name: string): MetaMaskInpageProvider => { - switch (name) { - case 'metamask': - return window.ethereum; - default: { - if (window.evmproviders != null && name in window.evmproviders) { - return window.evmproviders[name]; + // add options and de-duplicate based off the display name + // we can't duplicate on other keys because they have a different formats for different EIPs + const addOptions: (options: ConnectionOption[]) => void = options => { + for (const option of options) { + if (seenNames.has(option.metadata.name)) continue; + seenNames.add(option.metadata.name); + allWallets.push(option); } - throw new WalletNotFound(`EVM wallet ${name} not found`); + }; + + // 1) Add EIP6963 and prioritize it for deduplicating + { + const eip6963Options = eip5953Providers.map(({ info, provider }) => ({ + metadata: { + name: info.rdns, + displayName: info.name, + icon: info.icon, + }, + api: () => Promise.resolve(provider), + })); + addOptions(eip6963Options); } - } -}; -export class EvmConnector implements IConnector { - private provider: EvmProvider | undefined; - private static INSTANCE: undefined | EvmConnector = undefined; + // 2) Add EIP5749 + { + const eip5749Options = Object.entries(window.evmproviders ?? {}).map(([key, provider]) => ({ + metadata: { + name: key, + displayName: provider.info.name, + icon: provider.info.icon, + }, + api: () => Promise.resolve(provider), + })); + addOptions(eip5749Options); + } + // Metamask doesn't support EIP6963 yet, but it plans to. In the meantime, we add it manually + if (window.ethereum != null && window.ethereum.isMetaMask) { + const ethereum = window.ethereum; + addOptions([ + { + metadata: { + name: 'metamask', + displayName: 'Metamask', + }, + api: () => Promise.resolve(ethereum), + }, + ]); + } + return allWallets; + } static instance(): EvmConnector { if (EvmConnector.INSTANCE == null) { const newInstance = new EvmConnector(); @@ -69,8 +150,11 @@ export class EvmConnector implements IConnector { if (this.provider != null) { return this.provider; } - // TODO: probably this should be better - return await this.connectNamed(gameInfo, SupportedEvmWallets.Metamask); + const options = EvmConnector.getWalletOptions(); + if (options.length === 0) { + throw new WalletNotFound(`No EVM wallet found`); + } + return await this.connectExternal(gameInfo, await optionToActive(options[0])); }; connectExternal = async ( gameInfo: GameInfo, @@ -83,7 +167,7 @@ export class EvmConnector implements IConnector { // Update the selected Eth address if the user changes after logging in. // warning: not supported by all wallets (ex: Flint) - window.ethereum.on('accountsChanged', newAccounts => { + window.ethereum?.on('accountsChanged', newAccounts => { const accounts = newAccounts as string[]; if (!accounts || !accounts[0] || accounts[0] !== this.provider?.address) { this.provider = undefined; @@ -96,12 +180,11 @@ export class EvmConnector implements IConnector { return this.provider; } - return await this.connectExternal(gameInfo, { - metadata: { - name, - }, - api: getProvider(name), - }); + const provider = EvmConnector.getWalletOptions().find(entry => entry.metadata.name === name); + if (provider == null) { + throw new WalletNotFound(`EVM wallet ${name} not found`); + } + return await this.connectExternal(gameInfo, await optionToActive(provider)); }; getProvider = (): undefined | EvmProvider => { return this.provider; diff --git a/packages/paima-sdk/paima-providers/src/polkadot.ts b/packages/paima-sdk/paima-providers/src/polkadot.ts index 28c81b46b..5f1f0c082 100644 --- a/packages/paima-sdk/paima-providers/src/polkadot.ts +++ b/packages/paima-sdk/paima-providers/src/polkadot.ts @@ -1,20 +1,47 @@ -import type { ActiveConnection, GameInfo, IConnector, IProvider, UserSignature } from './IProvider'; import { - ProviderApiError, - ProviderNotInitialized, - UnsupportedWallet, - WalletNotFound, -} from './errors'; -import type { InjectedExtension } from '@polkadot/extension-inject/types'; + optionToActive, + type ActiveConnection, + type ConnectionOption, + type GameInfo, + type IConnector, + type IProvider, + type UserSignature, +} from './IProvider'; +import { ProviderApiError, ProviderNotInitialized, WalletNotFound } from './errors'; +import type { InjectedExtension, InjectedWindowProvider } from '@polkadot/extension-inject/types'; import { utf8ToHex } from 'web3-utils'; export type PolkadotAddress = string; export type PolkadotApi = InjectedExtension; +declare global { + interface Window { + injectedWeb3?: Record; + } +} + export class PolkadotConnector implements IConnector { private provider: PolkadotProvider | undefined; private static INSTANCE: undefined | PolkadotConnector = undefined; + static async getWalletOptions(gameName: string): Promise[]> { + if (window.injectedWeb3 == null) return []; + return Object.keys(window.injectedWeb3).map(wallet => ({ + metadata: { + name: wallet, + // polkadot provides no way to get a human-friendly name or icon for wallets + displayName: wallet, + }, + api: async (): Promise => { + const { web3Enable, web3FromSource } = await import('@polkadot/extension-dapp'); + + await web3Enable(gameName); + const injector = await web3FromSource(wallet); + return injector; + }, + })); + } + static instance(): PolkadotConnector { if (PolkadotConnector.INSTANCE == null) { const newInstance = new PolkadotConnector(); @@ -31,6 +58,9 @@ export class PolkadotConnector implements IConnector { if (extensions.length === 0) { throw new WalletNotFound(`[polkadot] no extension detected`); } + + // we get all accounts instead of picking a specific extension + // because some extensions could have no accounts in them const allAccounts = await web3Accounts(); for (const account of allAccounts) { const injector = await web3FromAddress(account.address); @@ -38,6 +68,8 @@ export class PolkadotConnector implements IConnector { return await this.connectExternal(gameInfo, { metadata: { name: account.meta.source, + // polkadot provides no way to get a human-friendly name or icon for wallets + displayName: account.meta.source, }, api: injector, }); @@ -58,21 +90,13 @@ export class PolkadotConnector implements IConnector { if (this.provider?.getConnection().metadata?.name === name) { return this.provider; } - - const { web3Enable, web3FromSource } = await import('@polkadot/extension-dapp'); - - await web3Enable(gameInfo.gameName); - try { - const injector = await web3FromSource(name); - return await this.connectExternal(gameInfo, { - metadata: { - name, - }, - api: injector, - }); - } catch (e) { - throw new UnsupportedWallet(`[polkadot] no account found for extension ${name}`); + const provider = (await PolkadotConnector.getWalletOptions(gameInfo.gameName)).find( + entry => entry.metadata.name === name + ); + if (provider == null) { + throw new WalletNotFound(`Polkadot wallet ${name} not found`); } + return await this.connectExternal(gameInfo, await optionToActive(provider)); }; getProvider = (): undefined | PolkadotProvider => { return this.provider;