rspace-online/src/encryptid/external-signer.ts

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)],
});
}
}