-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: deterministic gen stealth meta address (#60)
* fix: valid compressed key input type clarity * fix: comment and variable clarity * feat: get stealth meta address from keys * feat: is valid pub key * feat: is valid key tests * feat: gen keys from sig * chore: default export * chore: fix import * fix: test * feat: extract portions into own func for testing * feat: get stealth meta address from signature * feat: test * feat: example for gen stealth meta address from sig * chore: lint * fix: comment * feat: change get to generate (verbiage) * fix: generate verbiage * chore: clean * Update examples/generateDeterministicStealthMetaAddress/README.md Co-authored-by: Gary Ghayrat <[email protected]> * feat: send and receive test (#62) * feat: handle just passing the stealth meta-address directly * chore: format * chore: format * feat: send and receive test * chore: format * chore: comment * chore: comment * chore: clean * chore: clean * fix: just wait for the receipt --------- Co-authored-by: Gary Ghayrat <[email protected]>
- Loading branch information
1 parent
b6d866d
commit c1f8df2
Showing
21 changed files
with
769 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
VITE_RPC_URL='Your rpc url' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# Generate Deterministic Stealth Meta-Address Example |
Binary file not shown.
12 changes: 12 additions & 0 deletions
12
examples/generateDeterministicStealthMetaAddress/index.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
</head> | ||
<body> | ||
<h1>Generate Deterministic Stealth Meta-address Example</h1> | ||
<div id="root"></div> | ||
<script type="module" src="/index.tsx"></script> | ||
</body> | ||
</html> |
81 changes: 81 additions & 0 deletions
81
examples/generateDeterministicStealthMetaAddress/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React, { useState } from "react"; | ||
import ReactDOM from "react-dom/client"; | ||
import { Address, createWalletClient, custom } from "viem"; | ||
import { sepolia } from "viem/chains"; | ||
import "viem/window"; | ||
|
||
import { generateStealthMetaAddressFromSignature } from "@scopelift/stealth-address-sdk"; | ||
|
||
/** | ||
* This React component demonstrates the process of generating a stealth meta-address deterministically using a user-signed message | ||
* It's deterministic in that the same stealth meta-address is generated for the same user, chain id, and message | ||
* It utilizes Viem's walletClient for wallet interaction | ||
* | ||
* @returns The component renders a button to first handle connecting the wallet, and a subsequent button to handle stealth meta-address generation | ||
* | ||
* @example | ||
* To run the development server: `bun run dev`. | ||
*/ | ||
const Example = () => { | ||
// Initialize your configuration | ||
const chain = sepolia; // Example Viem chain | ||
|
||
// Initialize Viem wallet client if using Viem | ||
const walletClient = createWalletClient({ | ||
chain, | ||
transport: custom(window.ethereum!), | ||
}); | ||
|
||
// State | ||
const [account, setAccount] = useState<Address>(); | ||
const [stealthMetaAddress, setStealthMetaAddress] = useState<`0x${string}`>(); | ||
|
||
const connect = async () => { | ||
const [address] = await walletClient.requestAddresses(); | ||
setAccount(address); | ||
}; | ||
|
||
const signMessage = async () => { | ||
// An example message to sign for generating the stealth meta-address | ||
// Usually this message includes the chain id to mitigate replay attacks across different chains | ||
// The message that is signed should clearly communicate to the user what they are signing and why | ||
const MESSAGE_TO_SIGN = `Generate Stealth Meta-Address on ${chain.id} chain`; | ||
|
||
if (!account) throw new Error("A connected account is required"); | ||
|
||
const signature = await walletClient.signMessage({ | ||
account, | ||
message: MESSAGE_TO_SIGN, | ||
}); | ||
|
||
return signature; | ||
}; | ||
|
||
const handleSignAndGenStealthMetaAddress = async () => { | ||
const signature = await signMessage(); | ||
const stealthMetaAddress = | ||
generateStealthMetaAddressFromSignature(signature); | ||
|
||
setStealthMetaAddress(stealthMetaAddress); | ||
}; | ||
|
||
if (account) | ||
return ( | ||
<> | ||
{!stealthMetaAddress ? ( | ||
<button onClick={handleSignAndGenStealthMetaAddress}> | ||
Generate Stealth Meta-Address | ||
</button> | ||
) : ( | ||
<div>Stealth Meta-Address: {stealthMetaAddress}</div> | ||
)} | ||
<div>Connected: {account}</div> | ||
</> | ||
); | ||
|
||
return <button onClick={connect}>Connect Wallet</button>; | ||
}; | ||
|
||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( | ||
<Example /> | ||
); |
21 changes: 21 additions & 0 deletions
21
examples/generateDeterministicStealthMetaAddress/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"name": "example-generate-deterministic-stealth-meta-address", | ||
"private": true, | ||
"type": "module", | ||
"scripts": { | ||
"dev": "vite" | ||
}, | ||
"dependencies": { | ||
"@types/react": "^18.2.61", | ||
"@types/react-dom": "^18.2.19", | ||
"@vitejs/plugin-react": "^4.2.1", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"@scopelift/stealth-address-sdk": "latest", | ||
"viem": "latest", | ||
"vite": "latest" | ||
}, | ||
"devDependencies": { | ||
"typescript": "^5.3.3" | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
examples/generateDeterministicStealthMetaAddress/tsconfig.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "ESNext", | ||
"module": "ESNext", | ||
"lib": ["ESNext", "DOM", "DOM.Iterable"], | ||
"moduleResolution": "Node", | ||
"strict": true, | ||
"esModuleInterop": true, | ||
"noEmit": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noImplicitReturns": true, | ||
"skipLibCheck": true, | ||
"jsx": "react", | ||
}, | ||
"include": ["."], | ||
} |
7 changes: 7 additions & 0 deletions
7
examples/generateDeterministicStealthMetaAddress/vite.config.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import react from '@vitejs/plugin-react'; | ||
import { defineConfig } from 'vite'; | ||
|
||
// https://vitejs.dev/config/ | ||
export default defineConfig({ | ||
plugins: [react()] | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import { | ||
http, | ||
type Address, | ||
type Client, | ||
type WalletClient, | ||
createWalletClient, | ||
publicActions, | ||
} from "viem"; | ||
import { privateKeyToAccount } from "viem/accounts"; | ||
import { getRpcUrl } from "../../lib/helpers/test/setupTestEnv"; | ||
import setupTestWallet from "../../lib/helpers/test/setupTestWallet"; | ||
import { type SuperWalletClient, VALID_CHAINS } from "../../lib/helpers/types"; | ||
import { generateKeysFromSignature } from "../../utils/helpers"; | ||
|
||
// Default private key for testing; the setupTestWallet function uses the first anvil default key, so the below will be different | ||
const ANVIL_DEFAULT_PRIVATE_KEY_2 = | ||
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; | ||
|
||
/* Gets the signature to be able to generate reproducible public/private viewing/spending keys */ | ||
export const getSignature = async ({ | ||
walletClient, | ||
}: { | ||
walletClient: WalletClient; | ||
}) => { | ||
if (!walletClient.chain) throw new Error("Chain not found"); | ||
if (!walletClient.account) throw new Error("Account not found"); | ||
|
||
const MESSAGE = `Signing message for stealth transaction on chain id: ${walletClient.chain.id}`; | ||
const signature = await walletClient.signMessage({ | ||
message: MESSAGE, | ||
account: walletClient.account, | ||
}); | ||
|
||
return signature; | ||
}; | ||
|
||
/* Generates the public/private viewing/spending keys from the signature */ | ||
export const getKeys = async ({ | ||
walletClient, | ||
}: { | ||
walletClient: WalletClient; | ||
}) => { | ||
const signature = await getSignature({ walletClient }); | ||
const keys = generateKeysFromSignature(signature); | ||
return keys; | ||
}; | ||
|
||
/* Sets up the sending and receiving wallet clients for testing */ | ||
export const getWalletClients = async () => { | ||
const sendingWalletClient = await setupTestWallet(); | ||
|
||
const chain = sendingWalletClient.chain; | ||
if (!chain) throw new Error("Chain not found"); | ||
if (!(chain.id in VALID_CHAINS)) { | ||
throw new Error("Invalid chain"); | ||
} | ||
|
||
const rpcUrl = getRpcUrl(); | ||
|
||
const receivingWalletClient: SuperWalletClient = createWalletClient({ | ||
account: privateKeyToAccount(ANVIL_DEFAULT_PRIVATE_KEY_2), | ||
chain, | ||
transport: http(rpcUrl), | ||
}).extend(publicActions); | ||
|
||
return { sendingWalletClient, receivingWalletClient }; | ||
}; | ||
|
||
export const getAccount = (walletClient: WalletClient | Client) => { | ||
if (!walletClient.account) throw new Error("Account not found"); | ||
return walletClient.account; | ||
}; | ||
|
||
/* Gets the wallet clients, accounts, and keys for the sending and receiving wallets */ | ||
export const getWalletClientsAndKeys = async () => { | ||
const { sendingWalletClient, receivingWalletClient } = | ||
await getWalletClients(); | ||
|
||
const sendingAccount = getAccount(sendingWalletClient); | ||
const receivingAccount = getAccount(receivingWalletClient); | ||
|
||
const receivingAccountKeys = await getKeys({ | ||
walletClient: receivingWalletClient, | ||
}); | ||
|
||
return { | ||
sendingWalletClient, | ||
receivingWalletClient, | ||
sendingAccount, | ||
receivingAccount, | ||
receivingAccountKeys, | ||
}; | ||
}; | ||
|
||
/* Set up the initial balance details for the sending and receiving wallets */ | ||
export const setupInitialBalances = async ({ | ||
sendingWalletClient, | ||
receivingWalletClient, | ||
}: { | ||
sendingWalletClient: SuperWalletClient; | ||
receivingWalletClient: SuperWalletClient; | ||
}) => { | ||
const sendingAccount = getAccount(sendingWalletClient); | ||
const receivingAccount = getAccount(receivingWalletClient); | ||
const sendingWalletStartingBalance = await sendingWalletClient.getBalance({ | ||
address: sendingAccount.address, | ||
}); | ||
const receivingWalletStartingBalance = await receivingWalletClient.getBalance( | ||
{ | ||
address: receivingAccount.address, | ||
} | ||
); | ||
|
||
return { | ||
sendingWalletStartingBalance, | ||
receivingWalletStartingBalance, | ||
}; | ||
}; | ||
|
||
/* Send ETH and wait for the transaction to be confirmed */ | ||
export const sendEth = async ({ | ||
sendingWalletClient, | ||
to, | ||
value, | ||
}: { | ||
sendingWalletClient: SuperWalletClient; | ||
to: Address; | ||
value: bigint; | ||
}) => { | ||
const account = getAccount(sendingWalletClient); | ||
const hash = await sendingWalletClient.sendTransaction({ | ||
value, | ||
to, | ||
account, | ||
chain: sendingWalletClient.chain, | ||
}); | ||
|
||
const receipt = await sendingWalletClient.waitForTransactionReceipt({ hash }); | ||
|
||
const gasPriceSend = receipt.effectiveGasPrice; | ||
const gasEstimate = receipt.gasUsed * gasPriceSend; | ||
|
||
return { hash, gasEstimate }; | ||
}; | ||
|
||
/* Get the ending balances for the sending and receiving wallets */ | ||
export const getEndingBalances = async ({ | ||
sendingWalletClient, | ||
receivingWalletClient, | ||
}: { | ||
sendingWalletClient: SuperWalletClient; | ||
receivingWalletClient: SuperWalletClient; | ||
}) => { | ||
const sendingAccount = getAccount(sendingWalletClient); | ||
const receivingAccount = getAccount(receivingWalletClient); | ||
const sendingWalletEndingBalance = await sendingWalletClient.getBalance({ | ||
address: sendingAccount.address, | ||
}); | ||
const receivingWalletEndingBalance = await receivingWalletClient.getBalance({ | ||
address: receivingAccount.address, | ||
}); | ||
|
||
return { | ||
sendingWalletEndingBalance, | ||
receivingWalletEndingBalance, | ||
}; | ||
}; |
Oops, something went wrong.