feat: show addresses, not account ids, and default the prefix to 189

Two halves of #6.

**The approval screen names its recipient.** Moving call decoding onto
@quantus/codec left `dest` rendering as 32 bytes of hex, because the codec has no
business guessing an SS58 prefix. `metadataExpand` now tells it the one the
chain's own metadata definition carries, so a transfer shows
`qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y` rather than
`0x300bb607…`. Somebody approving a transfer has to be able to check the
recipient against what they meant to send to, and hex is the form nobody checks.

Account ids are identified by registry path, not by length, so a 32-byte block
hash still renders as hex — quantus/wasm has the test.

**The fallback prefix is 189, not 42.** Upstream falls back to the generic
Substrate prefix because it is a wallet for every Substrate chain and has no
reason to prefer one. This one does: every account it can hold is a Quantus
account, so 42 would show a correct address in a form no Quantus tool displays —
the same account id, the same funds, an unfamiliar string. Somebody comparing the
extension against quantus-cli or the mobile wallet would reasonably conclude they
had created the wrong account. A chain's own ss58Format still wins where one is
known, and the setting still overrides.

Refs #6, quantus/wasm#3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
2026-09-15 15:26:26 +03:00
parent 4f759c4094
commit 7fbcae31e4
8 changed files with 48 additions and 15 deletions

View File

@@ -34,7 +34,7 @@
"@polkadot/ui-settings": "^3.16.7",
"@polkadot/util": "^14.0.3",
"@polkadot/util-crypto": "^14.0.3",
"@quantus/codec": "^0.4.0",
"@quantus/codec": "^0.5.0",
"@quantus/crypto": "^0.1.1",
"eventemitter3": "^5.0.1",
"rxjs": "^7.8.1",

View File

