Skip to content

Commit

Permalink
feat: Add support for solana wallet address (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
skambalin authored May 29, 2024
1 parent a88a23a commit e342859
Show file tree
Hide file tree
Showing 18 changed files with 535 additions and 22 deletions.
328 changes: 326 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/embed-wallet/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export type UserInfo = {

export type WalletAccount = {
address: string;
type: 'ethereum' | 'ed25519';
type: 'ethereum' | 'ed25519' | 'solana';
name: string;
};

Expand Down
36 changes: 36 additions & 0 deletions packages/ui/src/icons/SolanaIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { memo } from 'react';
import { SvgIcon, SvgIconProps } from '@mui/material';

export const SolanaIcon = memo((props: SvgIconProps) => (
<SvgIcon {...props} viewBox="100 100 300 300">
<path
d="M115.6 226.1H347c2.9 0 5.6 1.1 7.6 3.2l36.6 36.8c6.8 6.8 2 18.4-7.6 18.4H152.2c-2.9 0-5.6-1.1-7.6-3.2L108 244.5c-6.8-6.7-2-18.4 7.6-18.4zm-7.7-48.8 36.6-36.8c2.1-2.1 4.8-3.2 7.6-3.2h231.3c9.6 0 14.5 11.6 7.6 18.4l-36.5 36.8c-2 2.1-4.8 3.2-7.6 3.2H115.6c-9.6 0-14.4-11.6-7.7-18.4zm283.2 156.2-36.6 36.9c-2 2-4.8 3.2-7.6 3.2H115.6c-9.6 0-14.4-11.6-7.7-18.4l36.6-36.9c2.1-2 4.8-3.2 7.6-3.2h231.3c9.7-.1 14.6 11.5 7.7 18.4z"
fill="url(#solana-gradient)"
/>

<defs>
<linearGradient
id="solana-gradient"
x1={242.52}
x2={755.68}
y1={267.33}
y2={-245.83}
gradientTransform="matrix(.5 0 0 .5 0 250)"
gradientUnits="userSpaceOnUse"
>
<stop
offset={0}
style={{
stopColor: '#cb4ee8',
}}
/>
<stop
offset={1}
style={{
stopColor: '#10f4b1',
}}
/>
</linearGradient>
</defs>
</SvgIcon>
));
1 change: 1 addition & 0 deletions packages/ui/src/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export * from './ComingSoonIcon';
export * from './EthIcon';
export * from './UsdcIcon';
export * from './UsdtIcon';
export * from './SolanaIcon';
export * from './TransactionInIcon';
export * from './TransactionOutIcon';
export * from './TopUpIcon';
Expand Down
9 changes: 6 additions & 3 deletions packages/wallet-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@
"dependencies": {
"@biconomy/mexa": "^3.0.6",
"@cere/freeport-sc-sdk": "0.23.0",
"@polkadot/keyring": "^11.1.2",
"@polkadot/api": "^10.2.1",
"@polkadot/keyring": "^11.1.2",
"@polkadot/types": "^10.2.1",
"@polkadot/util": "^11.1.2",
"@polkadot/util-crypto": "^11.1.2",
"@polkadot/types": "^10.2.1",
"@solana/web3.js": "^1.91.8",
"@toruslabs/openlogin-ed25519": "^7.0.0",
"@web3auth/ethereum-provider": "^7.3.2",
"eth-json-rpc-middleware": "^9.0.1",
"ethereumjs-wallet": "^1.0.2",
"json-rpc-engine": "^6.1.0"
"json-rpc-engine": "^6.1.0",
"tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1"
},
"scripts": {}
}
26 changes: 24 additions & 2 deletions packages/wallet-engine/src/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getED25519Key } from '@toruslabs/openlogin-ed25519';
import { decodeAddress, encodeAddress, isEthereumAddress } from '@polkadot/util-crypto';
import { hexToU8a, isHex } from '@polkadot/util';
import { Keyring } from '@polkadot/keyring';
import { Keypair as SolKeypair } from '@solana/web3.js';

