Skip to content
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
1 change: 1 addition & 0 deletions libs/model/src/aggregates/user/signIn/SignIn.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function SignIn(): Command<typeof schemas.SignIn> {
deserializeCanvas(session),
encodedAddress,
ss58Prefix,
{ walletId: wallet_id },
);

const verification_token = crypto.randomBytes(18).toString('hex');
Expand Down
15 changes: 15 additions & 0 deletions libs/model/src/services/session/verifySessionSignature.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Session } from '@canvas-js/interfaces';
import {
CANVAS_TOPIC,
WalletId,
addressSwapper,
getSessionSignerForDid,
} from '@hicommonwealth/shared';
Expand All @@ -13,6 +14,9 @@ export const verifySessionSignature = async (
session: Session,
address: string,
ss58_prefix?: number | null,
options?: {
walletId?: WalletId;
},
): Promise<void> => {
// Re-encode BOTH address if needed for substrate verification, to ensure matching
// between stored address (re-encoded based on community joined at creation time)
Expand Down Expand Up @@ -56,5 +60,16 @@ export const verifySessionSignature = async (
`session.did address (${walletAddress}) does not match (${expectedAddress})`,
);

const walletId = options?.walletId;
if (walletId && walletId === WalletId.Base) {
// base Wallet uses passkey / smart-account signatures that are encoded as
// ERC-4337 aggregator payloads. They cannot be verified with the legacy ECDSA check below,
// so we skip the SIWE verification for now and rely on the DID/address match above.
console.warn(
`Skipping SIWE session signature verification for wallet ${walletId}; unsupported signature length`,
);
return;
}

await signer.verifySession(CANVAS_TOPIC, session);
};
1 change: 1 addition & 0 deletions libs/shared/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export enum WalletId {
OkxWallet = 'okx-wallet',
bitgetWallet = 'bitget',
Gate = 'gate',
Base = 'base',
}

// Passed directly to Magic login.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { createBaseAccountSDK } from '@base-org/account';
import type Web3 from 'web3';
import type BlockInfo from '../../../models/BlockInfo';
import type IWebWallet from '../../../models/IWebWallet';

import { SIWESigner } from '@canvas-js/chain-ethereum';
import { getChainHex } from '@hicommonwealth/evm-protocols';
import { ChainBase, ChainNetwork, WalletId } from '@hicommonwealth/shared';
import { setActiveAccount } from 'controllers/app/login';
import app from 'state';
import { fetchCachedPublicEnvVar } from 'state/api/configuration';
import { userStore } from 'state/ui/user';
import { Web3BaseProvider } from 'web3';
import { hexToNumber } from 'web3-utils';

class BaseWebWalletController implements IWebWallet<string> {
// GETTERS/SETTERS
private _enabled: boolean;
private _enabling = false;
private _accounts: string[];
private _provider: Web3BaseProvider | undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _web3: Web3 | any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _sdk: any;

public readonly name = WalletId.Base;
public readonly label = 'Base Wallet';
public readonly defaultNetwork = ChainNetwork.Ethereum;
public readonly chain = ChainBase.Ethereum;

public get available() {
// base wallet/account is always available as it's a web-based solution
return true;
}

public get provider() {
return this._provider!;
}

public get enabled() {
return this.available && this._enabled;
}

public get enabling() {
return this._enabling;
}

public get accounts() {
return this._accounts || [];
}

public get api() {
return this._web3;
}

public getChainId() {
return app.chain?.meta?.ChainNode?.eth_chain_id?.toString() || '1'; // use community chain or default to mainnet
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async getRecentBlock(chainIdentifier: string): Promise<BlockInfo> {
const block = await this._web3.givenProvider.request({
method: 'eth_getBlockByNumber',
params: ['latest', false],
});

return {
number: Number(hexToNumber(block.number)),
hash: block.hash,
timestamp: Number(hexToNumber(block.timestamp)),
};
}

public getSessionSigner() {
// base wallet accounts are smart-contract / passkey wallets - their `personal_sign` output is
// an ERC-4337 aggregator payload rather than a legacy 65-byte ECDSA signature. The server
// accounts for this by skipping the SIWE verification path for Base sessions until we have a
// way to verify the signature.
return new SIWESigner({
signer: {
signMessage: (message) =>
this._web3.givenProvider.request({
method: 'personal_sign',
params: [message, this.accounts[0]],
}),
getAddress: () => this.accounts[0],
},
chainId: parseInt(this.getChainId()),
});
}

// ACTIONS
public async enable(forceChainId?: string) {
console.log('Attempting to enable Base wallet');
this._enabling = true;
try {
// initialize base SDK
this._sdk = createBaseAccountSDK({
appName: 'Commonwealth',
// TODO: fix this image
appLogoUrl: `${window.location.origin}/assets/img/branding/common-logo.svg`,
});

const baseProvider = this._sdk.getProvider();

const Web3 = (await import('web3')).default;
this._web3 = {
givenProvider: baseProvider,
};

this._accounts = (
await this._web3.givenProvider.request({
method: 'eth_requestAccounts',
})
).map((addr: string) => {
return Web3.utils.toChecksumAddress(addr);
});

this._provider = baseProvider;

if (this._accounts.length === 0) {
throw new Error('Base wallet fetched no accounts');
}

// force the correct chain
const chainId = forceChainId ?? this.getChainId();
const chainIdHex = getChainHex(parseInt(chainId, 10));

try {
const config = fetchCachedPublicEnvVar();
if (config?.TEST_EVM_ETH_RPC !== 'test') {
await this._web3.givenProvider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }],
});
}
} catch (switchError) {
// add chain to wallet if not added already
if (switchError.code === 4902) {
const wsRpcUrl = new URL(app.chain?.meta?.ChainNode?.url || '');
const rpcUrl =
app.chain?.meta?.ChainNode?.alt_wallet_url ||
`https://${wsRpcUrl.host}`;

// add requested chain to wallet
await this._web3.givenProvider.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: chainIdHex,
chainName: app.chain?.meta?.name || 'Custom Chain',
nativeCurrency: {
name: 'ETH',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: [rpcUrl],
},
],
});
} else {
throw switchError;
}
}

