diff --git a/.gitignore b/.gitignore index b512c09..e63ff76 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,20 @@ -node_modules \ No newline at end of file +# Debugging +npm-debug.* +yarn-debug.* +yarn-error.* +.eslintcache + +# Dependencies +node_modules + +# MacOS +.DS_Store + +# Production +build +build.zip +dist + +# VSCode +.vscode/ +workspace* diff --git a/.prettierrc b/.prettierrc index 7197da8..2a46d49 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,6 @@ { - "singleQuote": false, + "printWidth": 120, + "semi": true, + "singleQuote": true, "trailingComma": "es5" } diff --git a/README.md b/README.md index 7fdc766..87a049c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ -# sandbox -Created with CodeSandbox -https://r3byv.csb.app/ +# Phantom Wallet Sandbox + +> A CodeSandbox for learning how to interact with Phantom Wallet + +(https://r3byv.csb.app/)[Play with the sandbox in full view] diff --git a/package.json b/package.json index 4081f94..bc9c938 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { - "name": "react-typescript", + "name": "@phantom-labs/sandbox", "version": "1.0.0", - "description": "React and TypeScript example starter project", + "description": "The official CodeSandbox for learning how to interact with Phantom Wallet.", + "license": "MIT", "keywords": [ - "typescript", - "react", - "starter" + "phantom", + "phantom wallet", + "phantom-wallet", + "solana", + "codesandbox" ], "main": "src/index.tsx", "dependencies": { @@ -22,8 +25,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test --env=jsdom", - "eject": "react-scripts eject" + "test": "react-scripts test --env=jsdom" }, "browserslist": [ ">0.2%", diff --git a/public/index.html b/public/index.html index 42ae2d2..ba10eab 100644 --- a/public/index.html +++ b/public/index.html @@ -1,43 +1,17 @@ - - - - React App + Phantom Wallet – CodeSandbox -
- - \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index fcf54cd..8fd00f8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,97 +1,70 @@ -import { useState, useEffect, useCallback } from "react"; -import { - Connection, - PublicKey, - Transaction, - SystemProgram, - SendOptions, -} from "@solana/web3.js"; -import "./styles.css"; - -type DisplayEncoding = "utf8" | "hex"; -type PhantomEvent = "disconnect" | "connect" | "accountChanged"; -type PhantomRequestMethod = - | "connect" - | "disconnect" - | "signTransaction" - | "signAllTransactions" - | "signMessage"; - -interface ConnectOpts { - onlyIfTrusted: boolean; -} +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { Connection, PublicKey } from '@solana/web3.js'; -interface PhantomProvider { - publicKey: PublicKey | null; - isConnected: boolean | null; - signAndSendTransaction: ( - transaction: Transaction, - opts?: SendOptions - ) => Promise<{ signature: string; publicKey: PublicKey }>; - signTransaction: (transaction: Transaction) => Promise; - signAllTransactions: (transactions: Transaction[]) => Promise; - signMessage: ( - message: Uint8Array | string, - display?: DisplayEncoding - ) => Promise; - connect: (opts?: Partial) => Promise<{ publicKey: PublicKey }>; - disconnect: () => Promise; - on: (event: PhantomEvent, handler: (args: any) => void) => void; - request: (method: PhantomRequestMethod, params: any) => Promise; -} +import './styles.css'; -const getProvider = (): PhantomProvider | undefined => { - if ("solana" in window) { - const anyWindow: any = window; - const provider = anyWindow.solana; - if (provider.isPhantom) { - return provider; - } - } - window.open("https://phantom.app/", "_blank"); -}; +import { + getProvider, + signAllTransactions, + signAndSendTransaction, + signMessage, + signTransaction, + createTransferTransaction, + pollSignatureStatus, +} from './utils'; + +// ============================================================================= +// Constants +// ============================================================================= // alternatively, use clusterApiUrl("mainnet-beta"); -const NETWORK = "https://solana-api.projectserum.com"; +export const NETWORK = 'https://solana-api.projectserum.com'; +const provider = getProvider(); +const connection = new Connection(NETWORK); +const message = 'To avoid digital dognappers, sign below to authenticate with CryptoCorgis.'; + +// ============================================================================= +// Main Component +// ============================================================================= -export default function App() { +const App = () => { const [, setConnected] = useState(false); const [publicKey, setPublicKey] = useState(null); const [logs, setLogs] = useState([]); + const addLog = useCallback( - (log: string) => setLogs((logs) => [...logs, "> " + log]), - [] + (log: string) => { + return setLogs((logs) => [...logs, '> ' + log]); + }, + [logs] ); - const provider = getProvider(); - const connection = new Connection(NETWORK); - useEffect(() => { if (!provider) return; // try to eagerly connect - provider.connect({ onlyIfTrusted: true }).catch((err) => { + provider.connect({ onlyIfTrusted: true }).catch((error) => { // fail silently }); - provider.on("connect", (publicKey: PublicKey) => { + provider.on('connect', (publicKey: PublicKey) => { setPublicKey(publicKey); setConnected(true); - addLog("[connect] " + publicKey?.toBase58()); + addLog(`[connect] ${publicKey?.toBase58()}`); }); - provider.on("disconnect", () => { + provider.on('disconnect', () => { setPublicKey(null); setConnected(false); - addLog("[disconnect] 👋"); + addLog('[disconnect] 👋'); }); - provider.on("accountChanged", (publicKey: PublicKey | null) => { + provider.on('accountChanged', (publicKey: PublicKey | null) => { setPublicKey(publicKey); if (publicKey) { - addLog("[accountChanged] Switched account to " + publicKey?.toBase58()); + addLog(`[accountChanged] Switched account to ${publicKey?.toBase58()}`); } else { - addLog("[accountChanged] Switched unknown account"); + addLog('[accountChanged] Switched unknown account'); // In this case, dapps could not to anything, or, // Only re-connecting to the new account if it is trusted // provider.connect({ onlyIfTrusted: true }).catch((err) => { @@ -100,9 +73,9 @@ export default function App() { // Or, always trying to reconnect provider .connect() - .then(() => addLog("[accountChanged] Reconnected successfully")) - .catch((err) => { - addLog("[accountChanged] Failed to re-connect: " + err.message); + .then(() => addLog('[accountChanged] Reconnected successfully')) + .catch((error) => { + addLog(`[accountChanged] Failed to re-connect: ${error.message}`); }); } }); @@ -110,114 +83,117 @@ export default function App() { return () => { provider.disconnect(); }; - }, [provider, addLog]); + }, [provider]); if (!provider) { return