import { KeyPair, KeyType, Account } from './types';
import { CERE_SS58_PREFIX } from './constants';
Expand All @@ -29,6 +30,18 @@ const pairFactoryMap: Record<KeyType, (privateKey: string) => KeyPair> = {
address: encodeAddress(publicKey, CERE_SS58_PREFIX),
};
},

solana: (privateKey) => {
const { sk: ed25519Key } = getED25519Key(privateKey);
const { publicKey, secretKey } = SolKeypair.fromSecretKey(ed25519Key);

return {
type: 'solana',
publicKey: publicKey.toBuffer(),
secretKey: Buffer.from(secretKey),
address: publicKey.toBase58(),
};
},
};

export type KeyPairOptions = {
Expand All @@ -45,6 +58,10 @@ export const getKeyPair = ({ privateKey, type }: KeyPairOptions): KeyPair => {
};

export const exportAccountToJson = ({ privateKey, type, passphrase }: KeyPairOptions & { passphrase?: string }) => {
if (type === 'solana') {
throw new Error('Not implemented');
}

const { publicKey, secretKey } = getKeyPair({ type, privateKey });
const keyring = new Keyring({ type });

Expand All @@ -67,5 +84,10 @@ const isValidPolkadotAddress = (address: string) => {
}
};

export const isValidAddress = (address: string, type: KeyType) =>
type === 'ethereum' ? isEthereumAddress(address) : isValidPolkadotAddress(address);
export const isValidAddress = (address: string, type: KeyType) => {
if (type === 'solana') {
throw new Error('Not implemented');
}

return type === 'ethereum' ? isEthereumAddress(address) : isValidPolkadotAddress(address);
};
11 changes: 8 additions & 3 deletions packages/wallet-engine/src/engine/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const createAccountsEngine = ({ getPrivateKey, getAccounts, onUpdateAccou
engine.push(
createScaffoldMiddleware({
wallet_accounts: createAsyncMiddleware(async (req, res) => {
res.result = createAccounts(['ethereum', 'ed25519']);
res.result = createAccounts(['ethereum', 'ed25519', 'solana']);
}),

ed25519_accounts: createAsyncMiddleware(async (req, res) => {
Expand All @@ -33,13 +33,17 @@ export const createAccountsEngine = ({ getPrivateKey, getAccounts, onUpdateAccou
res.result = createAccounts(['ethereum']).map((account) => account.address);
}),

solana_accounts: createAsyncMiddleware(async (req, res) => {
res.result = createAccounts(['solana']).map((account) => account.address);
}),

eth_requestAccounts: createAsyncMiddleware(async (req, res) => {
res.result = createAccounts(['ethereum']).map((account) => account.address);
}),

wallet_updateAccounts: createAsyncMiddleware(async (req, res) => {
const accounts = createAccounts(['ethereum', 'ed25519']);
const [eth, ed255519] = accounts;
const accounts = createAccounts(['ethereum', 'ed25519', 'solana']);
const [eth, ed255519, solana] = accounts;

onUpdateAccounts(accounts);

Expand All @@ -49,6 +53,7 @@ export const createAccountsEngine = ({ getPrivateKey, getAccounts, onUpdateAccou
engine.emit('message', { type: 'wallet_accountsChanged', data: accounts });
engine.emit('message', { type: 'eth_accountChanged', data: eth });
engine.emit('message', { type: 'ed25519_accountChanged', data: ed255519 });
engine.emit('message', { type: 'solana_accountChanged', data: solana });

/**
* Standard eip-1193 event
Expand Down
10 changes: 10 additions & 0 deletions packages/wallet-engine/src/engine/approve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ export const createApproveEngine = ({
});
}),

solana_signMessage: createRequestMiddleware<[string, string]>(async (req, proceed) => {
const [account, message] = req.params!;

await onPersonalSign({
preopenInstanceId: req.preopenInstanceId,
params: [message, account, 'solana' as KeyType],
proceed,
});
}),

eth_sendTransaction: createRequestMiddleware<[IncomingTransaction]>(async (req, proceed) => {
await onSendTransaction({
preopenInstanceId: req.preopenInstanceId,
Expand Down
8 changes: 8 additions & 0 deletions packages/wallet-engine/src/engine/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import { createPermissionsEngine, PermissionsEngineOptions } from './permissions
import type { EthereumEngineOptions } from './ethereum';
import type { PolkadotEngineOptions } from './polkadot';
import type { AccountsEngineOptions } from './accounts';
import type { SolanaEngineOptions } from './solana';

export type ProviderEngineOptions = WalletEngineOptions &
AccountsEngineOptions &
ApproveEngineOptions &
EthereumEngineOptions &
PolkadotEngineOptions &
SolanaEngineOptions &
PermissionsEngineOptions;

class EngineProvider extends EventEmitter implements Provider {
Expand Down Expand Up @@ -53,6 +55,12 @@ class UnsafeEngine extends Engine {
return createPolkadotEngine(options);
});

this.pushEngine(
import(/* webpackChunkName: "accountsEngine" */ './solana').then(({ createSolanaEngine }) =>
createSolanaEngine(options),
),
);

/**
* Should always be the last one since it is currently handles real RPC requests
* TODO: Replace with fetch middleware in future
Expand Down
47 changes: 47 additions & 0 deletions packages/wallet-engine/src/engine/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createAsyncMiddleware, createScaffoldMiddleware } from 'json-rpc-engine';
import { u8aToHex } from '@polkadot/util';
import nacl from 'tweetnacl';
import { decodeUTF8 } from 'tweetnacl-util';

import { Engine } from './engine';
import { getKeyPair } from '../accounts';

export type SolanaEngineOptions = {
getPrivateKey: () => string | undefined;
};

export const createSolanaEngine = ({ getPrivateKey }: SolanaEngineOptions) => {
const engine = new Engine();

const getPair = (address: string) => {
const privateKey = getPrivateKey();

if (!privateKey) {
throw new Error('No private key was provided!');
}

return getKeyPair({ type: 'solana', privateKey });
};

engine.push(
createScaffoldMiddleware({
/**
* Sign a message with solana keypair
* https://solana.com/developers/cookbook/wallets/sign-message
*
* TODO: Rethink the implementation later after the solana blockchain hackathon
*/
solana_signMessage: createAsyncMiddleware(async (req, res) => {
const [address, message] = req.params as string[];
const pair = getPair(address);

const messageBytes = decodeUTF8(message);
const signature = nacl.sign.detached(messageBytes, pair.secretKey);

res.result = u8aToHex(signature);
}),
}),
);

return engine;
};
8 changes: 7 additions & 1 deletion packages/wallet-engine/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ export declare type ChainConfig = {
tickerName: string;
};

export type KeyType = 'ethereum' | 'ed25519';
/**
* TODO: Solana key type was added as a temparary solution.
* Solana uses `ed25519` so it would be better to add another key property eg. `chainNamespace` instead of extending`type`.
*
* For simplification, we can use `type` for now.
*/
export type KeyType = 'ethereum' | 'ed25519' | 'solana';
export type KeyPair = {
type: KeyType;
address: string;
Expand Down
31 changes: 30 additions & 1 deletion playground/Wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const Wallet = () => {
const [ethBalance, setEthBalance] = useState<string>();
const [cereAddress, setCereAddress] = useState<string>();
const [cereBalance, setCereBalance] = useState<string>();
const [solanaAddress, setSolanaAddress] = useState<string>();
const [isNewUser, setIsNewUser] = useState(false);

const wallet = useWallet();
Expand Down Expand Up @@ -41,10 +42,11 @@ export const Wallet = () => {
accounts.map((account) => account.address),
);

const [ethAccount, cereAccount] = accounts;
const [ethAccount, cereAccount, solanaAccount] = accounts;

setCereAddress(cereAccount?.address);
setEthAddress(ethAccount?.address);
setSolanaAddress(solanaAccount?.address);
});

window.addEventListener('focus', () => {
Expand All @@ -60,6 +62,7 @@ export const Wallet = () => {
permissions: {
personal_sign: {},
ed25519_signRaw: {},
solana_signMessage: {},
},
},

Expand Down Expand Up @@ -219,6 +222,16 @@ export const Wallet = () => {
console.log(`Signed message: ${signed}`);
}, [wallet]);

const handleSolanaSign = useCallback(async () => {
const [, , solanaAccount] = await wallet.getAccounts();
const signed = await wallet.provider.request({
method: 'solana_signMessage',
params: [solanaAccount.address, 'Hello!!!'],
});

console.log(`Signed message: ${signed}`);
}, [wallet]);

const handleGetAccounts = useCallback(async () => {
const accounts = await wallet.getAccounts();

Expand Down Expand Up @@ -264,6 +277,7 @@ export const Wallet = () => {
const permissions = await wallet.requestPermissions({
personal_sign: {},
ed25519_signRaw: {},
solana_signMessage: {},
});

console.log('Approved permissions', permissions);
Expand Down Expand Up @@ -308,6 +322,17 @@ export const Wallet = () => {
</Stack>
)}

{solanaAddress && (
<Stack spacing={1} alignItems="center">
<Typography width={150} fontWeight="bold" align="center">
Solana Address
</Typography>
<Typography variant="body2" align="center">
{solanaAddress}
</Typography>
</Stack>
)}

{ethBalance && (
<Stack spacing={1} alignItems="center">
<Typography width={200} fontWeight="bold" align="center">
Expand Down Expand Up @@ -374,6 +399,10 @@ export const Wallet = () => {
Sign payload (ed25519)
</Button>

<Button variant="outlined" color="primary" disabled={status === 'disconnecting'} onClick={handleSolanaSign}>
Sign message (solana)
</Button>

<Button variant="outlined" color="primary" disabled={status === 'disconnecting'} onClick={handleShowWallet}>
Show wallet
</Button>
Expand Down
16 changes: 14 additions & 2 deletions src/components/AddressDropdown/AddressDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import { CoinIcon } from '../CoinIcon';

export type AddressDropdownProps = Pick<UIAddressDropdownProps, 'variant' | 'size' | 'maxLength'>;

const labelByType = {
ethereum: 'Polygon',
ed25519: 'Cere Network',
solana: 'Solana',
};

const iconByType = {
ethereum: 'matic',
ed25519: 'cere',
solana: 'solana',
};

const AddressDropdown = (props: AddressDropdownProps) => {
const store = useAccountStore();
const { selectedAccount, accounts } = store;
Expand All @@ -19,8 +31,8 @@ const AddressDropdown = (props: AddressDropdownProps) => {
() =>
accounts.map((account) => ({
address: account.address,
label: account.type === 'ethereum' ? 'Polygon' : 'Cere Network',
icon: account.type === 'ethereum' ? <CoinIcon coin="matic" /> : <CoinIcon coin="cere" />,
label: labelByType[account.type],
icon: <CoinIcon coin={iconByType[account.type]} />,
})),
[accounts],
);
Expand Down
3 changes: 2 additions & 1 deletion src/components/CoinIcon/coinIconsMap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CereIcon, MaticIcon, UsdcIcon, EthIcon, UsdtIcon } from '@cere-wallet/ui';
import { CereIcon, MaticIcon, UsdcIcon, EthIcon, UsdtIcon, SolanaIcon } from '@cere-wallet/ui';
import { ComponentType } from 'react';

export const coinIconsMap: Record<string, ComponentType> = {
Expand All @@ -7,4 +7,5 @@ export const coinIconsMap: Record<string, ComponentType> = {
matic: MaticIcon,
usdc: UsdcIcon,
usdt: UsdtIcon,
solana: SolanaIcon,
};
Loading

0 comments on commit e342859

Please sign in to comment.