Skip to main content
Version: 0.4.1

Local Signer

LocalSignerClient (packages/wallet/src/background/signer.ts) is the wallet's implementation of ITezosWalletClient. It signs Michelson runtime 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 Michelson runtime 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; // Michelson runtime 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 Michelson runtime 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; // Michelson runtime opHash

The decision is made in the service worker's SEND_TX handler based on detectRuntime(msg.to):

assetrecipient runtimepath
XTZl1 (tz1 / KT1 / …)sendNativeTransfer (no gateway)
XTZl2 (0x…)RelayerProvider.request('eth_sendTransaction')sendContractCall('default', …)
USDCl2 (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

AspectLocalSignerClientBeaconClient
Key locationSW memory (from keyring)Temple Wallet
Temple requiredNoYes
Signing popupNoneTemple popup
Fee estimationTaquito (automatic)Beacon / Temple
Used byTezosX Wallet extensionTezosX 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.