Skip to content

Commit

Permalink
feature: list wallet options for a provider & EIP6963
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienGllmt committed Oct 27, 2023
1 parent 058f1e4 commit d56eb72
Show file tree
Hide file tree
Showing 13 changed files with 357 additions and 153 deletions.
20 changes: 1 addition & 19 deletions packages/paima-sdk/paima-mw-core/src/endpoints/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import type { PostingInfo, PostingModeSwitchResult, Result, Wallet } from '../types';
import { specificWalletLogin, stringToWalletMode } 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';

export async function userWalletLoginWithoutChecks(
Expand All @@ -28,24 +28,6 @@ export async function userWalletLoginWithoutChecks(
return await specificWalletLogin(walletMode, preferBatchedMode);
}

export async function cardanoWalletLoginEndpoint(): Promise<Result<Wallet>> {
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 automaticWalletLogin(privateKey: string): Promise<Result<Wallet>> {
const errorFxn = buildEndpointErrorFxn('automaticWalletLogin');
try {
Expand Down
2 changes: 0 additions & 2 deletions packages/paima-sdk/paima-mw-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { queryEndpoints } from './endpoints/queries';
import { utilityEndpoints } from './endpoints/utility';

import {
cardanoWalletLoginEndpoint,
retrievePostingInfo,
switchToBatchedCardanoMode,
switchToBatchedEthMode,
Expand Down Expand Up @@ -83,7 +82,6 @@ export {

// NOT FOR USE IN PRODUCTION, just internal endpoints and helper functions for easier testing and debugging:
export {
cardanoWalletLoginEndpoint,
retrievePostingInfo,
switchToBatchedCardanoMode,
switchToBatchedEthMode,
Expand Down
31 changes: 26 additions & 5 deletions packages/paima-sdk/paima-mw-core/src/wallets/algorand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,36 @@ import type { Result, Wallet } from '../types';
import { PaimaMiddlewareErrorCode, buildEndpointErrorFxn } from '../errors';
import { AlgorandConnector } from '@paima/providers';
import { getGameName } from '../state';
import { WalletMode } from './wallet-modes';

export async function algorandLoginWrapper(): Promise<Result<Wallet>> {
// TODO: the whole concept of converting wallet mode to names should be removed
function algorandWalletModeToName(walletMode: WalletMode): string {
switch (walletMode) {
case WalletMode.ALGORAND_PERA:
return 'pera';
default:
return '';
}
}

export async function algorandLoginWrapper(walletMode: WalletMode): Promise<Result<Wallet>> {
const errorFxn = buildEndpointErrorFxn('algorandLoginWrapper');

const walletName = algorandWalletModeToName(walletMode);
try {
const provider = await AlgorandConnector.instance().connectSimple({
gameName: getGameName(),
gameChainId: undefined,
});
const provider =
walletMode === WalletMode.ALGORAND
? await AlgorandConnector.instance().connectSimple({
gameName: getGameName(),
gameChainId: undefined, // Not needed because of batcher
})
: await AlgorandConnector.instance().connectNamed(
{
gameName: getGameName(),
gameChainId: undefined, // Not needed because of batcher
},
walletName
);
return {
success: true,
result: {
Expand Down
28 changes: 17 additions & 11 deletions packages/paima-sdk/paima-mw-core/src/wallets/evm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,25 @@ export async function evmLoginWrapper(walletMode: WalletMode): Promise<Result<Wa
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(
{
if (walletMode === WalletMode.EVM) {
await EvmConnector.instance().connectSimple({
gameName: getGameName(),
gameChainId: '0x' + getChainId().toString(16),
},
walletName
);
});
} else {
if (!walletName) {
return errorFxn(PaimaMiddlewareErrorCode.WALLET_NOT_SUPPORTED);
}
console.log(`[evmLoginWrapper] Attempting to log into ${walletName}`);
await EvmConnector.instance().connectNamed(
{
gameName: getGameName(),
gameChainId: '0x' + getChainId().toString(16),
},
walletName
);
}
} catch (err) {
if (err instanceof WalletNotFound || err instanceof UnsupportedWallet) {
return errorFxn(
Expand All @@ -122,7 +128,7 @@ export async function evmLoginWrapper(walletMode: WalletMode): Promise<Result<Wa
FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED
);
}
console.log(`[evmLoginWrapper] Error while logging into wallet ${walletName}`);
console.log(`[evmLoginWrapper] Error while logging into wallet ${walletName ?? 'simple'}`);

return errorFxn(PaimaMiddlewareErrorCode.EVM_LOGIN, err);
}
Expand Down
29 changes: 24 additions & 5 deletions packages/paima-sdk/paima-mw-core/src/wallets/polkadot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,34 @@ import { getGameName } from '../state';
import type { Result, Wallet } from '../types';
import { UnsupportedWallet, WalletNotFound } from '@paima/providers';
import { PolkadotConnector } from '@paima/providers';
import { WalletMode } from './wallet-modes';

export async function polkadotLoginWrapper(): Promise<Result<Wallet>> {
// TODO: the whole concept of converting wallet mode to names should be removed
function polkadotWalletModeToName(walletMode: WalletMode): string {
switch (walletMode) {
default:
return '';
}
}

export async function polkadotLoginWrapper(walletMode: WalletMode): Promise<Result<Wallet>> {
const errorFxn = buildEndpointErrorFxn('polkadotLoginWrapper');

const walletName = polkadotWalletModeToName(walletMode);
try {
const provider = await PolkadotConnector.instance().connectSimple({
gameName: getGameName(),
gameChainId: undefined,
});
const provider =
walletMode === WalletMode.POLKADOT
? await PolkadotConnector.instance().connectSimple({
gameName: getGameName(),
gameChainId: undefined, // Not needed because of batcher
})
: await PolkadotConnector.instance().connectNamed(
{
gameName: getGameName(),
gameChainId: undefined, // Not needed because of batcher
},
walletName
);
return {
success: true,
result: {
Expand Down
2 changes: 2 additions & 0 deletions packages/paima-sdk/paima-mw-core/src/wallets/wallet-modes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum WalletMode {
NO_WALLET,
EVM,
METAMASK,
EVM_FLINT,
CARDANO,
Expand All @@ -8,5 +9,6 @@ export enum WalletMode {
CARDANO_NAMI,
CARDANO_ETERNL,
POLKADOT,
ALGORAND,
ALGORAND_PERA,
}
10 changes: 8 additions & 2 deletions packages/paima-sdk/paima-mw-core/src/wallets/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ 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 'evm':
return WalletMode.EVM;
case 'metamask':
return WalletMode.METAMASK;
case 'evm-flint':
Expand All @@ -36,6 +38,8 @@ export function stringToWalletMode(loginType: string): WalletMode {
return WalletMode.POLKADOT;
case 'pera':
return WalletMode.ALGORAND_PERA;
case 'algorand':
return WalletMode.ALGORAND;
default:
return WalletMode.NO_WALLET;
}
Expand All @@ -48,6 +52,7 @@ export async function specificWalletLogin(
const errorFxn = buildEndpointErrorFxn('specificWalletLogin');

switch (walletMode) {
case WalletMode.EVM:
case WalletMode.METAMASK:
case WalletMode.EVM_FLINT:
if (preferBatchedMode) {
Expand All @@ -65,10 +70,11 @@ export async function specificWalletLogin(
return await cardanoLoginWrapper(walletMode);
case WalletMode.POLKADOT:
setBatchedPolkadotMode();
return await polkadotLoginWrapper();
return await polkadotLoginWrapper(walletMode);
case WalletMode.ALGORAND:
case WalletMode.ALGORAND_PERA:
setBatchedAlgorandMode();
return await algorandLoginWrapper();
return await algorandLoginWrapper(walletMode);
case WalletMode.NO_WALLET:
return errorFxn(FE_ERR_SPECIFIC_WALLET_NOT_INSTALLED);
default:
Expand Down
31 changes: 27 additions & 4 deletions packages/paima-sdk/paima-providers/src/IProvider.ts
Original file line number Diff line number Diff line change
@@ -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 <img> 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<T> = {
metadata: {
// TODO: should also expose the icon for the wallet
name: string;
};
metadata: WalletOption;
api: T;
};
export type ConnectionOption<T> = {
metadata: WalletOption;
api: () => Promise<T>;
};
export async function optionToActive<T>(option: ConnectionOption<T>): Promise<ActiveConnection<T>> {
const connection = {
metadata: option.metadata,
api: await option.api(),
};
return connection;
}

export type GameInfo = {
gameName: string;
Expand Down
66 changes: 47 additions & 19 deletions packages/paima-sdk/paima-providers/src/algorand.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
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<AlgorandApi> {
private provider: AlgorandProvider | undefined;
private static INSTANCE: undefined | AlgorandConnector = undefined;

static getWalletOptions(): ConnectionOption<AlgorandApi>[] {
// 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
return [
{
metadata: {
name: 'pera',
displayName: 'Pera Wallet',
},
api: async (): Promise<AlgorandApi> => {
const { PeraWalletConnect } = await import('@perawallet/connect');
const peraWallet = new PeraWalletConnect();
return peraWallet;
},
},
];
}

static instance(): AlgorandConnector {
if (AlgorandConnector.INSTANCE == null) {
const newInstance = new AlgorandConnector();
Expand All @@ -27,7 +56,11 @@ export class AlgorandConnector implements IConnector<AlgorandApi> {
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,
Expand All @@ -43,18 +76,13 @@ export class AlgorandConnector implements IConnector<AlgorandApi> {
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;
Expand Down
Loading

0 comments on commit d56eb72

Please sign in to comment.