Skip to content

Commit

Permalink
feat: Add particle wallet (#86)
Browse files Browse the repository at this point in the history
* feat: Setting `reloadOnDisconnect` = false in default for coinbaseWallet

* docs: Add changeset log

* feat: Add `closeModalAfterSwitchingNetwork`

* feat: Add particle wallet

* feat: Add particle wallet
  • Loading branch information
wenty22 authored Dec 22, 2023
1 parent f9b7e3d commit a9a34af
Show file tree
Hide file tree
Showing 13 changed files with 505 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-turkeys-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@totejs/walletkit': patch
---

Add particle wallet
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ dist-ssr
*.sln
*.sw?
.npmrc
.pnpm-store
.pnpm-store
.env
27 changes: 25 additions & 2 deletions packages/walletkit/dev/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,41 @@ import {
tokenPocket,
trustWallet,
walletConnect,
particleWallet,
} from '../src/wallets';
import React from 'react';
import { ParticleNetwork } from '@particle-network/auth';

new VConsole();

//!!! environment variables for testing, use directly
const PARTICLE_APP_APP_ID = '9f8f0969-f7b3-474b-ae93-8773231e6c05';
const PARTICLE_APP_PROJECT_ID = '33eea7b2-d76b-4b5a-978f-4413a6b70e82';
const PARTICLE_APP_CLIENT_KEY = 'clprc7kown00uAKQrWsMOAwzXXiWxYDMq9bpfTta';
const WALLET_CONNECT_PROJECT_ID = 'e68a1816d39726c2afabf05661a32767';

const particle = new ParticleNetwork({
projectId: PARTICLE_APP_PROJECT_ID as string,
clientKey: PARTICLE_APP_CLIENT_KEY as string,
appId: PARTICLE_APP_APP_ID as string,
wallet: { displayWalletEntry: true },
chainId: 204,
chainName: 'opBNB',
});

particle.setERC4337({
name: 'BICONOMY',
version: '2.0.0',
});

const config = createConfig(
getDefaultConfig({
autoConnect: true,
appName: 'WalletKit',

// WalletConnect 2.0 requires a projectId which you can create quickly
// and easily for free over at WalletConnect Cloud https://cloud.walletconnect.com/sign-in
walletConnectProjectId: 'e68a1816d39726c2afabf05661a32767',
walletConnectProjectId: WALLET_CONNECT_PROJECT_ID,

chains,
connectors: [
Expand All @@ -44,13 +66,14 @@ const config = createConfig(
binanceWeb3Wallet(),
coinbaseWallet(),
tokenPocket(),
particleWallet(),
walletConnect(),
],
}),
);

const options: WalletKitOptions = {
// initialChainId: 204,
initialChainId: 204,
};

export default function App() {
Expand Down
22 changes: 22 additions & 0 deletions packages/walletkit/dev/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ export const chains: Chain[] = [
default: { name: 'BSC Testnet Scan', url: `https://testnet.bscscan.com` },
},
},
{
id: 1017,
name: 'BNB Greenfield',
network: 'BNB Greenfield',
nativeCurrency: {
name: 'tBNB',
symbol: 'tBNB',
decimals: 18,
},
rpcUrls: {
default: {
http: [`https://greenfield-chain-us.bnbchain.org`],
},
public: {
http: [`https://greenfield-chain-us.bnbchain.org`],
},
},
blockExplorers: {
etherscan: { name: 'Greenfield Scan', url: `https://greenfieldscan.com` },
default: { name: 'Greenfield Scan', url: `https://greenfieldscan.com` },
},
},
bsc,
mainnet,
];
4 changes: 3 additions & 1 deletion packages/walletkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@
"viem": "^1.19.9",
"vite": "^4.5.0",
"vite-plugin-dts": "^3.6.3",
"wagmi": "^1.4.7"
"wagmi": "^1.4.7",
"@particle-network/auth": "^1.2.2"
},
"dependencies": {
"@particle-network/provider": "^1.2.1",
"qrcode": "^1.5.3"
}
}
2 changes: 1 addition & 1 deletion packages/walletkit/src/defaultConfig/getDefaultConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface ConnectWalletClientProps {
webSocketPublicClient?: WebSocketPublicClient;
}

const defaultChains = [mainnet];
const defaultChains: Chain[] = [mainnet];

