Local Signer
LocalSignerClient (packages/wallet/src/background/signer.ts) is the wallet's implementation of ITezosWalletClient. It signs Tezos L1 operations locally using a secret key held in service worker memory — no Temple or Beacon SDK required.
Interface
LocalSignerClient implements ITezosWalletClient:
interface ITezosWalletClient {
getActiveAccount(): Promise<WalletPermissions | null>;
setAccountChangeHandler(cb: (tz1: string | null) => void): void;
requestPermissions(): Promise<WalletPermissions>;
sendContractCall(
entrypoint: string,
michelineArg: MichelsonV1Expression,
mutezAmount?: string,
): Promise<string>; // returns Tezos L1 opHash
disconnect(): Promise<void>;
}
Construction
The service worker creates a LocalSignerClient from the unlocked identity immediately after keyring.unlock():
const signer = new LocalSignerClient(
unlocked.secretKey, // edsk…
unlocked.publicKey, // edpk…
unlocked.tz1, // tz1…
);
provider = new RelayerProvider(signer);
Internally it initialises a Taquito TezosToolkit pointed at the Tezos X Previewnet RPC, and registers an InMemorySigner with the secret key:
this.toolkit = new TezosToolkit(TEZOS_L1_RPC);
this.toolkit.setProvider({ signer: new InMemorySigner(secretKey) });
sendContractCall
When RelayerProvider needs to send an operation (e.g. for eth_sendTransaction), it calls:
signer.sendContractCall(entrypoint, michelineArg, mutezAmount)
This submits a TRANSACTION operation to the NAC gateway contract (KT18oDJJKXMKhfE1bSuAPGp92pYcwVDiqsPw) using Taquito's contract.transfer:
const op = await this.toolkit.contract.transfer({
to: NAC_CONTRACT,
amount: Number(mutezAmount),
mutez: true,
parameter: { entrypoint, value: michelineArg },
});
return op.hash; // Tezos L1 opHash (Base58Check)
Taquito handles fee estimation, gas and storage limit simulation, and signature injection automatically.
sendNativeTransfer
For same-runtime XTZ transfers (tz1 → tz1 / KT1), routing through the gateway would be wasteful — the NAC contract would just receive mutez from the source and forward them to the destination, with no EVM state ever touched. The wallet bypasses the gateway in that case and calls a plain Tezos L1 transfer directly:
signer.sendNativeTransfer(to, mutezAmount)
const op = await this.toolkit.contract.transfer({
to, // tz1 / tz2 / tz3 / KT1
amount: Number(mutezAmount),
mutez: true,
});
return op.hash; // Tezos L1 opHash
The decision is made in the service worker's SEND_TX handler based on detectRuntime(msg.to):
| asset | recipient runtime | path |
|---|---|---|
XTZ | l1 (tz1 / KT1 / …) | sendNativeTransfer (no gateway) |
XTZ | l2 (0x…) | RelayerProvider.request('eth_sendTransaction') → sendContractCall('default', …) |
USDC | l2 (0x…) | RelayerProvider.request('eth_sendTransaction') → sendContractCall('call_evm', …) |
sendNativeTransfer is a wallet-only addition; it lives on LocalSignerClient but is not part of the ITezosWalletClient interface — the relayer never needs to know about same-runtime shortcuts since it always targets EVM state.
Comparison with BeaconClient
| Aspect | LocalSignerClient | BeaconClient |
|---|---|---|
| Key location | SW memory (from keyring) | Temple Wallet |
| Temple required | No | Yes |
| Signing popup | None | Temple popup |
| Fee estimation | Taquito (automatic) | Beacon / Temple |
| Used by | TezosX Wallet extension | TezosX Relayer extension |
disconnect() | No-op (keyring handles lock) | Clears Beacon active account |
Lifecycle
LocalSignerClient is stateless beyond its constructor arguments. It is recreated every time the wallet unlocks:
keyring.unlock() → new LocalSignerClient(sk, pk, tz1) → new RelayerProvider(signer)
When the wallet locks, provider = null discards the instance. The secret key is no longer accessible until the next unlock.