diff --git a/pages/dev-advanced-concepts/_meta.json b/pages/dev-advanced-concepts/_meta.json index 8ba6b5f8..d7c94052 100644 --- a/pages/dev-advanced-concepts/_meta.json +++ b/pages/dev-advanced-concepts/_meta.json @@ -1,11 +1,12 @@ { - "fee-grants": "Fee Grants", - "account-structure": "Account Structure", - "hardware-wallets": "Hardware Wallets", - "oracles": "Oracles", - "execute-multiple": "Execute Multiple Transactions", - "hd-path-coin-types": "HD Path & Coin Types", - "proposals": "Proposals", - "ibc-relayer": "IBC Relayer", - "differences-with-ethereum": "Differences from Ethereum" + "fee-grants": "Fee Grants", + "account-structure": "Account Structure", + "wallet-association": "Wallet Association", + "hardware-wallets": "Hardware Wallets", + "oracles": "Oracles", + "execute-multiple": "Execute Multiple Transactions", + "hd-path-coin-types": "HD Path & Coin Types", + "proposals": "Proposals", + "ibc-relayer": "IBC Relayer", + "differences-with-ethereum": "Differences from Ethereum" } diff --git a/pages/dev-advanced-concepts/account-structure.mdx b/pages/dev-advanced-concepts/account-structure.mdx index e09d4ef7..606f48eb 100644 --- a/pages/dev-advanced-concepts/account-structure.mdx +++ b/pages/dev-advanced-concepts/account-structure.mdx @@ -1,27 +1,208 @@ -# Account Structure +# **Sei Account Structure and Address Linking** -Both EVM derived (0x) and Sei bech32 derived addresses on Sei are derived from the same public key. Accounts are automatically linked once a transaction is broadcasted and the chain has their public key. This ensures that balances and transactions are reflected consistently across both address formats. +On Sei, accounts are represented by two address formats: -### How It Works +- **Sei native Bech32** (`sei...`) +- **EVM-compatible Hex** (`0x...`) -**Auto-linking**: +Both addresses for a single account are derived from the same **public key**, but the chain can only determine their association **after the public key is known**. -- When an account broadcasts a transaction, the public key is recorded on-chain. This allows for the automatic linking of EVM and Sei addresses. Balances will be reflected in both address formats once linked. +--- -**Manual Association**: +## **How Accounts Work on Sei** -- If the chain does not yet have the public key, users can manually associate their accounts using the `sei_associate` function. This is a gasless function that requires at least 1 wei in their account to execute. More details can be found in the [EVM RPC Endpoints documentation](../endpoints/evm.mdx#sei_associate). +### **Automatic Linking** -### Key Points +- When an account **broadcasts a transaction** (e.g., sending tokens), its public key is recorded on-chain. +- Once the public key is known, the **EVM address** and **Bech32 address** are linked automatically. +- This ensures balances and transactions are accessible across both address formats. -- **Different Accounts Before Linking**: Until these accounts are linked, the chain treats the Sei address and EVM address as different accounts with different balances. This means that certain dApps may not be able to access your full balance until the accounts are linked. -- **Public Key Requirement**: The linking won’t happen until the chain has the public key. Using `sei_associate`, users can provide their public key to the chain without any gas fees, as long as they have at least 1 wei in their account. +### **Manual Association** -### Example +If the account has not broadcasted any transaction yet: -1. **Broadcast a Transaction**: When you make any transaction from your EVM account, the public key is recorded, and the accounts get linked automatically. -2. **Manual Association Using `sei_associate`**: If the accounts are not linked automatically, you can manually associate them with the following command as long as the account you are trying to associate has at least 1 wei in it: +- Use the `sei_associate` function to **manually record the public key** on-chain. +- This is a **gasless operation** as long as the account has **at least 1 wei**. +> **Why is the public key required?** +> The public key enables the chain to derive and validate both the Bech32 and EVM addresses. Without it, the chain cannot determine the relationship between these two addresses. + +--- + +## **Key Points** + +### **Before Linking** + +- The Bech32 (`sei...`) and EVM (`0x...`) addresses are treated as **separate accounts**. +- They will have separate balances until linked. +- Native tokens received by the EVM address prior to association will be held in a temporary native address, which will transfer to the associated address upon linking. +- Some types of transactions will **not be possible** (see table below). + +### **After Linking** + +- Balances are reflected consistently across both addresses. +- Applications can query either address format seamlessly. + +--- + +## **Wallet Association and Transfer Limitations** + +Certain actions are **not possible** before wallets are associated: + +- Transfers of **CW-based tokens** (e.g., CW20) from a Sei native wallet to an **unassociated EVM address**. +- Transfers of **ERC-based tokens** (e.g., ERC20) from an EVM wallet to an **unassociated Sei native address**. + +### **Transfer Scenarios** + +| Source Address | Sender Linked | Receiver Linked | Destination Address | Asset Type | Method | +| -------------- | ------------- | --------------- | ------------------- | --------------- | ------------- | +| SEI | Y | Y | Sei | Native | bankSend | +| SEI | Y | N | Sei | Native | bankSend | +| SEI | Y | Y | 0x | Native | evmSendNative | +| SEI | Y | N | 0x | Native | evmSendNative | +| SEI | N | N | Sei | Native | bankSend | +| SEI | N | N | 0x | Native | Not Possible | +| SEI | N | Y | Sei | Native | bankSend | +| SEI | N | Y | 0x | Native | evmSendNative | +| EVM | N | N | 0x | Native | Not Possible | +| EVM | N | Y | Sei | Native | bankSend | +| EVM | N | Y | 0x | Native | evmSendNative | +| SEI | Y | N | Sei | CW-based Token | wasm transfer | +| SEI | Y | N | 0x | CW-based Token | Not Possible | +| SEI | N | Y | Sei | CW-based Token | wasm transfer | +| SEI | N | Y | 0x | CW-based Token | Not Possible | +| EVM | Y | N | Sei | ERC-based Token | Not Possible | +| EVM | Y | N | 0x | ERC-based Token | evm transfer | +| EVM | N | Y | Sei | ERC-based Token | evm transfer | +| EVM | N | Y | 0x | ERC-based Token | evm transfer | + +--- + +## Query Linked Addresses + +### Fetch EVM Address for a Sei Address + +```bash +curl -X POST $SEIEVM -H "Content-Type: application/json" -d \ +'{"jsonrpc": "2.0", "method": "sei_getEVMAddress", "params": [""], "id": 1}' +``` + +**Example Response**: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x4e1ae6017997128D421074FbE31d90362F181C" +} ``` + +**Failure Example**: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32000, + "message": "failed to find EVM address for sei1wev8ptz..." + } +} +``` + +### Fetch Bech32 Address for an EVM Address + +```bash +curl -X POST $SEIEVM -H "Content-Type: application/json" -d \ +'{"jsonrpc": "2.0", "method": "sei_getSeiAddress", "params": [""], "id": 1}' +``` + +**Example Response**: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": "sei1wev8ptzj27aueu04wg..." +} +``` + +--- + +### Manual Association Using `sei_associate` + +If no transaction has been broadcasted, use this command to manually associate the addresses: + +```bash seid tx evm associate-address [optional priv key hex] --rpc= --from= [flags] ``` + +> **Note**: The account must have at least 1 wei to perform this operation. + +--- + +## **Deriving Addresses from the Public Key** + +### **Sei Address Derivation** + +The Sei native address is derived from the public key using the following steps: + +1. Hash the public key using the `keccak256` algorithm. +2. Extract the first 20 bytes of the resulting hash. +3. Encode the extracted bytes in **Bech32 format** with the `sei` prefix. + +Example implementation: + +```ts +import { bech32 } from 'bech32'; +import { keccak256 } from 'ethereumjs-util'; + +export function deriveSeiAddress(publicKey: Buffer): string { + const hash = keccak256(publicKey); + const words = bech32.toWords(hash.slice(0, 20)); + return bech32.encode('sei', words); +} +``` + +### **EVM Address Derivation** + +The EVM-compatible address is derived as follows: + +1. Hash the public key using the `keccak256` algorithm. +2. Extract the **last 20 bytes** of the resulting hash. +3. Prefix the extracted bytes with `0x` to obtain the EVM address. + +Example implementation: + +```ts +import { keccak256 } from 'ethereumjs-util'; + +export function deriveEVMAddress(publicKey: Buffer): string { + const hash = keccak256(publicKey); + return `0x${hash.slice(-20).toString('hex')}`; +} +``` + +### **Summary** + +- **Public Key Hashing**: Both derivations rely on the `keccak256` hashing algorithm. +- **Sei Address**: Extract the **first 20 bytes** of the hash and encode it in **Bech32 format**. +- **EVM Address**: Extract the **last 20 bytes** of the hash and format it in **Hex** with a `0x` prefix. + +### **Why It Works** + +The `keccak256` hashing ensures a consistent and verifiable process for deriving both address formats from the same public key. This enables a single account to maintain compatibility across the Sei native and EVM environments. + +--- + +## **Conclusion** + +- Accounts are automatically linked when a transaction is broadcasted or can be manually associated using `sei_associate`. +- Both address formats share the same **public key**. +- Linking enables dApps and tools to access balances consistently across both address formats. + +--- + +## **Next Steps** + +For detailed technical instructions on how to perform account association, refer to the **Wallet Association** section. diff --git a/pages/dev-advanced-concepts/wallet-association.mdx b/pages/dev-advanced-concepts/wallet-association.mdx new file mode 100644 index 00000000..4023b946 --- /dev/null +++ b/pages/dev-advanced-concepts/wallet-association.mdx @@ -0,0 +1,172 @@ +--- +next: true +--- + +# **Methods for Associating a Wallet on Sei** + +Wallet association on the Sei network ensures the public key becomes known to the chain. Without this step, the chain cannot determine the Bech32 (`sei...`) address or the EVM-compatible (`0x...`) address from one another. + +Below are **4 distinct methods** for associating a wallet. Each method differs in terms of security considerations and required actions. + +--- + +## **Method 1: Direct Private Key Association** + +> **Security Risk**: **High** – Requires the private key to be directly available. Exposing the private key can compromise the wallet. + +This method directly uses the private key to interact with the network. + +```ts +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { seiTestnet } from 'viem/chains'; +import { ADDRESS_PRECOMPILE_ABI, ADDRESS_PRECOMPILE_ADDRESS } from '@sei-js/evm'; + +const PRIVATE_KEY = ''; + +const publicClient = createPublicClient({ chain: seiTestnet, transport: http() }); +const client = createWalletClient({ chain: seiTestnet, transport: http() }); + +const account = privateKeyToAccount(PRIVATE_KEY); + +const response = await client.writeContract({ + account, + address: ADDRESS_PRECOMPILE_ADDRESS, + abi: ADDRESS_PRECOMPILE_ABI, + functionName: 'associate', + args: ['0', '0', '0', 'example_message'], + gasPrice: BigInt(100_000_000_000) +}); +console.log(response); +``` + +--- + +## **Method 2: Associate via Signed Message** + +> **Security Risk**: **Medium** – Requires signing a specific message using the private key. + +This method involves signing a predefined message to prove ownership of the account. + +```ts +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; +import { parseSignature, toHex } from 'viem'; + +const associate = async () => { + const account = privateKeyToAccount(''); + const newPk = generatePrivateKey(); + const newAccount = privateKeyToAccount(newPk); + + const message = 'associate'; + const signature = await newAccount.signMessage({ message }); + const parsedSignature = parseSignature(signature); + + const response = await client.writeContract({ + account, + address: ADDRESS_PRECOMPILE_ADDRESS, + abi: ADDRESS_PRECOMPILE_ABI, + functionName: 'associate', + args: [toHex(Number(parsedSignature.v) - 27), parsedSignature.r, parsedSignature.s, message], + gasPrice: BigInt(100_000_000_000) + }); + console.log(response); +}; + +associate(); +``` + +--- + +## **Method 3: Associate via Public Key** + +> **Security Risk**: **Lower** – Involves using the public key, which is less sensitive than the private key. + +This method compresses the public key and sends it for association. + +```ts +import secp256k1 from 'secp256k1'; +import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'; + +const associateViaPubkey = async () => { + const account = privateKeyToAccount(''); + const newPk = generatePrivateKey(); + const newAccount = privateKeyToAccount(newPk); + + const publicKeyBuffer = Buffer.from(newAccount.publicKey.slice(2), 'hex'); + const compressedPubKey = secp256k1.publicKeyConvert(publicKeyBuffer, true); + + const response = await client.writeContract({ + account, + address: ADDRESS_PRECOMPILE_ADDRESS, + abi: ADDRESS_PRECOMPILE_ABI, + functionName: 'associatePubKey', + args: [Buffer.from(compressedPubKey).toString('hex')], + gasPrice: BigInt(100_000_000_000) + }); + console.log(response); +}; + +associateViaPubkey(); +``` + +--- + +## **Method 4: Gasless Association via Signed Message** + +> **Security Risk**: **Low** – Requires signing a message but does not expose the private key. No gas is consumed if the account already has funds. + +This method signs a message and uses the `sei_associate` RPC call to finalize the association. + +```ts +import { parseSignature, numberToHex } from 'viem'; + +interface AssociateRequest { + r: string; + s: string; + v: string; + custom_message: string; +} + +interface AssociateRequestSchema { + Method: 'sei_associate'; + Parameters: [request: AssociateRequest]; + ReturnType: null; +} + +const associateGasless = async (signature: `0x${string}`, message: string) => { + const parsedSignature = parseSignature(signature); + const messageLength = Buffer.from(message, 'utf8').length; + const messageToSign = `\x19Ethereum Signed Message:\n${messageLength}${message}`; + + const request: AssociateRequest = { + r: parsedSignature.r, + s: parsedSignature.s, + v: numberToHex(Number(parsedSignature.v) - 27), + custom_message: messageToSign + }; + + const response = await client.request({ + method: 'sei_associate', + params: [request] + }); + console.log(response); +}; + +// Example Usage +associateGasless('', 'example_message'); +``` + +--- + +## **Summary of Methods** + +| Method | Security Risk | User Action Required | +| ----------------------------- | ------------- | ------------------------------------------------- | +| **1. Direct Private Key** | High | Provide private key directly. | +| **2. Signed Message** | Medium | Sign a predefined message to prove ownership. | +| **3. Public Key** | Lower | Provide a compressed public key for association. | +| **4. Gasless Signed Message** | Low | Sign a message without requiring gas (if funded). | + +--- + +Using any of these methods will ensure the **public key** is known to the chain, enabling automatic association between the EVM-compatible and Bech32 addresses.