Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for solana wallet address #197

Merged
merged 5 commits into from
May 29, 2024
Merged
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
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
Loading