await this.initAccountsChanged();
this._enabled = true;
this._enabling = false;
} catch (error) {
let errorMsg = `Failed to enable Base wallet: ${error.message}`;
if (error.code === 4902) {
errorMsg = `Failed to enable Base wallet: Please add chain ID ${
app?.chain?.meta?.ChainNode?.eth_chain_id || 0
}`;
}
console.error(errorMsg);
this._enabling = false;
throw new Error(errorMsg);
}
}

public async initAccountsChanged() {
// base SDK handles account changes internally
// we only listen for account changes
if (this._web3?.givenProvider?.on) {
await this._web3.givenProvider.on(
'accountsChanged',
async (accounts: string[]) => {
const updatedAddress = userStore
.getState()
.accounts.find((addr) => addr.address === accounts[0]);
if (!updatedAddress) return;
await setActiveAccount(updatedAddress);
},
);
}
}

public async switchNetwork(chainId?: string) {
try {
const communityChain = chainId ?? this.getChainId();
const chainIdHex = getChainHex(parseInt(communityChain, 10));

try {
await this._web3.givenProvider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: chainIdHex }],
});
} catch (error) {
if (error.code === 4902) {
const rpcUrl =
app?.chain?.meta?.ChainNode?.alt_wallet_url ||
app?.chain?.meta?.ChainNode?.url ||
'';

await this._web3.givenProvider.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: chainIdHex,
chainName: app.chain?.meta?.name || 'Custom Chain',
nativeCurrency: {
name: 'ETH',
symbol: 'ETH',
decimals: 18,
},
rpcUrls: [rpcUrl],
},
],
});
}
}
} catch (error) {
console.error('Error checking and switching chain:', error);
}
}

public async reset() {
if (this._sdk) {
try {
await this._sdk.getProvider().request({ method: 'wallet_disconnect' });
} catch (error) {
console.error('Error disconnecting from Base wallet:', error);
}
}
this._enabled = false;
this._accounts = [];
this._provider = undefined;
this._web3 = null;
this._sdk = null;
}
}

export default BaseWebWalletController;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { userStore } from 'state/ui/user';
import Account from '../../models/Account';
import IWebWallet from '../../models/IWebWallet';
import BackpackWebWalletController from './webWallets/backpack_web_wallet';
import BaseWebWalletController from './webWallets/base_web_wallet';
import BinanceWebWalletController from './webWallets/binance_web_wallet';
import BitgetWebWalletController from './webWallets/bitget_web_wallet';
import CoinbaseWebWalletController from './webWallets/coinbase_web_wallet';
Expand Down Expand Up @@ -139,6 +140,7 @@ export default class WebWalletController {
new PhantomWebWalletController(),
new TerraWalletConnectWebWalletController(),
new CoinbaseWebWalletController(),
new BaseWebWalletController(),
new BackpackWebWalletController(),
new SolflareWebWalletController(),
new SuiWebWalletController(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ export const AUTH_TYPES: AuthTypesList = {
},
label: 'Coinbase',
},
base: {
icon: {
name: 'basewallet',
isCustom: true,
},
label: 'Base Wallet',
},
terrastation: {
icon: {
name: 'terrastation',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type SubstrateWallets = 'polkadot';
export type SolanaWallets = 'phantom' | 'backpack' | 'solflare';
export type SuiWallets = 'sui-wallet' | 'suiet' | 'okx-wallet' | 'bitget';
export type EVMWallets =
| 'base'
| 'walletconnect'
| 'metamask'
| 'gate'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1394,3 +1394,23 @@ export const CWGate = (props: CustomIconProps) => {
</svg>
);
};

export const CWBaseWallet = (props: CustomIconProps) => {
const { componentType, iconSize, ...otherProps } = props;
return (
<svg
className={getClasses<CustomIconStyleProps>({ iconSize }, componentType)}
width="32"
height="32"
viewBox="0 0 249 249"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...otherProps}
>
<path
d="M0 19.671C0 12.9332 0 9.56425 1.26956 6.97276C2.48511 4.49151 4.49151 2.48511 6.97276 1.26956C9.56425 0 12.9332 0 19.671 0H229.329C236.067 0 239.436 0 242.027 1.26956C244.508 2.48511 246.515 4.49151 247.73 6.97276C249 9.56425 249 12.9332 249 19.671V229.329C249 236.067 249 239.436 247.73 242.027C246.515 244.508 244.508 246.515 242.027 247.73C239.436 249 236.067 249 229.329 249H19.671C12.9332 249 9.56425 249 6.97276 247.73C4.49151 246.515 2.48511 244.508 1.26956 242.027C0 239.436 0 236.067 0 229.329V19.671Z"
fill="#0000FF"
/>
</svg>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ export const iconLookup = {

export const customIconLookup = {
base: CustomIcons.CWBase,
basewallet: CustomIcons.CWBaseWallet,
blast: CustomIcons.CWBlast,
binance: CustomIcons.CWBinance,
google: Icons.CWGoogle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ const AddressItem = (props: AddressItemProps) => {
iconLeft={
walletId === WalletId.Magic
? getSsoIconName(walletSsoSource)
: walletId
: walletId === 'base'
? 'basewallet'
: walletId
}
address={`\u2022 ${formatAddressShort(address)}`}
/>
Expand Down
Loading
Loading