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
4 changes: 2 additions & 2 deletions wallets/react/src/legacy/useLegacyProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export function useLegacyProviders(
*/
document.addEventListener('readystatechange', initWhenPageIsReady);
});
}, []);
}, [props.providers]);

// Setting supported blockchains on instances
useEffect(() => {
Expand All @@ -222,7 +222,7 @@ export function useLegacyProviders(
});
});
}
}, [props.allBlockChains]);
}, [props.allBlockChains, props.providers]);

// Setting event handler on instances
useEffect(() => {
Expand Down
6 changes: 5 additions & 1 deletion widget/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@
"@rango-dev/logging-console": "^0.11.0",
"@rango-dev/logging-subscriber": "^0.11.0",
"@rango-dev/widget-embedded": "^0.50.1-next.7",
"@privy-io/react-auth": "^2.24.0",
"@solana/kit": "^3.0.3",
"@solana/spl-token": "^0.4.14",
"@solana/web3.js": "^1.91.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.8.0"
}
}
}
24 changes: 23 additions & 1 deletion widget/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ import {
TREZOR_MANIFEST,
WC_PROJECT_ID,
} from './constants';
import { usePrivyProvider } from './usePrivyProvider';

export function App() {
const [searchParams] = useSearchParams();
const configRef = useRef<WidgetConfig>();
const configParam = searchParams.get('config');
const {
provider: privyProvider,
exportEvmWallet,
exportSolanaWallet,
} = usePrivyProvider();

let config: WidgetConfig | undefined = undefined;

Expand Down Expand Up @@ -41,6 +47,7 @@ export function App() {
walletConnectProjectId: WC_PROJECT_ID,
trezorManifest: TREZOR_MANIFEST,
tonConnect: { manifestUrl: TON_CONNECT_MANIFEST_URL },
wallets: ['metamask', privyProvider],
};
}
if (!!config) {
Expand All @@ -50,7 +57,22 @@ export function App() {

return (
<Routes>
<Route path="/*" element={<Widget config={configRef.current} />} />
<Route
path="/*"
element={
<div>
<div>
<button id="export-evm-btn" onClick={exportEvmWallet}>
Export EVM Wallet
</button>
<button id="export-solana-btn" onClick={exportSolanaWallet}>
Export Solana Wallet
</button>
</div>
<Widget config={configRef.current} />
</div>
}
/>
</Routes>
);
}
34 changes: 31 additions & 3 deletions widget/app/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { PrivyProvider } from '@privy-io/react-auth';
import { layer as consoleLayer } from '@rango-dev/logging-console';
import { init, Level } from '@rango-dev/logging-subscriber';
import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit';
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
Expand All @@ -16,7 +18,33 @@ const container = document.getElementById('app')!;
const root = createRoot(container);

root.render(
<BrowserRouter>
<App />
</BrowserRouter>
<PrivyProvider
appId="cmfuz5c0k00gfju0cacxr36so"
config={{
solana: {
rpcs: {
'solana:mainnet': {
rpc: createSolanaRpc(
'https://purple-practical-friday.solana-mainnet.quiknode.pro/d94ab067f793d48c81354c78c86ae908d9fc1582/'
),
rpcSubscriptions: createSolanaRpcSubscriptions(
'wss://api.mainnet-beta.solana.com'
),
},
},
},
embeddedWallets: {
ethereum: {
createOnLogin: 'users-without-wallets',
},
solana: {
createOnLogin: 'users-without-wallets',
},
},
loginMethods: ['email'],
}}>
<BrowserRouter>
<App />
</BrowserRouter>
</PrivyProvider>
);
243 changes: 243 additions & 0 deletions widget/app/src/usePrivyProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import type { ConnectedStandardSolanaWallet } from '@privy-io/react-auth/solana';
import type { SolanaWeb3Signer } from '@rango-dev/signer-solana';
import type { Transaction, VersionedTransaction } from '@solana/web3.js';
import type {
BlockchainMeta,
GenericSigner,
SolanaTransaction,
} from 'rango-types';

import { usePrivy, useWallets as usePrivyWallets } from '@privy-io/react-auth';
import {
useConnectedStandardWallets,
useExportWallet,
} from '@privy-io/react-auth/solana';
import { DefaultEvmSigner } from '@rango-dev/signer-evm';
import { generalSolanaTransactionExecutor } from '@rango-dev/signer-solana';
import {
canEagerlyConnectToEvm,
getEvmAccounts,
} from '@rango-dev/wallets-shared';
import { type ProviderInterface } from '@rango-dev/widget-embedded';
import base58 from 'bs58';
import {
DefaultSignerFactory,
evmBlockchains,
isEvmBlockchain,
solanaBlockchain,
TransactionType,
} from 'rango-types';
import { useCallback, useEffect, useRef } from 'react';

const USER_CHECK_INTERVAL = 500;

export function usePrivyProvider(): {
provider: ProviderInterface;
exportEvmWallet: () => Promise<void>;
exportSolanaWallet: () => Promise<void>;
} {
const { user, login, logout, ready } = usePrivy();
const { wallets: privyEvmWallets } = usePrivyWallets();
const { wallets: privySolanaWallets } = useConnectedStandardWallets();

const { exportWallet: exportEvmWallet } = usePrivy();
const { exportWallet: exportSolanaWallet } = useExportWallet();

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const instanceRef = useRef<Map<string, any | null>>();

const updateInstance = async () => {
if (!readyRef.current) {
return;
}

const instances = new Map();
const evmWallet = await privyEvmWallets?.[0]?.getEthereumProvider();
const solanaWallet = privySolanaWallets?.[0];

instances.set('ETH', evmWallet);
instances.set('Solana', solanaWallet);
instanceRef.current = instances;
};

const userRef = useRef(user);
useEffect(() => {
userRef.current = user;
void updateInstance();
}, [user]);
const readyRef = useRef(ready);
useEffect(() => {
readyRef.current = ready;
void updateInstance();
}, [ready]);
useEffect(() => {
void updateInstance();
}, [privyEvmWallets, privySolanaWallets]);
const loginRef = useRef(login);
useEffect(() => {
loginRef.current = login;
}, [login]);

const getInstance = () => {
return instanceRef.current;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const connect = async (): Promise<any> => {
if (!!instanceRef.current?.get('ETH')) {
const evmProvider = instanceRef.current?.get('ETH');
const solanaProvider = instanceRef.current?.get('Solana');
const result = [];
if (evmProvider) {
const evmResult = await getEvmAccounts(evmProvider);
result.push(evmResult);
}
if (solanaProvider && solanaProvider.address) {
result.push({
accounts: [solanaProvider.address],
chainId: 'SOLANA',
});
}

return result;
}
await loginRef.current();
const getUserPromise = new Promise((resolve) => {
const intervalId = setInterval(async () => {
if (!!instanceRef.current?.get('ETH')) {
clearInterval(intervalId);
const provider = instanceRef.current?.get('ETH');
const { accounts, chainId } = await getEvmAccounts(provider);

resolve({ accounts, chainId });
}
}, USER_CHECK_INTERVAL);
});
const result = await getUserPromise;

return result;
};

const canEagerConnect = async ({ meta }: { meta: BlockchainMeta[] }) => {
try {
if (!!instanceRef.current?.get('ETH')) {
const instance = instanceRef.current.get('ETH');
return await canEagerlyConnectToEvm({ instance, meta });
}
return false;
} catch {
return false;
}
};

const switchNetwork = useCallback(
async ({
network,
meta,
}: {
network: string;
meta: BlockchainMeta[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}): Promise<any> => {
const chainId = meta.find((chain) => chain.name === network)?.chainId;
if (!!chainId && !!instanceRef.current?.get('ETH')) {
const provider = instanceRef.current.get('ETH');
await provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId }],
});
}
},
[]
);

