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.
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.
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.
TwinKeyProbe at — — … bytes of runtime. Read over the standard EVM JSON-RPC.
Demo signing is off on this deployment — connect a wallet above to write.
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.
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.
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 } }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.