export const getDefaultConfig = (props: DefaultConfigProps) => {
const {
Expand Down
2 changes: 2 additions & 0 deletions packages/walletkit/src/wallets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export * from './mathWallet';
export * from './binanceWeb3Wallet';
export * from './coinbaseWallet';
export * from './coinbaseWallet/connector';
export * from './particleWallet';
export * from './particleWallet/connector';
203 changes: 203 additions & 0 deletions packages/walletkit/src/wallets/particleWallet/connector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { PARTICLE_WALLET_ID } from '.';
import { ParticleProvider } from '@particle-network/provider';
import type { AuthType } from '@particle-network/auth';
import {
Chain,
ChainNotConfiguredError,
Connector,
ConnectorNotFoundError,
WalletClient,
} from 'wagmi';
import {
ProviderRpcError,
SwitchChainError,
UserRejectedRequestError,
createWalletClient,
custom,
getAddress,
numberToHex,
} from 'viem';

type ParticleAuth = ConstructorParameters<typeof ParticleProvider>[0];

export type ParticleConnectorOptions = {
shimDisconnect?: boolean;
auth?: ParticleAuth;
authType?: AuthType;
};

export class ParticleConnector extends Connector<ParticleProvider, ParticleConnectorOptions> {
readonly id: string = PARTICLE_WALLET_ID;
readonly name: string = 'Particle';
readonly ready: boolean;

protected shimDisconnectKey = `${this.id}.shimDisconnect`;
private provider?: ParticleProvider;

constructor({
chains,
options: _options,
}: {
chains?: Chain[];
options?: ParticleConnectorOptions;
} = {}) {
const options = {
name: 'Particle',
shimDisconnect: true,
..._options,
};

super({
chains,
options,
});

this.name = options.name ?? this.name;
this.ready = true;

this.onAccountsChanged = this.onAccountsChanged.bind(this);
this.onChainChanged = this.onChainChanged.bind(this);
this.onDisconnect = this.onDisconnect.bind(this);
}

async connect({ chainId }: { chainId?: number } = {}) {
if (!this.options.auth) {
throw new Error('Please init Particle first');
}

try {
const provider = await this.getProvider();
if (!provider) throw new ConnectorNotFoundError();

provider.on('accountsChanged', this.onAccountsChanged);
provider.on('chainChanged', this.onChainChanged);
provider.on('disconnect', this.onDisconnect);

this.emit('message', { type: 'connecting' });

if (!this.options.auth.isLogin()) {
await this.options.auth.login({
preferredAuthType: this.options.authType,
});
}

let id = await this.getChainId();
let unsupported = this.isChainUnsupported(id);
if (chainId && id !== chainId) {
const chain = await this.switchChain(chainId);
id = chain.id;
unsupported = this.isChainUnsupported(id);
}

const account = await this.getAccount();
return {
account,
chain: { id, unsupported },
};
} catch (error) {
if ((error as ProviderRpcError).code === 4001) {
throw new UserRejectedRequestError(error as Error);
}
throw error;
}
}

async disconnect() {
const provider = await this.getProvider();
await provider.disconnect();

provider.removeListener('accountsChanged', this.onAccountsChanged);
provider.removeListener('chainChanged', this.onChainChanged);
provider.removeListener('disconnect', this.onDisconnect);
}

async getAccount() {
const provider = await this.getProvider();
const accounts = await provider.request({ method: 'eth_accounts' });
return getAddress(accounts[0]);
}

async getChainId() {
const provider = await this.getProvider();
const chainId = await provider.request({ method: 'eth_chainId' });
return Number(chainId);
}

async getProvider() {
if (!this.options.auth) {
throw new Error('Please init Particle first');
}
if (!this.provider) {
const { ParticleProvider } = await import('@particle-network/provider');
this.provider = new ParticleProvider(this.options.auth);
}
return this.provider;
}

async getWalletClient({ chainId }: { chainId?: number } = {}) {
const [provider, account] = await Promise.all([this.getProvider(), this.getAccount()]);
const chain = this.chains.find((x) => x.id === chainId);

if (!provider) throw new Error('provider is required.');

return createWalletClient({
account,
chain,
transport: custom(provider),
}) as WalletClient;
}

async isAuthorized() {
if (!this.options.auth) {
throw new Error('Please init Particle first');
}
return this.options.auth.isLogin() && this.options.auth.walletExist();
}

async switchChain(chainId: number) {
const provider = await this.getProvider();
if (!provider) throw new ConnectorNotFoundError();
const id = numberToHex(chainId);

try {
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: id }],
});

return (
this.chains.find((x) => x.id === chainId) ?? {
id: chainId,
name: `Chain ${id}`,
network: `${id}`,
nativeCurrency: { name: 'Ether', decimals: 18, symbol: 'ETH' },
rpcUrls: { default: { http: [''] }, public: { http: [''] } },
}
);
} catch (error) {
const chain = this.chains.find((x) => x.id === chainId);
if (!chain) {
throw new ChainNotConfiguredError({ chainId, connectorId: this.id });
}
throw new SwitchChainError(error as Error);
}
}

protected onAccountsChanged(accounts: string[]) {
if (accounts.length === 0) {
this.emit('disconnect');
} else {
this.emit('change', { account: getAddress(accounts[0]) });
}
}

protected onChainChanged(chainId: number | string) {
const id = Number(chainId);
const unsupported = this.isChainUnsupported(id);
this.emit('change', { chain: { id, unsupported } });
}

protected onDisconnect() {
this.emit('disconnect');
}
}
22 changes: 22 additions & 0 deletions packages/walletkit/src/wallets/particleWallet/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SVGIconProps } from '../..';

export const ParticleWalletTransparentIcon = (props: SVGIconProps) => {
return <ParticleWalletIcon width={34} height={34} {...props} />;
};

export const ParticleWalletIcon = (props: SVGIconProps) => {
return (
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" {...props}>
<path
d="M0 18.1333C0 8.11857 8.11857 0 18.1333 0H49.8667C59.8814 0 68 8.11857 68 18.1333V49.8667C68 59.8814 59.8814 68 49.8667 68H18.1333C8.11857 68 0 59.8814 0 49.8667V18.1333Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.2812 13.2812H27.0938V27.0938H13.2812V13.2812ZM40.9062 27.0938H27.0938V40.9062H13.2812V54.7188H27.0938V40.9062H40.9062V54.7188H54.7188V40.9062H40.9062V27.0938ZM40.9062 27.0938V13.2812H54.7188V27.0938H40.9062Z"
fill="white"
/>
</svg>
);
};
Loading

0 comments on commit a9a34af

Please sign in to comment.