const disconnect = async () => {
await logout();
};

const getSigners = async () => {
const signers = new DefaultSignerFactory();
const evmProvider = instanceRef.current?.get('ETH');
const solanaProvider = instanceRef.current?.get('Solana');
signers.registerSigner(
TransactionType.EVM,
new DefaultEvmSigner(evmProvider)
);
signers.registerSigner(
TransactionType.SOLANA,
new SolanaSigner(solanaProvider)
);

return signers;
};

const canSwitchNetworkTo = useCallback(
({ network, meta }: { network: string; meta: BlockchainMeta[] }) => {
return meta
.filter(isEvmBlockchain)
.map((blockchain) => blockchain.name)
.includes(network);
},
[]
);

return {
exportEvmWallet,
exportSolanaWallet,
provider: {
config: {
type: 'privy',
},
connect,
disconnect,
getInstance,
canEagerConnect,
switchNetwork,
getSigners,
canSwitchNetworkTo,
// switchNetwork,
getWalletInfo: (allBlockChains: BlockchainMeta[]) => {
const evms = evmBlockchains(allBlockChains);
const solana = solanaBlockchain(allBlockChains);
return {
name: 'Privy',
img: 'https://raw.githubusercontent.com/rango-exchange/assets/main/wallets/privy/icon.svg',
installLink: {
DEFAULT: 'https://www.privy.io/',
},
color: '#dac7ae',
supportedChains: [...evms, ...solana],
};
},
},
};
}

export class SolanaSigner implements GenericSigner<SolanaTransaction> {
private provider: ConnectedStandardSolanaWallet;
constructor(provider: ConnectedStandardSolanaWallet) {
this.provider = provider;
}

async signMessage(msg: string): Promise<string> {
const encodedMessage = new TextEncoder().encode(msg);
const signedMessage = await this.provider.signMessage({
message: encodedMessage,
});
return base58.encode(signedMessage.signedMessage);
}
async signAndSendTx(tx: SolanaTransaction): Promise<{ hash: string }> {
const DefaultSolanaSigner: SolanaWeb3Signer = async (
solanaWeb3Transaction: Transaction | VersionedTransaction
) => {
const signature = await this.provider.signTransaction({
transaction: solanaWeb3Transaction.serialize(),
});
return signature.signedTransaction;
};
const hash = await generalSolanaTransactionExecutor(
tx,
DefaultSolanaSigner
);
return { hash };
}
}
Loading