Skip to content

Commit

Permalink
Solana provider base implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
qrtp committed Nov 27, 2024
1 parent e4642b1 commit eaf39af
Show file tree
Hide file tree
Showing 17 changed files with 1,095 additions and 98 deletions.
8 changes: 4 additions & 4 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
CUSTOM_RUNTIME=extension
ALWAYS_PROMPT_COMPATIBILITY_MODE=true
BUGSNAG_API_KEY=4a72fb23d3ddc00054db744c3006f0cd
DEFAULT_CHAIN=1
LD_CLIENT_ID=6272a84a39dbeb158b34bd58
NODE_ENV=production
DEFAULT_CHAIN=1
BUGSNAG_API_KEY=4a72fb23d3ddc00054db744c3006f0cd
ALWAYS_PROMPT_COMPATIBILITY_MODE=true
SOLANA_ENABLED=false
VERSION_DESCRIPTION=`Unstoppable Lite Wallet is a web3 wallet built for domainers. Enable your **free wallet** to get started with Bitcoin, Ethereum, Base, Polygon and Solana.

This version contains the following updates:
Expand Down
7 changes: 4 additions & 3 deletions .env.staging
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
ALWAYS_PROMPT_COMPATIBILITY_MODE=true
BUGSNAG_API_KEY=4a72fb23d3ddc00054db744c3006f0cd
DEFAULT_CHAIN=11155111
LD_CLIENT_ID=6272a84a39dbeb158b34bd57
NODE_ENV=staging
DEFAULT_CHAIN=11155111
BUGSNAG_API_KEY=4a72fb23d3ddc00054db744c3006f0cd
ALWAYS_PROMPT_COMPATIBILITY_MODE=true
SOLANA_ENABLED=true
VERSION_DESCRIPTION=`Development build, **staging / testnet environment only**. Contains the following updates:
- Sherlock Assistant magnifying glass
- Optimize performance
Expand Down
7 changes: 4 additions & 3 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
ALWAYS_PROMPT_COMPATIBILITY_MODE=true
BUGSNAG_API_KEY=abc123
DEFAULT_CHAIN=1
LD_CLIENT_ID=123
NODE_ENV=staging
DEFAULT_CHAIN=1
BUGSNAG_API_KEY=abc123
ALWAYS_PROMPT_COMPATIBILITY_MODE=true
SOLANA_ENABLED=true
VERSION_DESCRIPTION="Information about this release"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
},
"dependencies": {
"@metamask/providers": "^17.1.2",
"@types/bs58": "^4.0.4",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@unstoppabledomains/config": "0.0.19",
Expand Down
240 changes: 168 additions & 72 deletions src/pages/Wallet/Connect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import {PublicKey} from "@solana/web3.js";
import {fetcher} from "@xmtp/proto";
import type {Signer} from "ethers";
import Markdown from "markdown-to-jsx";
import queryString from "query-string";
import React, {useEffect, useState} from "react";
import useIsMounted from "react-is-mounted-hook";
Expand Down Expand Up @@ -41,12 +44,35 @@ import {
isPermissionType,
} from "../../types/wallet/provider";

// define connection popup states
enum ConnectionState {
ACCOUNT = 1,
CHAINID,
PERMISSIONS,
SIGN,
SWITCH_CHAIN,
SOLANA_SIGN_MESSAGE,
SOLANA_SIGN_TX,
}

// define groups of connection popup states
const SOLANA_STATES = [
ConnectionState.SOLANA_SIGN_MESSAGE,
ConnectionState.SOLANA_SIGN_TX,
];
const CONNECT_ACCOUNT_STATES = [
ConnectionState.ACCOUNT,
ConnectionState.SWITCH_CHAIN,
];
const RENDER_CONTENT_STATES = [
ConnectionState.PERMISSIONS,
...CONNECT_ACCOUNT_STATES,
...SOLANA_STATES,
];

interface connectedAccount {
address: string;
networkId?: number | string;
}

const Connect: React.FC = () => {
Expand All @@ -59,7 +85,9 @@ const Connect: React.FC = () => {
const {connections} = useConnections();
const [isConnected, setIsConnected] = useState<boolean>();
const [accountAssets, setAccountAssets] = useState<BootstrapState>();
const [accountEvmAddresses, setAccountEvmAddresses] = useState<any[]>([]);
const [accountAddresses, setAccountAddresses] = useState<connectedAccount[]>(
[],
);
const [errorMessage, setErrorMessage] = useState<string>();
const [isLoaded, setIsLoaded] = useState(false);
const navigate = useNavigate();
Expand Down Expand Up @@ -101,6 +129,7 @@ const Connect: React.FC = () => {
return;
}

// build list of EVM addresses
const evmAddresses = [
...new Set(
signInState.assets
Expand All @@ -114,11 +143,31 @@ const Connect: React.FC = () => {
),
];

if (evmAddresses.length === 0) {
// build list of Solana addresses
const solanaAddresses = [
...new Set(
signInState.assets
?.map(a => {
return {
address: a.address,
networkId: "solana",
};
})
.filter(a => {
try {
return PublicKey.isOnCurve(a.address);
} catch (e) {
return false;
}
}),
),
];

if (evmAddresses.length === 0 && solanaAddresses.length === 0) {
return;
}
setAccountAssets(signInState);
setAccountEvmAddresses(evmAddresses);
setAccountAddresses([...evmAddresses, ...solanaAddresses]);

// retrieve the source tab information if available, used to show the name
// and logo of the calling application
Expand Down Expand Up @@ -241,6 +290,7 @@ const Connect: React.FC = () => {

// handle the message
switch (message.type) {
// EVM handlers
case "accountRequest":
setConnectionStateMessage(message);
setConnectionState(ConnectionState.CHAINID);
Expand Down Expand Up @@ -281,6 +331,11 @@ const Connect: React.FC = () => {
case "closeWindowRequest":
handleClose();
break;
// Solana handlers
case "signSolanaMessageRequest":
setConnectionStateMessage(message);
setConnectionState(ConnectionState.SOLANA_SIGN_MESSAGE);
break;
// the following messages types can silently be ignored, as they
// are not relevant in the connect window
case "getPreferencesRequest":
Expand Down Expand Up @@ -323,11 +378,20 @@ const Connect: React.FC = () => {
};
}, [web3Deps]);

const getAccount = (chainId: number = config.DEFAULT_CHAIN) => {
const matchingAccount = accountEvmAddresses.find(
a => chainId === a.networkId,
);
return matchingAccount?.address ? matchingAccount : accountEvmAddresses[0];
const getAccount = (chainId?: number | string) => {
// if chainID is not specified, determine the default
if (!chainId) {
// determine type of permission request
const isSolana =
(connectionState && SOLANA_STATES.includes(connectionState)) ||
(connectionStateMessage?.params &&
connectionStateMessage.params.length > 0 &&
!!connectionStateMessage.params[0].solana_accounts);
chainId = isSolana ? "solana" : config.DEFAULT_CHAIN;
}

const matchingAccount = accountAddresses.find(a => chainId === a.networkId);
return matchingAccount?.address ? matchingAccount : accountAddresses[0];
};

const handleGetChainId = () => {
Expand Down Expand Up @@ -423,6 +487,7 @@ const Connect: React.FC = () => {

// add permission to accepted permission list
acceptedPermissions.push({
account,
parentCapability: permission,
date: new Date().getTime(),
caveats: [
Expand Down Expand Up @@ -492,6 +557,25 @@ const Connect: React.FC = () => {
);
};

const handleSignSolanaMessage = async () => {
// retrieve encoded message
const encodedMsg = fetcher.b64Decode(connectionStateMessage.params[0]);

// TODO - send the message to backend API to be signed
var decodedMsg = new TextDecoder().decode(encodedMsg);
Logger.log(
"Signing message",
JSON.stringify({decodedMsg, encodedMsg}, undefined, 2),
);

// TODO - return actual signature once backend is available
const signature = connectionStateMessage.params[0];
await chrome.runtime.sendMessage({
type: "signSolanaMessageResponse",
response: signature,
});
};

const handleSignTypedMessage = async (params: any[]) => {
try {
// validate there are at least two available parameter args
Expand Down Expand Up @@ -588,84 +672,96 @@ const Connect: React.FC = () => {
};

const renderButton = () => {
// no work to do until a connection state is set
if (!connectionState) {
return;
}

// show error message if present
if (errorMessage) {
return (
<Box mb={5}>
<Alert severity="error">{errorMessage}</Alert>
</Box>
);
} else if (
connectionState &&
[ConnectionState.ACCOUNT, ConnectionState.SWITCH_CHAIN].includes(
connectionState,
)
) {
return (
<Button
onClick={handleConnectAccount}
disabled={!isLoaded || errorMessage !== undefined}
fullWidth
variant="contained"
>
{t("common.connect")}
</Button>
);
} else {
return (
<Button
onClick={handleRequestPermissions}
disabled={!isLoaded || errorMessage !== undefined}
fullWidth
variant="contained"
>
{t("wallet.approve")}
</Button>
);
}

// render a confirmation button with different behavior depending on the
// connection popup state
return (
<Button
fullWidth
variant="contained"
disabled={!isLoaded || errorMessage !== undefined}
onClick={
CONNECT_ACCOUNT_STATES.includes(connectionState)
? handleConnectAccount
: connectionState === ConnectionState.SOLANA_SIGN_MESSAGE
? handleSignSolanaMessage
: connectionState === ConnectionState.PERMISSIONS
? handleRequestPermissions
: undefined
}
>
{CONNECT_ACCOUNT_STATES.includes(connectionState)
? t("common.connect")
: t("wallet.approve")}
</Button>
);
};

// show wallet connect information
return (
<Paper className={classes.container}>
<Box className={cx(classes.walletContainer, classes.contentContainer)}>
{connectionState &&
[
ConnectionState.ACCOUNT,
ConnectionState.PERMISSIONS,
ConnectionState.SWITCH_CHAIN,
].includes(connectionState) && (
<Box className={classes.contentContainer}>
<Typography variant="h4">{t("wallet.signMessage")}</Typography>
{web3Deps?.unstoppableWallet?.connectedApp && (
<SignForDappHeader
name={web3Deps.unstoppableWallet.connectedApp.name}
iconUrl={web3Deps.unstoppableWallet.connectedApp.iconUrl}
hostUrl={web3Deps.unstoppableWallet.connectedApp.hostUrl}
actionText={
connectionState === ConnectionState.PERMISSIONS
? t("extension.connectRequest")
: connectionState === ConnectionState.SWITCH_CHAIN
? t("extension.connectToChain", {
chainName:
connectionStateMessage.params[0].chainName,
})
{connectionState && RENDER_CONTENT_STATES.includes(connectionState) && (
<Box className={classes.contentContainer}>
<Typography variant="h4">{t("wallet.signMessage")}</Typography>
{web3Deps?.unstoppableWallet?.connectedApp && (
<SignForDappHeader
name={web3Deps.unstoppableWallet.connectedApp.name}
iconUrl={web3Deps.unstoppableWallet.connectedApp.iconUrl}
hostUrl={web3Deps.unstoppableWallet.connectedApp.hostUrl}
actionText={
connectionState === ConnectionState.PERMISSIONS
? t("extension.connectRequest")
: connectionState === ConnectionState.SWITCH_CHAIN
? t("extension.connectToChain", {
chainName: connectionStateMessage.params[0].chainName,
})
: connectionState === ConnectionState.SOLANA_SIGN_MESSAGE
? t("wallet.signMessageAction")
: t("extension.connect")
}
/>
)}
<Typography variant="body1" mt={3}>
{t("auth.walletAddress")}:
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: "bold",
}}
>
{getAccount()?.address}
</Typography>
</Box>
)}
}
/>
)}
<Typography variant="body1" mt={3}>
{t("auth.walletAddress")}:
</Typography>
<Typography
variant="body2"
sx={{
fontWeight: "bold",
}}
>
{getAccount()?.address}
</Typography>
{connectionState === ConnectionState.SOLANA_SIGN_MESSAGE && (
<>
<Typography mt={3} variant="body1">
{t("wallet.signMessageSubtitle")}:
</Typography>
<Box className={classes.messageContainer}>
<Markdown>
{new TextDecoder().decode(
fetcher.b64Decode(connectionStateMessage.params[0]),
)}
</Markdown>
</Box>
</>
)}
</Box>
)}
<Box className={classes.contentContainer}>
{renderButton()}
<Box mt={1} className={classes.contentContainer}>
Expand Down
Loading

0 comments on commit eaf39af

Please sign in to comment.