@@ -25,7 +25,7 @@
"@polkadot/networks": "^14.0.3",
"@polkadot/util": "^14.0.3",
"@polkadot/util-crypto": "^14.0.3",
"@quantus/codec": "^0.4.0",
"@quantus/codec": "^0.5.0",
"tslib": "^2.8.1"
},
"peerDependencies": {

View File

@@ -29,9 +29,16 @@ const expanded = new Map<string, Chain>();
* for — see `RequestExtrinsicSign` — which is a better outcome than an exception
* thrown from a metadata lookup on an unrelated screen.
*/
function fromMetadata (bytes: Uint8Array, genesisHash: string): Runtime | null {
function fromMetadata (bytes: Uint8Array, genesisHash: string, ss58Format: number): Runtime | null {
try {
return Runtime.fromMetadata(bytes);
const runtime = Runtime.fromMetadata(bytes);
// So a decoded call names its recipient as an address rather than as 32
// bytes of hex. That matters on exactly one screen — the one asking somebody
// to approve a transfer — and hex is the form nobody checks.
runtime.setSs58Format(ss58Format);
return runtime;
} catch (error) {
console.error(`Unable to read the metadata for ${genesisHash}: ${(error as Error).message}`);
@@ -76,7 +83,7 @@ export function metadataExpand (definition: MetadataDef, isPartial = false): Cha
// replacement, and it is the runtime describing itself rather than a decoder
// written against a version of it.
const runtime = metadataBytes && !isPartial
? fromMetadata(metadataBytes, genesisHash)
? fromMetadata(metadataBytes, genesisHash, ss58Format)
: null;
const hasMetadata = !!runtime;

View File

@@ -21,6 +21,7 @@ import details from '../assets/details.svg';
import { useMetadata, useOutsideClick, useToast, useTranslation } from '../hooks/index.js';
import { showAccount } from '../messaging.js';
import { styled } from '../styled.js';
import { DEFAULT_PREFIX } from '../util/defaultPrefix.js';
import { DEFAULT_TYPE } from '../util/defaultType.js';
import getParentNameSuri from '../util/getParentNameSuri.js';
import { AccountContext, SettingsContext } from './contexts.js';
@@ -75,7 +76,7 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain:
const publicKey = isHex(address) ? hexToU8a(address) : decodeAddress(address);
// find our account using the actual publicKey, and then find the associated chain
const account = findSubstrateAccount(accounts, publicKey);
const prefix = chain ? chain.ss58Format : (settings.prefix === -1 ? 42 : settings.prefix);
const prefix = chain ? chain.ss58Format : (settings.prefix === -1 ? DEFAULT_PREFIX : settings.prefix);
// always allow the actual settings to override the display
return {

View File

@@ -10,6 +10,7 @@ import { ActionContext, ActionText, Checkbox, chooseTheme, Dropdown, Menu, MenuD
import { useIsPopup, useTranslation } from '../hooks/index.js';
import { setNotification, windowOpen } from '../messaging.js';
import { styled } from '../styled.js';
import { DEFAULT_PREFIX } from '../util/defaultPrefix.js';
import getLanguageOptions from '../util/getLanguageOptions.js';
interface Option {
@@ -32,7 +33,7 @@ const prefixOptions = settings.availablePrefixes
function MenuSettings ({ className, reference }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const [camera, setCamera] = useState(settings.camera === 'on');
const [prefix, setPrefix] = useState(`${settings.prefix === -1 ? 42 : settings.prefix}`);
const [prefix, setPrefix] = useState(`${settings.prefix === -1 ? DEFAULT_PREFIX : settings.prefix}`);
const [notification, updateNotification] = useState(settings.notification);
const [theme, setTheme] = useState(chooseTheme());
const setThemeContext = useContext(ThemeSwitchContext);

View File

@@ -19,8 +19,12 @@ describe('decoding a call for the approval screen', (): void => {
throw new Error('the fixture metadata did not produce a runtime');
}
// crystal_bob on Heisenberg, as an account id and as the address it renders to.
const BOB_ID = '0x300bb607ba60e89461d2f9005668231ceb30237b33db53a614164b8590965519';
const BOB = 'qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y';
const transfer = u8aToHex(runtime.encodeCall('Balances', 'transfer_keep_alive', {
dest: { Id: `0x${'11'.repeat(32)}` },
dest: { Id: BOB_ID },
value: '1000000000'
}));
@@ -33,7 +37,9 @@ describe('decoding a call for the approval screen', (): void => {
expect(name).toEqual('Balances.transfer_keep_alive');
expect(args?.['value']).toEqual('1000000000');
expect(args?.['dest']).toEqual({ Id: `0x${'11'.repeat(32)}` });
// An address, not 32 bytes of hex — somebody approving a transfer has to be
// able to check the recipient against what they meant to send to.
expect(args?.['dest']).toEqual({ Id: BOB });
});
// Metadata from a different runtime decodes a call into something plausible and

View File

@@ -0,0 +1,18 @@
// Copyright 2019-2026 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
/**
* The SS58 prefix to display an address at when nothing else has decided.
*
* Upstream falls back to 42, the generic Substrate prefix, because it is a
* wallet for every Substrate chain and has no reason to prefer one. This one
* does: every account it can hold is a Quantus account, so 42 would show a
* correct address in a form no Quantus tool displays — the same account id, the
* same funds, an unfamiliar string. Somebody comparing the extension with
* `quantus-cli` or the mobile wallet would reasonably conclude they had created
* the wrong account.
*
* A chain's own `ss58Format` still wins where one is known; this is only the
* fallback, and the user can still override it in settings.
*/
export const DEFAULT_PREFIX = 189;

View File

@@ -982,7 +982,7 @@ __metadata:
"@polkadot/ui-settings": "npm:^3.16.7"
"@polkadot/util": "npm:^14.0.3"
"@polkadot/util-crypto": "npm:^14.0.3"
"@quantus/codec": "npm:^0.4.0"
"@quantus/codec": "npm:^0.5.0"
"@quantus/crypto": "npm:^0.1.1"
eventemitter3: "npm:^5.0.1"
rxjs: "npm:^7.8.1"
@@ -998,7 +998,7 @@ __metadata:
"@polkadot/networks": "npm:^14.0.3"
"@polkadot/util": "npm:^14.0.3"
"@polkadot/util-crypto": "npm:^14.0.3"
"@quantus/codec": "npm:^0.4.0"
"@quantus/codec": "npm:^0.5.0"
tslib: "npm:^2.8.1"
peerDependencies:
"@polkadot/api": "*"
@@ -1604,13 +1604,13 @@ __metadata:
languageName: node
linkType: hard
"@quantus/codec@npm:^0.4.0":
version: 0.4.0
resolution: "@quantus/codec@npm:0.4.0::__archiveUrl=https%3A%2F%2Fgit.lair.cafe%2Fapi%2Fpackages%2Fquantus%2Fnpm%2F%2540quantus%252Fcodec%2F-%2F0.4.0%2Fcodec-0.4.0.tgz"
"@quantus/codec@npm:^0.5.0":
version: 0.5.0
resolution: "@quantus/codec@npm:0.5.0::__archiveUrl=https%3A%2F%2Fgit.lair.cafe%2Fapi%2Fpackages%2Fquantus%2Fnpm%2F%2540quantus%252Fcodec%2F-%2F0.5.0%2Fcodec-0.5.0.tgz"
dependencies:
fflate: "npm:^0.8.2"
tslib: "npm:^2.7.0"
checksum: 10/9327551cf97a5bf0a54d5cf1ef9703efc94e85138f24346d597fe4860570d451a3301db0dc1f7a382bc760491ff3ea823cecf384d07e0559f766bafb9ec19b46
checksum: 10/248a2ae9650b1521d56aa3bf81c01084ae37474365df1f2dbece928b59647004c719bfb396ba1fccd33ec8e6e67c7e5e82660f9fbbcc2fcda41b5dbe6e3a38fb
languageName: node
linkType: hard