From df14db3a43e7a4f81e703058f3a07a49f7b93d0a Mon Sep 17 00:00:00 2001 From: alexander-sei Date: Mon, 25 May 2026 12:58:18 +0300 Subject: [PATCH 1/2] Retire deprecated sei_* association methods, document precompile path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sei_* JSON-RPC namespace is deprecated and gated by enabled_legacy_sei_apis in app.toml. Of the three address-related legacy methods: - sei_associate is not in the default allowlist and returns legacy_sei_deprecated on every public RPC unless the node operator has opted in. It is functionally a relay-only path that no public Sei node currently accepts. - sei_getSeiAddress and sei_getEVMAddress are enabled by default today, but belong to the same deprecated namespace and may be removed in a future release. All three are duplicated by the on-chain addr precompile at 0x...1004: associate(v, r, s, customMessage) / associatePubKey(pubKeyHex) for writes, getSeiAddr(address) / getEvmAddr(string) for reads. The precompile is a normal EVM call available on every public RPC, never deprecated, and is what the dashboard and other first-party integrations now use. learn/accounts.mdx: - Drop the entire "Method 5: Gasless Association via Signed Message" section (sei_associate-based) and its row in the methods table. - Drop the "Manual Association From the CLI" subsection — its only consumer was an embedded seid command for a flow we no longer document here. - Replace the legacy curl examples under "Query Linked Addresses" with precompile-based eth_call examples (curl + viem TS) covering both resolution directions. - Add an explanatory on the methods table and recap pointing operators who genuinely want sei_associate to the node-operators page. evm/reference.mdx: - Drop the sei_associate, sei_getSeiAddress, sei_getEVMAddress rows from the legacy methods accordion and their dedicated subsections in "Address & Cross-VM Helpers". - Split that section into an "Address Resolution" subsection (precompile + curl + viem examples, plus a on the historical methods) and a "Cross-VM Transaction Lookup" subsection that keeps sei_getCosmosTx (no precompile equivalent yet). - Leave the default enabled_legacy_sei_apis code block intact — it documents what `seid init` actually emits, and the chain still ships with the two get-helpers enabled by default. node/node-operators.mdx is intentionally untouched: it mirrors the chain's default app.toml, so the commented sei_associate line and the two enabled get-helpers stay there as accurate operator reference. Co-Authored-By: Claude Opus 4.7 --- evm/reference.mdx | 117 ++++++++++++----------------------- learn/accounts.mdx | 148 ++++++++++++++++----------------------------- 2 files changed, 88 insertions(+), 177 deletions(-) diff --git a/evm/reference.mdx b/evm/reference.mdx index 1f014a7..2aaa5b5 100644 --- a/evm/reference.mdx +++ b/evm/reference.mdx @@ -734,9 +734,6 @@ To enable additional legacy methods, add them to this array. All other `sei_*` a | Method | Description | | ------ | ----------- | -| `sei_associate` | Associate Sei and EVM addresses | -| `sei_getSeiAddress` | Get Sei address for an EVM address | -| `sei_getEVMAddress` | Get EVM address for a Sei address | | `sei_getCosmosTx` | Get Cosmos transaction by EVM tx hash | | `sei_getEvmTx` | Get EVM transaction by Cosmos tx hash | | `sei_getTransactionErrorByHash` | Get error message for a failed transaction | @@ -777,99 +774,59 @@ To enable additional legacy methods, add them to this array. All other `sei_*` a -### Address & Cross-VM Helpers +### Address Resolution -These endpoints provide cross-VM address resolution and transaction lookup between the EVM and Cosmos environments. `sei_getSeiAddress`, `sei_getEVMAddress`, and `sei_getCosmosTx` are enabled by default. +For resolving the EVM (`0x…`) ↔ Sei (`sei1…`) address pair, use the **`addr` precompile at `0x0000000000000000000000000000000000001004`** via a standard `eth_call`. The precompile is universally available on every Sei RPC, is not part of the deprecated `sei_*` namespace, and is the canonical resolution path going forward. - -#### sei_associate - -Sends a transaction to establish association between the signer's Sei address -and EVM address on-chain. - -- **Parameters**: - -| Type | Description | -| :------- | :--------------------------------------------------------------------------------- | -| `object` | A custom object containing a string message and the v, r, s of the signed message. | - -Object Schema: - -```json -{ - custom_message: // Any string message - r: // The R-part of the signature over the Keccak256 hash of the custom message. - s: // The S-part of the signature over the Keccak256 hash of the custom message. - v: // The V-part of the signature over the Keccak256 hash of the custom message. -} -``` - -- **Result**: - -| Type | Description | -| :------- | :------------------------------------ | -| `string` | The transaction hash of the association transaction. | - ---- - -#### sei_getSeiAddress - -Returns the Sei (bech32) address associated with an EVM address. - -- **Parameters**: - -| Type | Description | -| :------- | :---------------------------------- | -| `string` | The EVM address (0x-prefixed hex). | - -- **Result**: - -| Type | Description | -| :------- | :------------------------------------- | -| `string` | The associated Sei bech32 address. | - -**Example Request** - -```json -{ +```bash +# EVM → Sei (getSeiAddr(address), selector 0x0c3c20ed) +curl -X POST $SEIEVM -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", - "method": "sei_getSeiAddress", - "params": ["0x1234567890abcdef1234567890abcdef12345678"], + "method": "eth_call", + "params": [{ + "to": "0x0000000000000000000000000000000000001004", + "data": "0x0c3c20ed000000000000000000000000" + }, "latest"], "id": 1 -} +}' ``` ---- +In TypeScript with viem: -#### sei_getEVMAddress +```ts +import { createPublicClient, http } from 'viem'; +import { sei } from 'viem/chains'; -Returns the EVM address associated with a Sei (bech32) address. +const ADDR_PRECOMPILE = '0x0000000000000000000000000000000000001004'; +const ADDR_ABI = [ + { name: 'getSeiAddr', type: 'function', stateMutability: 'view', + inputs: [{ name: 'addr', type: 'address' }], + outputs: [{ name: 'response', type: 'string' }] }, + { name: 'getEvmAddr', type: 'function', stateMutability: 'view', + inputs: [{ name: 'addr', type: 'string' }], + outputs: [{ name: 'response', type: 'address' }] }, +] as const; -- **Parameters**: +const client = createPublicClient({ chain: sei, transport: http() }); -| Type | Description | -| :------- | :------------------------------ | -| `string` | The Sei bech32 address. | +const seiAddr = await client.readContract({ + address: ADDR_PRECOMPILE, abi: ADDR_ABI, functionName: 'getSeiAddr', args: ['0x…'], +}); -- **Result**: +const evmAddr = await client.readContract({ + address: ADDR_PRECOMPILE, abi: ADDR_ABI, functionName: 'getEvmAddr', args: ['sei1…'], +}); +``` -| Type | Description | -| :------- | :------------------------------- | -| `string` | The associated EVM address. | +The call reverts when the input address is not yet associated. See [Accounts](/learn/accounts) for the full association lifecycle and how the bidirectional mapping is established. -**Example Request** +The legacy JSON-RPC helpers `sei_getSeiAddress` and `sei_getEVMAddress` historically served this role and are still enabled by default on nodes today, but they belong to the deprecated `sei_*` namespace and may be removed in a future release. New integrations should call the precompile. -```json -{ - "jsonrpc": "2.0", - "method": "sei_getEVMAddress", - "params": ["sei1..."], - "id": 1 -} -``` +### Cross-VM Transaction Lookup ---- +`sei_getCosmosTx` resolves the underlying Cosmos transaction hash for a given EVM transaction. It does not yet have a precompile equivalent and is enabled by default on Sei nodes. + #### sei_getCosmosTx Returns the Cosmos transaction details for a given EVM transaction hash. diff --git a/learn/accounts.mdx b/learn/accounts.mdx index 900c7dc..491b267 100644 --- a/learn/accounts.mdx +++ b/learn/accounts.mdx @@ -61,14 +61,15 @@ Certain actions are **not possible** before wallets are associated: | --- | --- | --- | | 1. Broadcast a Transaction | Low | Association happens automatically | | 2. Direct Private Key | High | Provide private key directly | -| 3. Signed Message | Medium | Sign a predefined message to prove ownership | +| 3. Signed Message | Medium | Sign a predefined message to prove ownership *(recommended for wallets)* | | 4. Public Key | Low | Provide a compressed public key for association | -| 5. 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. +Each method ensures the **public key** is known to the chain, enabling automatic association between the EVM-compatible and Bech32 addresses. All four go through the on-chain `addr` precompile (`0x0000000000000000000000000000000000001004`) or the EVM ante handler and are available on every Sei EVM RPC. + +A previous "gasless" association flow that used the `sei_associate` JSON-RPC method has been retired — the method is part of the deprecated `sei_*` namespace and is not in the default `enabled_legacy_sei_apis` allowlist, so it returns `legacy_sei_deprecated` on public RPCs. Method 3 covers the same wallet-signed-message UX without depending on a gated endpoint. Node operators who want to re-enable the legacy method on their own infrastructure can add `sei_associate` to `enabled_legacy_sei_apis` in `app.toml` — see [Node Operators](/node/node-operators) for the surrounding config. Constants for the `addr` precompile can also be found in the repo -[Sei-Chain/precompiles](https://github.com/sei-protocol/sei-chain/tree/44fd60cec6a5ef301df1472431d6db40b382e486/precompiles/addr): +[Sei-Chain/precompiles](https://github.com/sei-protocol/sei-chain/tree/main/precompiles/addr): ## Method 1: Broadcast a Transaction @@ -178,111 +179,64 @@ const associateViaPubkey = async () => { associateViaPubkey(); ``` -## Method 5: 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'); -``` - ## 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..." - } -} -``` +Resolve either side of an existing association by calling the `addr` precompile at `0x0000000000000000000000000000000000001004` over a standard `eth_call`. The precompile is universally available on every Sei RPC. ### 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}' +curl -X POST $SEIEVM -H "Content-Type: application/json" -d '{ + "jsonrpc": "2.0", + "method": "eth_call", + "params": [{ + "to": "0x0000000000000000000000000000000000001004", + "data": "0x0c3c20ed000000000000000000000000" + }, "latest"], + "id": 1 +}' ``` -**Example Response**: +The selector `0x0c3c20ed` is `getSeiAddr(address)`. The returned ABI-encoded string is the bech32 `sei1…` address, or the call reverts if the EVM address is not yet associated. -```json -{ - "jsonrpc": "2.0", - "id": 1, - "result": "sei1wev8ptzj27aueu04wg..." -} -``` +In TypeScript with viem: -### Manual Association Using `sei_associate` +```ts +import { createPublicClient, http } from 'viem'; +import { sei } from 'viem/chains'; + +const ADDR_PRECOMPILE = '0x0000000000000000000000000000000000001004'; +const ADDR_ABI = [ + { name: 'getSeiAddr', type: 'function', stateMutability: 'view', + inputs: [{ name: 'addr', type: 'address' }], + outputs: [{ name: 'response', type: 'string' }] }, + { name: 'getEvmAddr', type: 'function', stateMutability: 'view', + inputs: [{ name: 'addr', type: 'string' }], + outputs: [{ name: 'response', type: 'address' }] }, +] as const; + +const client = createPublicClient({ chain: sei, transport: http() }); + +const seiAddr = await client.readContract({ + address: ADDR_PRECOMPILE, + abi: ADDR_ABI, + functionName: 'getSeiAddr', + args: ['0x…'], +}); +``` -If no transaction has been broadcasted, use this command to manually associate -the addresses: +### Fetch EVM Address for a Sei Address -```bash -seid tx evm associate-address [optional priv key hex] --rpc= --from= [flags] +```ts +const evmAddr = await client.readContract({ + address: ADDR_PRECOMPILE, + abi: ADDR_ABI, + functionName: 'getEvmAddr', + args: ['sei1…'], +}); ``` -**Note**: The account must have at least 1 wei to perform this operation. +The precompile reverts if the address has never been associated; surface this as "not linked" rather than re-throwing. ## Deriving Addresses from the Public Key @@ -343,8 +297,8 @@ maintain compatibility across the Cosmos and EVM environments. ### Recap -- Accounts are automatically linked when a transaction is broad-casted or can be - manually associated using `sei_associate`. +- Accounts are automatically linked when a transaction is broadcast, or can be + associated manually via the `addr` precompile (`associate` / `associatePubKey`). - Both address formats share the same **public key**. - Linking enables dApps and tools to access balances consistently across both address formats. From c2b92f1917487e1a12384745701124781e76cba8 Mon Sep 17 00:00:00 2001 From: alexander-sei Date: Mon, 25 May 2026 13:05:39 +0300 Subject: [PATCH 2/2] Fix Cosmos address derivation snippet (use SHA256+RIPEMD160, not keccak256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Sei Address Derivation" snippet was applying the Ethereum derivation algorithm to the Cosmos side: keccak256 the pubkey, then take the first 20 bytes. That doesn't match what the chain actually does — see `PubkeyBytesToSeiPubKey` and the standard cosmos secp256k1 `PubKey.Address()` in sei-chain/utils/helpers/address.go, which is SHA256 -> RIPEMD160 of the compressed pubkey (the canonical Cosmos derivation). The Python and TypeScript "Generating Wallets" snippets later in the same doc already implement this correctly, so the bug was just in the standalone "Sei Address Derivation" section near the top. Also tightened the paired EVM derivation snippet: - Made the input type explicit (uncompressed pubkey, 65 bytes, leading 0x04). - Slice off the 0x04 prefix byte before hashing, matching the Python snippet ("Exclude the first byte") and the actual EVM derivation in sei-chain/utils/helpers/address.go:PubkeyToEVMAddress. Updated the Summary block and the "Why It Works" paragraph to reflect that the two derivations use different algorithms, and switched both example imports from ethereumjs-util to @noble/hashes for consistency with the canonical snippets later in the file. Co-Authored-By: Claude Opus 4.7 --- learn/accounts.mdx | 60 +++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/learn/accounts.mdx b/learn/accounts.mdx index 491b267..8d2a527 100644 --- a/learn/accounts.mdx +++ b/learn/accounts.mdx @@ -240,24 +240,34 @@ The precompile reverts if the address has never been associated; surface this as ## Deriving Addresses from the Public Key +Both address formats come from the same secp256k1 public key, but they use +**different hashing schemes**: the bech32 (`sei1…`) side follows the standard +Cosmos derivation (`SHA256` then `RIPEMD160` of the **compressed** pubkey), and +the EVM (`0x…`) side follows the standard Ethereum derivation (`keccak256` of +the **uncompressed** pubkey without its `0x04` prefix byte). + ### Sei Address Derivation The Cosmos 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. +1. Take the **compressed** secp256k1 public key (33 bytes; first byte `0x02` or `0x03`). +2. Hash it with `SHA256`. +3. Hash the result with `RIPEMD160` to get a 20-byte digest. +4. Encode that digest in **Bech32 format** with the `sei` prefix. Example implementation: ```ts import { bech32 } from 'bech32'; -import { keccak256 } from 'ethereumjs-util'; +import { sha256 } from '@noble/hashes/sha256'; +import { ripemd160 } from '@noble/hashes/ripemd160'; -export function deriveSeiAddress(publicKey: Buffer): string { - const hash = keccak256(publicKey); - const words = bech32.toWords(hash.slice(0, 20)); - return bech32.encode('sei', words); +/** + * @param compressedPublicKey 33-byte secp256k1 pubkey in compressed form + */ +export function deriveSeiAddress(compressedPublicKey: Uint8Array): string { + const digest = ripemd160(sha256(compressedPublicKey)); + return bech32.encode('sei', bech32.toWords(digest)); } ``` @@ -265,35 +275,37 @@ export function deriveSeiAddress(publicKey: Buffer): string { 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. +1. Take the **uncompressed** secp256k1 public key (65 bytes; first byte `0x04`). +2. Drop the leading `0x04` prefix byte so the input to the hash is the bare 64-byte `(x, y)` coordinate pair. +3. Hash with `keccak256`. +4. Take the **last 20 bytes** of the hash and format as `0x…` hex. Example implementation: ```ts -import { keccak256 } from 'ethereumjs-util'; +import { keccak_256 } from '@noble/hashes/sha3'; -export function deriveEVMAddress(publicKey: Buffer): string { - const hash = keccak256(publicKey); - return `0x${hash.slice(-20).toString('hex')}`; +/** + * @param uncompressedPublicKey 65-byte secp256k1 pubkey in uncompressed form (leading 0x04) + */ +export function deriveEVMAddress(uncompressedPublicKey: Uint8Array): string { + const hash = keccak_256(uncompressedPublicKey.slice(1)); + return `0x${Buffer.from(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. +- **Sei Address**: `bech32('sei', RIPEMD160(SHA256(compressedPubKey)))` — 20 bytes, Cosmos-standard derivation. +- **EVM Address**: `'0x' + keccak256(uncompressedPubKey[1:])[-20:]` — last 20 bytes of the keccak256 hash, Ethereum-standard derivation. +- The two formats share an account because the chain stores the **public key** itself on association; either format can be derived from it deterministically. ### 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 Cosmos and EVM environments. +Both formats are deterministic, public-key-derived address schemes. Once the +public key is on-chain (via any of the four association methods above, or +implicitly via a first signed transaction), the chain can derive both formats +itself and route any incoming reference to the same account. ### Recap