Advanced Concepts
Wallet Association

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 along with relevant defitnitions. Each method differs in terms of security considerations and required actions.


Global Constants

These constants for the addr precompile can also be found in the repo Sei-Chain/precompiles (opens in a new tab):

// Constants for precompile address and ABI
export const ADDRESS_PRECOMPILE_ADDRESS = '0x0000000000000000000000000000000000001004';
 
export const ADDRESS_PRECOMPILE_ABI = [
  {
    inputs: [
      { internalType: 'string', name: 'v', type: 'string' },
      { internalType: 'string', name: 'r', type: 'string' },
      { internalType: 'string', name: 's', type: 'string' },
      { internalType: 'string', name: 'customMessage', type: 'string' }
    ],
    name: 'associate',
    outputs: [
      { internalType: 'string', name: 'seiAddr', type: 'string' },
      { internalType: 'address', name: 'evmAddr', type: 'address' }
    ],
    stateMutability: 'nonpayable',
    type: 'function'
  },
  {
    inputs: [{ internalType: 'string', name: 'pubKeyHex', type: 'string' }],
    name: 'associatePubKey',
    outputs: [
      { internalType: 'string', name: 'seiAddr', type: 'string' },
      { internalType: 'address', name: 'evmAddr', type: 'address' }
    ],
    stateMutability: 'nonpayable',
    type: 'function'
  },
  {
    inputs: [{ internalType: 'address', name: 'addr', type: 'address' }],
    name: 'getSeiAddr',
    outputs: [{ internalType: 'string', name: 'response', type: 'string' }],
    stateMutability: 'view',
    type: 'function'
  },
  {
    inputs: [{ internalType: 'string', name: 'addr', type: 'string' }],
    name: 'getEvmAddr',
    outputs: [{ internalType: 'address', name: 'response', type: 'address' }],
    stateMutability: 'view',
    type: 'function'
  }
];

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.

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 = '<replace_with_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.

import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
import { parseSignature, toHex } from 'viem';
 
const associate = async () => {
	const account = privateKeyToAccount('<replace_with_private_key>');
	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.

import secp256k1 from 'secp256k1';
import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts';
 
const associateViaPubkey = async () => {
	const account = privateKeyToAccount('<replace_with_private_key>');
	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.

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<AssociateRequestSchema>({
		method: 'sei_associate',
		params: [request]
	});
	console.log(response);
};
 
// Example Usage
associateGasless('<replace_with_signature>', 'example_message');

Summary of Methods

MethodSecurity RiskUser Action Required
1. Direct Private KeyHighProvide private key directly.
2. Signed MessageMediumSign a predefined message to prove ownership.
3. Public KeyLowerProvide a compressed public key for association.
4. Gasless Signed MessageLowSign 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.