118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
/**
|
|
* External Wallet Signer
|
|
*
|
|
* Wraps an EIP-1193 provider (from EIP-6963 discovery) for transaction
|
|
* operations. The browser wallet (MetaMask, Rainbow, etc.) handles all
|
|
* signing — we just construct and forward requests.
|
|
*/
|
|
|
|
import type { EIP1193Provider } from './eip6963';
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
export interface TransactionRequest {
|
|
from: string;
|
|
to: string;
|
|
value?: string;
|
|
data?: string;
|
|
gas?: string;
|
|
gasPrice?: string;
|
|
maxFeePerGas?: string;
|
|
maxPriorityFeePerGas?: string;
|
|
nonce?: string;
|
|
chainId?: string;
|
|
}
|
|
|
|
export interface TypedDataDomain {
|
|
name?: string;
|
|
version?: string;
|
|
chainId?: number;
|
|
verifyingContract?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// EXTERNAL SIGNER
|
|
// ============================================================================
|
|
|
|
export class ExternalSigner {
|
|
private provider: EIP1193Provider;
|
|
|
|
constructor(provider: EIP1193Provider) {
|
|
this.provider = provider;
|
|
}
|
|
|
|
async getAccounts(): Promise<string[]> {
|
|
return this.provider.request({ method: 'eth_requestAccounts' });
|
|
}
|
|
|
|
async getChainId(): Promise<string> {
|
|
return this.provider.request({ method: 'eth_chainId' });
|
|
}
|
|
|
|
async switchChain(chainId: number): Promise<void> {
|
|
const hexChainId = '0x' + chainId.toString(16);
|
|
try {
|
|
await this.provider.request({
|
|
method: 'wallet_switchEthereumChain',
|
|
params: [{ chainId: hexChainId }],
|
|
});
|
|
} catch (err: any) {
|
|
// 4902 = chain not added
|
|
if (err?.code === 4902) {
|
|
throw new Error(`Chain ${chainId} not configured in wallet. Please add it manually.`);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async sendTransaction(tx: TransactionRequest): Promise<string> {
|
|
// Ensure correct chain
|
|
if (tx.chainId) {
|
|
const currentChain = await this.getChainId();
|
|
const targetHex = '0x' + parseInt(tx.chainId).toString(16);
|
|
if (currentChain.toLowerCase() !== targetHex.toLowerCase()) {
|
|
await this.switchChain(parseInt(tx.chainId));
|
|
}
|
|
}
|
|
|
|
return this.provider.request({
|
|
method: 'eth_sendTransaction',
|
|
params: [tx],
|
|
});
|
|
}
|
|
|
|
async personalSign(message: string, account: string): Promise<string> {
|
|
return this.provider.request({
|
|
method: 'personal_sign',
|
|
params: [message, account],
|
|
});
|
|
}
|
|
|
|
async signTypedData(
|
|
account: string,
|
|
domain: TypedDataDomain,
|
|
types: Record<string, Array<{ name: string; type: string }>>,
|
|
value: Record<string, any>,
|
|
): Promise<string> {
|
|
// Build EIP712Domain type array dynamically from domain fields
|
|
const domainType: Array<{ name: string; type: string }> = [];
|
|
if (domain.name !== undefined) domainType.push({ name: 'name', type: 'string' });
|
|
if (domain.version !== undefined) domainType.push({ name: 'version', type: 'string' });
|
|
if (domain.chainId !== undefined) domainType.push({ name: 'chainId', type: 'uint256' });
|
|
if (domain.verifyingContract !== undefined) domainType.push({ name: 'verifyingContract', type: 'address' });
|
|
const data = {
|
|
types: { EIP712Domain: domainType, ...types },
|
|
domain,
|
|
primaryType: Object.keys(types).find(k => k !== 'EIP712Domain') || '',
|
|
message: value,
|
|
};
|
|
|
|
return this.provider.request({
|
|
method: 'eth_signTypedData_v4',
|
|
params: [account, JSON.stringify(data)],
|
|
});
|
|
}
|
|
}
|