TwinKey
chain …spec …block …evm chain id …

One key. Both runtimes.

Portaldot V3 gives every account an SS58 address and an H160, then asks you to install a second wallet with a second seed to use Solidity. TwinKey turns the key you already have into a working EVM account — no second wallet, no seed import, no bridging between your own two identities.

Every number on this page was read from PortaldotV3 Test seconds ago.

Connect the wallet you already have

A Substrate wallet signs with sr25519 — the only signature this chain accepts for a contract call. TwinKey never sees your seed: the signature happens inside the extension.

No Substrate wallet detected in this browser. Install the polkadot.js extension, Talisman or SubWallet, then reload — or use the demo signer below, which signs the same transaction type server-side so the flow is identical.

Your account, both faces resolved on-chain

The H160 is asked of the runtime API ReviveApi_address — never derived locally. The mapping is not a truncation or a hash of the public key, so any client that computes it itself produces the wrong address.

reading chain…

Live contract state eth_call

TwinKeyProbe at — — … bytes of runtime. Read over the standard EVM JSON-RPC.

value—
callCount—
lastCaller—

Demo signing is off on this deployment — connect a wallet above to write.

EVM events, decoded read from Substrate, not eth_getLogs

The EVM endpoint's log index omits logs emitted by native-origin revive.call — including our own contract's. So this scans the Substrate event stream and decodes revive.ContractEmitted against the ABI instead.

Independent verdict Rust verifier, separate process

A second implementation with its own RPC readers checks that the state the EVM layer reports actually matches what was emitted on the Substrate side. If it is not running, this says so rather than inventing a verdict.

What a dApp developer changes: one line

Existing viem / wagmi / ethers code keeps working. The provider answers eth_call, eth_estimateGas, eth_sendTransaction and eth_getTransactionReceipt — the write goes out as a Substrate extrinsic signed by the user's own key.

import { createWalletClient, custom } from "viem";
import { createTwinKeyProvider } from "twinkey";

const twinKey = createTwinKeyProvider({ signer });   // polkadot.js, Talisman, SubWallet

const wallet = createWalletClient({ transport: custom(twinKey) });
const hash = await wallet.writeContract({
  address: "0x19d5aa109cbf0d4e0a126f1438ea73b619cb7725",
  abi, functionName: "setValue", args: [2077n],
});

const receipt = await wallet.waitForTransactionReceipt({ hash });
// receipt.logs[0].parsed -> { eventName: "ValueSet", args: { caller, value, callCount } }
Why the write cannot just be an EVM transaction: on this testnet build an external EOA cannot send one at all — every transaction type, fee and gas combination returns Invalid Transaction, while reads work normally. The native revive.call path is the only write path that exists, and it requires an sr25519 signature. That is the technical wall TwinKey is built on, and it is why a plain wallet adapter is not enough.