2026-06-02 00:28:22 +08:00
import { requestWalletNonce , verifyWalletSignature } from "./api" ;
2026-06-02 02:58:01 +08:00
export type WalletKind = "tokenPocket" | "metaMask" | "imToken" ;
2026-06-02 21:25:05 +08:00
const BNB_CHAIN_ID_HEX = "0x38" ;
const BNB_CHAIN_PARAMS = {
chainId : BNB_CHAIN_ID_HEX ,
chainName : "BNB Smart Chain" ,
nativeCurrency : { name : "BNB" , symbol : "BNB" , decimals : 18 } ,
rpcUrls : [ "https://bsc-dataseed.binance.org" ] ,
blockExplorerUrls : [ "https://bscscan.com" ] ,
} ;
2026-06-02 00:28:22 +08:00
export type EthereumProvider = {
2026-06-02 02:58:01 +08:00
isMetaMask? : boolean ;
isTokenPocket? : boolean ;
isImToken? : boolean ;
providers? : EthereumProvider [ ] ;
2026-06-02 00:28:22 +08:00
request : < T = unknown > ( args : {
method : string ;
params? : unknown [ ] ;
} ) = > Promise < T > ;
} ;
2026-06-02 21:25:05 +08:00
function isAddress ( value : unknown ) : value is string {
return typeof value === "string" && /^0x[a-fA-F0-9]{40}$/ . test ( value ) ;
}
function utf8ToHex ( value : string ) : string {
return ` 0x ${ Array . from ( new TextEncoder ( ) . encode ( value ) , ( byte ) = >
byte . toString ( 16 ) . padStart ( 2 , "0" ) ,
) . join ( "" ) } ` ;
}
function errorText ( error : unknown ) : string {
if ( ! error || typeof error !== "object" ) return String ( error ? ? "" ) ;
const parts : string [ ] = [ ] ;
const record = error as Record < string , unknown > ;
for ( const key of [ "shortMessage" , "message" , "details" ] ) {
const value = record [ key ] ;
if ( typeof value === "string" ) parts . push ( value ) ;
}
if ( record . cause ) parts . push ( errorText ( record . cause ) ) ;
return parts . join ( "\n" ) ;
}
function isNoAccountError ( error : unknown ) : boolean {
return /wallet must has at least one account|wallet must has one account|must have at least one account|no wallet account returned/i . test (
errorText ( error ) ,
) ;
}
function normalizeWalletError ( error : unknown ) : Error {
if ( isNoAccountError ( error ) ) return new Error ( "walletNoAccount" ) ;
if ( error instanceof Error ) return error ;
const message = errorText ( error ) ;
return new Error ( message || "Wallet login failed" ) ;
}
function shouldRetryPersonalSign ( error : unknown ) : boolean {
const text = errorText ( error ) ;
return /wallet must has at least one account|wallet must has one account|must have at least one account|invalid params|invalid account|account not found/i . test (
text ,
) ;
}
async function ensureBnbChain ( ethereum : EthereumProvider ) : Promise < void > {
const chainId = await ethereum
. request < string > ( { method : "eth_chainId" } )
. catch ( ( ) = > "" ) ;
if ( chainId . toLowerCase ( ) === BNB_CHAIN_ID_HEX ) return ;
try {
await ethereum . request ( {
method : "wallet_switchEthereumChain" ,
params : [ { chainId : BNB_CHAIN_ID_HEX } ] ,
} ) ;
} catch ( error ) {
const code = ( error as { code? : number | string } | null ) ? . code ;
if ( code !== 4902 && code !== "4902" ) throw error ;
await ethereum . request ( {
method : "wallet_addEthereumChain" ,
params : [ BNB_CHAIN_PARAMS ] ,
} ) ;
}
}
async function requestInjectedAddress (
ethereum : EthereumProvider ,
) : Promise < string > {
const existingAccounts : unknown [ ] = await ethereum
. request < unknown [ ] > ( { method : "eth_accounts" } )
. catch ( ( ) : unknown [ ] = > [ ] ) ;
const existingAddress = existingAccounts . find ( isAddress ) ;
if ( existingAddress ) return existingAddress ;
const requestedAccounts = await ethereum
. request < unknown [ ] > ( {
method : "eth_requestAccounts" ,
} )
. catch ( ( error : unknown ) : never = > {
throw normalizeWalletError ( error ) ;
} ) ;
const requestedAddress = requestedAccounts . find ( isAddress ) ;
if ( ! requestedAddress ) throw new Error ( "walletNoAccount" ) ;
return requestedAddress ;
}
async function personalSign ( params : {
ethereum : EthereumProvider ;
message : string ;
address : string ;
} ) : Promise < string > {
const { ethereum , message , address } = params ;
const hexMessage = utf8ToHex ( message ) ;
try {
return await ethereum . request < string > ( {
method : "personal_sign" ,
params : [ hexMessage , address ] ,
} ) ;
} catch ( error ) {
if ( ! shouldRetryPersonalSign ( error ) ) throw error ;
// Some injected wallets incorrectly expect the legacy param order.
return ethereum
. request < string > ( {
method : "personal_sign" ,
params : [ address , hexMessage ] ,
} )
. catch ( ( retryError : unknown ) : never = > {
throw normalizeWalletError ( retryError ) ;
} ) ;
}
}
2026-06-02 00:28:22 +08:00
export function getInjectedEthereum ( ) : EthereumProvider | null {
if ( typeof window === "undefined" ) return null ;
const maybeWindow = window as typeof window & { ethereum? : EthereumProvider } ;
return maybeWindow . ethereum ? ? null ;
}
2026-06-02 02:58:01 +08:00
export function getInjectedWallet ( kind? : WalletKind ) : EthereumProvider | null {
const ethereum = getInjectedEthereum ( ) ;
if ( ! ethereum || ! kind ) return ethereum ;
const providers = ethereum . providers ? . length
? ethereum . providers
: [ ethereum ] ;
const match = providers . find ( ( provider ) = > {
if ( kind === "metaMask" ) return provider . isMetaMask ;
if ( kind === "tokenPocket" ) return provider . isTokenPocket ;
if ( kind === "imToken" ) return provider . isImToken ;
return false ;
} ) ;
return match ? ? null ;
}
2026-06-02 10:38:29 +08:00
/** Diagnostic: log what injected providers the browser exposes. */
export function logWalletProviders ( ) : void {
const ethereum = getInjectedEthereum ( ) ;
const list = (
ethereum ? . providers ? . length
? ethereum . providers
: ethereum
? [ ethereum ]
: [ ]
) . map ( ( p ) = > ( {
isMetaMask : Boolean ( p . isMetaMask ) ,
isTokenPocket : Boolean ( p . isTokenPocket ) ,
isImToken : Boolean ( p . isImToken ) ,
} ) ) ;
// eslint-disable-next-line no-console
console . info ( "[wallet-login] providers" , {
hasEthereum : Boolean ( ethereum ) ,
count : list.length ,
list ,
} ) ;
}
2026-06-02 23:32:39 +08:00
export async function connectInjectedWallet (
kind? : WalletKind ,
) : Promise < string > {
/* eslint-disable no-console */
console . info ( "[wallet-login] start injected connect" , { kind } ) ;
logWalletProviders ( ) ;
const ethereum = getInjectedWallet ( kind ) ;
if ( ! ethereum ) {
console . warn ( "[wallet-login] no injected provider found" ) ;
throw new Error ( "No injected wallet found" ) ;
}
console . info ( "[wallet-login] requesting BNB wallet account…" ) ;
const address = await requestInjectedAddress ( ethereum ) ;
console . info ( "[wallet-login] injected account" , address ) ;
console . info ( "[wallet-login] ensuring BNB Chain (0x38)…" ) ;
await ensureBnbChain ( ethereum ) ;
return address ;
/* eslint-enable no-console */
}
2026-06-02 02:58:01 +08:00
export async function signInWithInjectedWallet ( kind? : WalletKind ) : Promise < {
2026-06-02 00:28:22 +08:00
token : string ;
wallet : string ;
} > {
2026-06-02 10:38:29 +08:00
/* eslint-disable no-console */
console . info ( "[wallet-login] start injected" , { kind } ) ;
logWalletProviders ( ) ;
2026-06-02 02:58:01 +08:00
const ethereum = getInjectedWallet ( kind ) ;
2026-06-02 10:38:29 +08:00
if ( ! ethereum ) {
console . warn ( "[wallet-login] no injected provider found" ) ;
throw new Error ( "No injected wallet found" ) ;
}
2026-06-02 00:28:22 +08:00
2026-06-02 21:25:05 +08:00
// BNB Smart Chain is EVM-compatible, so browser wallets still expose the
// standard EIP-1193 method names (`eth_*`) while operating on BNB chain 56.
console . info ( "[wallet-login] requesting BNB wallet account…" ) ;
const address = await requestInjectedAddress ( ethereum ) ;
console . info ( "[wallet-login] account" , address ) ;
console . info ( "[wallet-login] ensuring BNB Chain (0x38)…" ) ;
await ensureBnbChain ( ethereum ) ;
2026-06-02 00:28:22 +08:00
2026-06-02 10:38:29 +08:00
console . info ( "[wallet-login] requesting nonce for" , address ) ;
2026-06-02 00:28:22 +08:00
const nonce = await requestWalletNonce ( address ) ;
2026-06-02 10:38:29 +08:00
console . info ( "[wallet-login] got nonce, requesting personal_sign…" ) ;
2026-06-02 21:25:05 +08:00
const signature = await personalSign ( {
ethereum ,
message : nonce.message ,
address ,
2026-06-02 00:28:22 +08:00
} ) ;
2026-06-02 10:38:29 +08:00
console . info ( "[wallet-login] signed, verifying with backend…" ) ;
const result = await verifyWalletSignature ( {
2026-06-02 00:28:22 +08:00
address ,
message : nonce.message ,
signature ,
} ) ;
2026-06-02 10:38:29 +08:00
console . info ( "[wallet-login] verified, wallet =" , result . wallet ) ;
return result ;
/* eslint-enable no-console */
2026-06-02 00:28:22 +08:00
}