Could not find a provider

; } - const createTransferTransaction = async () => { - if (!provider.publicKey) return; - let transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: provider.publicKey, - toPubkey: provider.publicKey, - lamports: 100, - }) - ); - transaction.feePayer = provider.publicKey; - addLog("Getting latest blockhash"); - const anyTransaction: any = transaction; - anyTransaction.recentBlockhash = ( - await connection.getLatestBlockhash() - ).blockhash; - return transaction; - }; - - // A simple helper function used to space out our signature polling - const pause = (ms: number) => new Promise((res) => setTimeout(res, ms)); + /** SignAndSendTransaction */ + const handleSignAndSendTransaction = useCallback(async () => { + try { + const transaction = await createTransferTransaction(provider, connection); + addLog(`Requesting signature for: ${JSON.stringify(transaction)}`); + const signature = await signAndSendTransaction(provider, transaction); + addLog(`Signed and submitted transaction ${signature}, awaiting confirmation...`); + pollSignatureStatus(signature, connection, addLog); + } catch (error) { + console.warn(error); + addLog(`[error] signAndSendTransaction: ${JSON.stringify(error)}`); + } + }, [provider, connection, addLog]); - const pollSignatureStatus = async (signature: string) => { - const maxPolls = 10; - for (let pollCount = 0; pollCount < maxPolls; pollCount++) { - const { value } = await connection.getSignatureStatus(signature); - if (value?.confirmationStatus) { - addLog(`Transaction ${signature} ${value.confirmationStatus}`); - if ( - value.confirmationStatus === "confirmed" || - value.confirmationStatus === "finalized" - ) - return; - } - await pause(1000); + /** SignTransaction */ + const handleSignTransaction = useCallback(async () => { + try { + const transaction = await createTransferTransaction(provider, connection); + addLog(`Requesting signature for: ${JSON.stringify(transaction)}`); + const signedTransaction = await signTransaction(provider, transaction); + addLog(`Transaction signed: ${JSON.stringify(signedTransaction)}`); + } catch (error) { + console.warn(error); + addLog(`[error] signTransaction: ${JSON.stringify(error)}`); } - addLog(`Failed to confirm transaction ${signature}`); - }; + }, [provider, connection, addLog]); - const signAndSendTransaction = async () => { + /** SignAllTransactions */ + const handleSignAllTransactions = useCallback(async () => { try { - const transaction = await createTransferTransaction(); - if (!transaction) return; - addLog("Requesting signature for: " + JSON.stringify(transaction)); - const { signature } = await provider.signAndSendTransaction(transaction); - addLog( - "Signed and submitted transaction " + - signature + - ", awaiting confirmation..." - ); - pollSignatureStatus(signature); - } catch (err) { - console.warn(err); - addLog("[error] signAndSendTransaction: " + JSON.stringify(err)); + const transactions = [ + await createTransferTransaction(provider, connection), + await createTransferTransaction(provider, connection), + ]; + addLog(`Requesting signature for: ${JSON.stringify(transactions)}`); + const signedTransactions = await signAllTransactions(provider, transactions[0], transactions[1]); + addLog(`Transactions signed: ${JSON.stringify(signedTransactions)}`); + } catch (error) { + addLog(`[error] signAllTransactions: ${JSON.stringify(error)}`); } - }; + }, [provider, connection, addLog]); - const signTransaction = async () => { + /** SignMessage */ + const handleSignMessage = useCallback(async () => { try { - const transaction = await createTransferTransaction(); - if (!transaction) return; - addLog("Requesting signature for: " + JSON.stringify(transaction)); - const signedTransaction = await provider.signTransaction(transaction); - addLog("Transaction signed: " + JSON.stringify(signedTransaction)); - } catch (err) { - console.warn(err); - addLog("[error] signTransaction: " + JSON.stringify(err)); + const signedMessage = await signMessage(provider, message); + addLog(`Message signed: ${JSON.stringify(signedMessage)}`); + return signedMessage; + } catch (error) { + console.warn(error); + addLog(`[error] signMessage: ${JSON.stringify(error)}`); } - }; + }, [provider, message, addLog]); - const signAllTransactions = async () => { + /** Connect */ + const handleConnect = useCallback(async () => { try { - const [transaction1, transaction2] = await Promise.all([ - createTransferTransaction(), - createTransferTransaction(), - ]); - if (transaction1 && transaction2) { - addLog( - "Requesting signature for: " + - JSON.stringify([transaction1, transaction2]) - ); - const transactions = await provider.signAllTransactions([ - transaction1, - transaction2, - ]); - addLog("Transactions signed: " + JSON.stringify(transactions)); - } - } catch (err) { - console.warn(err); - addLog("[error] signAllTransactions: " + JSON.stringify(err)); + await provider.connect(); + } catch (error) { + console.warn(error); + addLog(`[error] connect: ${JSON.stringify(error)}`); } - }; + }, [provider]); - const signMessage = async (message: string) => { + /** Disconnect */ + const handleDisconnect = useCallback(async () => { try { - const data = new TextEncoder().encode(message); - const res = await provider.signMessage(data); - addLog("Message signed: " + JSON.stringify(res)); - } catch (err) { - console.warn(err); - addLog("[error] signMessage: " + JSON.stringify(err)); + await provider.disconnect(); + } catch (error) { + console.warn(error); + addLog(`[error] disconnect: ${JSON.stringify(error)}`); } - }; + }, [provider]); + + const methods = useMemo( + () => [ + { + name: 'Sign and Send Transaction', + onClick: handleSignAndSendTransaction, + }, + { + name: 'Sign Transaction', + onClick: handleSignTransaction, + }, + { + name: 'Sign All Transactions', + onClick: handleSignAllTransactions, + }, + { + name: 'Sign Message', + onClick: handleSignMessage, + }, + { + name: 'Disconnect', + onClick: handleDisconnect, + }, + ], + [ + handleSignAndSendTransaction, + handleSignTransaction, + handleSignAllTransactions, + handleSignMessage, + handleDisconnect, + ] + ); return (
@@ -231,57 +207,25 @@ export default function App() {
{publicKey.toBase58()}

- - - - - + {methods.map((method, i) => ( + + ))} ) : ( - <> - - + )}
{logs.map((log, i) => ( -
+
{log}
))}
); -} +}; + +export default App; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..f56d3ff --- /dev/null +++ b/src/types.ts @@ -0,0 +1,35 @@ +import { PublicKey, Transaction, SendOptions } from "@solana/web3.js"; + +export type DisplayEncoding = "utf8" | "hex"; + +export type PhantomEvent = "connect" | "disconnect" | "accountChanged"; + +export type PhantomRequestMethod = + | "connect" + | "disconnect" + | "signTransaction" + | "signAllTransactions" + | "signMessage"; + +export interface ConnectOpts { + onlyIfTrusted: boolean; +} + +export interface PhantomProvider { + publicKey: PublicKey | null; + isConnected: boolean | null; + signAndSendTransaction: ( + transaction: Transaction, + opts?: SendOptions + ) => Promise<{ signature: string; publicKey: PublicKey }>; + signTransaction: (transaction: Transaction) => Promise; + signAllTransactions: (transactions: Transaction[]) => Promise; + signMessage: ( + message: Uint8Array | string, + display?: DisplayEncoding + ) => Promise; + connect: (opts?: Partial) => Promise<{ publicKey: PublicKey }>; + disconnect: () => Promise; + on: (event: PhantomEvent, handler: (args: any) => void) => void; + request: (method: PhantomRequestMethod, params: any) => Promise; +} diff --git a/src/utils/createTransferTransaction.ts b/src/utils/createTransferTransaction.ts new file mode 100644 index 0000000..11dccb3 --- /dev/null +++ b/src/utils/createTransferTransaction.ts @@ -0,0 +1,22 @@ +import { Transaction, SystemProgram, Connection } from '@solana/web3.js'; +import { PhantomProvider } from '../types'; + +const createTransferTransaction = async (provider: PhantomProvider, connection: Connection) => { + if (!provider.publicKey) return; + + const transaction = new Transaction().add( + SystemProgram.transfer({ + fromPubkey: provider.publicKey, + toPubkey: provider.publicKey, + lamports: 100, + }) + ); + transaction.feePayer = provider.publicKey; + + const anyTransaction: any = transaction; + anyTransaction.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + + return transaction; +}; + +export default createTransferTransaction; diff --git a/src/utils/getProvider.ts b/src/utils/getProvider.ts new file mode 100644 index 0000000..f19f4e1 --- /dev/null +++ b/src/utils/getProvider.ts @@ -0,0 +1,16 @@ +import { PhantomProvider } from "../types"; + +const getProvider = (): PhantomProvider | undefined => { + if ("solana" in window) { + const anyWindow: any = window; + const provider = anyWindow.solana; + + if (provider.isPhantom) { + return provider; + } + } + + window.open("https://phantom.app/", "_blank"); +}; + +export default getProvider; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..41d567d --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,8 @@ +export { default as createTransferTransaction } from './createTransferTransaction'; +export { default as getProvider } from './getProvider'; +export { default as pause } from './pause'; +export { default as pollSignatureStatus } from './pollSignatureStatus'; +export { default as signAllTransactions } from './signAllTransactions'; +export { default as signAndSendTransaction } from './signAndSendTransaction'; +export { default as signMessage } from './signMessage'; +export { default as signTransaction } from './signTransaction'; diff --git a/src/utils/pause.ts b/src/utils/pause.ts new file mode 100644 index 0000000..8e74f13 --- /dev/null +++ b/src/utils/pause.ts @@ -0,0 +1,12 @@ +/** + * A simple helper function used to space out our signature polling + * @param {Number} milliseconds an amount of time in milliseconds + * @returns + */ +const pause = (milliseconds: number): Promise => { + return new Promise((res) => { + setTimeout(res, milliseconds); + }); +}; + +export default pause; diff --git a/src/utils/pollSignatureStatus.ts b/src/utils/pollSignatureStatus.ts new file mode 100644 index 0000000..92a2559 --- /dev/null +++ b/src/utils/pollSignatureStatus.ts @@ -0,0 +1,20 @@ +import { Connection } from '@solana/web3.js'; + +import { pause } from '.'; + +const MAX_POLLS = 10; + +const pollSignatureStatus = async (signature: string, connection: Connection, addLog: (log: string) => void) => { + for (let pollCount = 0; pollCount < MAX_POLLS; pollCount++) { + const { value } = await connection.getSignatureStatus(signature); + + if (value?.confirmationStatus) { + addLog(`Transaction ${signature} ${value.confirmationStatus}`); + if (value.confirmationStatus === 'confirmed' || value.confirmationStatus === 'finalized') return; + } + + await pause(1000); + } +}; + +export default pollSignatureStatus; diff --git a/src/utils/signAllTransactions.ts b/src/utils/signAllTransactions.ts new file mode 100644 index 0000000..93a91c7 --- /dev/null +++ b/src/utils/signAllTransactions.ts @@ -0,0 +1,14 @@ +import { Transaction } from '@solana/web3.js'; + +import { PhantomProvider } from '../types'; + +const signAllTransactions = async (provider: PhantomProvider, transaction1: Transaction, transaction2: Transaction) => { + try { + const transactions = await provider.signAllTransactions([transaction1, transaction2]); + return transactions; + } catch (err) { + console.warn(err); + } +}; + +export default signAllTransactions; diff --git a/src/utils/signAndSendTransaction.ts b/src/utils/signAndSendTransaction.ts new file mode 100644 index 0000000..03e4558 --- /dev/null +++ b/src/utils/signAndSendTransaction.ts @@ -0,0 +1,20 @@ +import { Transaction } from '@solana/web3.js'; + +import { PhantomProvider } from '../types'; + +/** + * Signs a transaction + * @param {PhantomProvider} provider a Phantom Provider + * @param {Transaction} transaction a transaction to sign + * @returns {Transaction} a signed transaction + */ +const signAndSendTransaction = async (provider: PhantomProvider, transaction: Transaction): Promise => { + try { + const { signature } = await provider.signAndSendTransaction(transaction); + return signature; + } catch (err) { + console.warn(err); + } +}; + +export default signAndSendTransaction; diff --git a/src/utils/signMessage.ts b/src/utils/signMessage.ts new file mode 100644 index 0000000..039a516 --- /dev/null +++ b/src/utils/signMessage.ts @@ -0,0 +1,19 @@ +import { PhantomProvider } from '../types'; + +/** + * Signs a message + * @param {PhantomProvider} provider a Phantom Provider + * @param {String} message a message to sign + * @returns {Any} TODO(get type) + */ +const signMessage = async (provider: PhantomProvider, message: string): Promise => { + try { + const encodedMessage = new TextEncoder().encode(message); + const signedMessage = await provider.signMessage(encodedMessage); + return signedMessage; + } catch (error) { + console.warn(error); + } +}; + +export default signMessage; diff --git a/src/utils/signTransaction.ts b/src/utils/signTransaction.ts new file mode 100644 index 0000000..f6e1759 --- /dev/null +++ b/src/utils/signTransaction.ts @@ -0,0 +1,20 @@ +import { Transaction } from '@solana/web3.js'; + +import { PhantomProvider } from '../types'; + +/** + * Signs a transaction + * @param {PhantomProvider} provider a Phantom Provider + * @param {Transaction} transaction a transaction to sign + * @returns {Transaction} a signed transaction + */ +const signTransaction = async (provider: PhantomProvider, transaction: Transaction): Promise => { + try { + const signedTransaction = await provider.signTransaction(transaction); + return signedTransaction; + } catch (error) { + console.warn(error); + } +}; + +export default signTransaction; diff --git a/tsconfig.json b/tsconfig.json index d84f240..dd9c3a5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,9 @@ "include": [ "./src/**/*" ], + "exclude": [ + "node_modules" + ], "compilerOptions": { "strict": false, "esModuleInterop": true,