From 75b0922bb584168205095058edf003210808f7dc Mon Sep 17 00:00:00 2001 From: h0ngcha0 Date: Wed, 25 Sep 2024 09:22:14 +0200 Subject: [PATCH] New parsing and formatting of chain and account --- packages/walletconnect/src/constants.ts | 1 + packages/walletconnect/src/provider.ts | 93 ++++++++++++++++--- .../walletconnect/test/walletconnect.test.ts | 38 ++++++-- 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/packages/walletconnect/src/constants.ts b/packages/walletconnect/src/constants.ts index 1d48d6039..e88c619cb 100644 --- a/packages/walletconnect/src/constants.ts +++ b/packages/walletconnect/src/constants.ts @@ -16,6 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ export const PROVIDER_NAMESPACE = 'alephium' +export const VALID_ADDRESS_GROUPS = [-1, 0, 1, 2, 3] // Note: // 1. the wallet client could potentially submit the signed transaction. diff --git a/packages/walletconnect/src/provider.ts b/packages/walletconnect/src/provider.ts index 8027aa071..20246f591 100644 --- a/packages/walletconnect/src/provider.ts +++ b/packages/walletconnect/src/provider.ts @@ -50,7 +50,14 @@ import { EnableOptionsBase } from '@alephium/web3' -import { ALEPHIUM_DEEP_LINK, LOGGER, PROVIDER_NAMESPACE, RELAY_METHODS, RELAY_URL } from './constants' +import { + ALEPHIUM_DEEP_LINK, + LOGGER, + PROVIDER_NAMESPACE, + RELAY_METHODS, + RELAY_URL, + VALID_ADDRESS_GROUPS +} from './constants' import { AddressGroup, RelayMethodParams, @@ -518,35 +525,81 @@ export function isCompatibleAddressGroup(group: number, expectedAddressGroup: Ad return expectedAddressGroup === undefined || expectedAddressGroup === group } -export function formatChain(networkId: NetworkId, addressGroup: AddressGroup): string { - if (addressGroup !== undefined && addressGroup < 0) { - throw Error('Address group in provider needs to be either undefined or non-negative') +export function parseChain(chainString: string): ChainInfo { + try { + const [namespace, _addressGroup, networkId] = chainString.replace(/_/g, ':').split(':') + if (namespace !== PROVIDER_NAMESPACE) { + throw Error(`Invalid namespace: expected ${PROVIDER_NAMESPACE}, but got ${namespace}`) + } + const addressGroup = parseInt(_addressGroup, 10) + validateAddressGroup(addressGroup) + + const networkIdList = networkIds as ReadonlyArray + if (!networkIdList.includes(networkId)) { + throw Error(`Invalid network id, expect one of ${networkIdList}`) + } + return { + networkId: networkId as NetworkId, + addressGroup: addressGroup === -1 ? undefined : addressGroup + } + } catch (error) { + console.debug('Failed to parse chain, falling back to legacy parsing', chainString) + return parseChainLegacy(chainString) } - const addressGroupEncoded = addressGroup !== undefined ? addressGroup : -1 - return `${PROVIDER_NAMESPACE}:${networkId}/${addressGroupEncoded}` } -export function parseChain(chainString: string): ChainInfo { - const [_namespace, networkId, addressGroup] = chainString.replace(/\//g, ':').split(':') - const addressGroupDecoded = parseInt(addressGroup, 10) - if (addressGroupDecoded < -1) { - throw Error('Address group in protocol needs to be either -1 or non-negative') - } +export function parseChainLegacy(chainString: string): ChainInfo { + const [_namespace, networkId, _addressGroup] = chainString.replace(/\//g, ':').split(':') + const addressGroup = parseInt(_addressGroup, 10) + validateAddressGroup(addressGroup) + const networkIdList = networkIds as ReadonlyArray if (!networkIdList.includes(networkId)) { throw Error(`Invalid network id, expect one of ${networkIdList}`) } return { networkId: networkId as NetworkId, - addressGroup: addressGroupDecoded === -1 ? undefined : addressGroupDecoded + addressGroup: addressGroup === -1 ? undefined : addressGroup } } +export function formatChain(networkId: NetworkId, addressGroup: AddressGroup): string { + const addressGroupNumber = toAddressGroupNumber(addressGroup) + return `${PROVIDER_NAMESPACE}:${addressGroupNumber}_${networkId}` +} + +export function formatChainLegacy(networkId: NetworkId, addressGroup: AddressGroup): string { + if (addressGroup !== undefined && addressGroup < 0) { + throw Error('Address group in provider needs to be either undefined or non-negative') + } + const addressGroupNumber = toAddressGroupNumber(addressGroup) + return `${PROVIDER_NAMESPACE}:${networkId}/${addressGroupNumber}` +} + export function formatAccount(permittedChain: string, account: Account): string { + return `${permittedChain}:${account.publicKey}_${account.keyType}` +} + +export function formatAccountLegacy(permittedChain: string, account: Account): string { return `${permittedChain}:${account.publicKey}/${account.keyType}` } -export function parseAccount(account: string): Account & { networkId: NetworkId } { +export function parseAccount(accountString: string): Account & { networkId: NetworkId } { + try { + const [_namespace, _group, networkId, publicKey, keyType] = accountString.replace(/_/g, ':').split(':') + const address = addressFromPublicKey(publicKey) + const group = groupOfAddress(address) + if (keyType !== 'default' && keyType !== 'bip340-schnorr') { + throw Error(`Invalid key type: ${keyType}`) + } + return { address, group, publicKey, keyType, networkId: networkId as NetworkId } + } catch (error) { + console.debug(`Failed to parse account ${accountString}, falling back to legacy parsing`) + return parseAccountLegacy(accountString) + } +} + +export function parseAccountLegacy(account: string): Account & { networkId: NetworkId } { const [_namespace, networkId, _group, publicKey, keyType] = account.replace(/\//g, ':').split(':') const address = addressFromPublicKey(publicKey) const group = groupOfAddress(address) @@ -577,3 +630,15 @@ function RateLimit(rps: number) { setTimeout(() => sema.release(), delay) } } + +function toAddressGroupNumber(addressGroup: AddressGroup): number { + const groupNumber = addressGroup !== undefined ? addressGroup : -1 + validateAddressGroup(groupNumber) + return groupNumber +} + +function validateAddressGroup(addressGroup: number) { + if (!VALID_ADDRESS_GROUPS.includes(addressGroup)) { + throw Error('Address group must be -1 (for any groups) or between 0 and 3 (inclusive)') + } +} diff --git a/packages/walletconnect/test/walletconnect.test.ts b/packages/walletconnect/test/walletconnect.test.ts index 41711db29..5ee94c578 100644 --- a/packages/walletconnect/test/walletconnect.test.ts +++ b/packages/walletconnect/test/walletconnect.test.ts @@ -15,7 +15,14 @@ GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the library. If not, see . */ -import { formatChain, parseChain, ProviderOptions, WalletConnectProvider } from '../src/index' +import { + parseChain, + formatChain, + formatChainLegacy, + parseChainLegacy, + ProviderOptions, + WalletConnectProvider +} from '../src/index' import { WalletClient } from './shared' import { web3, node, NodeProvider, verifySignedMessage, groupOfAddress, NetworkId } from '@alephium/web3' import { PrivateKeyWallet } from '@alephium/web3-wallet' @@ -112,15 +119,30 @@ describe('Unit tests', function () { const expectedAddressGroup0 = 2 const expectedAddressGroup1 = 1 + it('test formatChainLegacy & parseChainLegacy', () => { + expect(formatChainLegacy('devnet', expectedAddressGroup0)).toEqual('alephium:devnet/2') + expect(formatChainLegacy('devnet', expectedAddressGroup1)).toEqual('alephium:devnet/1') + expect(formatChainLegacy('devnet', undefined)).toEqual('alephium:devnet/-1') + expect(() => formatChainLegacy('devnet', -1)).toThrow() + expect(parseChainLegacy('alephium:devnet/2')).toEqual({ networkId: 'devnet', addressGroup: 2 }) + expect(parseChainLegacy('alephium:devnet/1')).toEqual({ networkId: 'devnet', addressGroup: 1 }) + expect(parseChainLegacy('alephium:devnet/-1')).toEqual({ networkId: 'devnet', addressGroup: undefined }) + expect(() => parseChainLegacy('alephium:devnet/-2')).toThrow() + }) + it('test formatChain & parseChain', () => { - expect(formatChain('devnet', expectedAddressGroup0)).toEqual('alephium:devnet/2') - expect(formatChain('devnet', expectedAddressGroup1)).toEqual('alephium:devnet/1') - expect(formatChain('devnet', undefined)).toEqual('alephium:devnet/-1') - expect(() => formatChain('devnet', -1)).toThrow() + expect(formatChain('devnet', expectedAddressGroup0)).toEqual('alephium:2_devnet') + expect(formatChain('devnet', expectedAddressGroup1)).toEqual('alephium:1_devnet') + expect(formatChain('devnet', undefined)).toEqual('alephium:-1_devnet') + expect(() => formatChain('devnet', -2)).toThrow() expect(parseChain('alephium:devnet/2')).toEqual({ networkId: 'devnet', addressGroup: 2 }) expect(parseChain('alephium:devnet/1')).toEqual({ networkId: 'devnet', addressGroup: 1 }) expect(parseChain('alephium:devnet/-1')).toEqual({ networkId: 'devnet', addressGroup: undefined }) + expect(parseChain('alephium:2_devnet')).toEqual({ networkId: 'devnet', addressGroup: 2 }) + expect(parseChain('alephium:1_devnet')).toEqual({ networkId: 'devnet', addressGroup: 1 }) + expect(parseChain('alephium:-1_devnet')).toEqual({ networkId: 'devnet', addressGroup: undefined }) expect(() => parseChain('alephium:devnet/-2')).toThrow() + expect(() => parseChain('alephium:-2_devnet')).toThrow() }) it('should initialize providers', async () => { @@ -153,7 +175,7 @@ describe('WalletConnectProvider with single addressGroup', function () { walletAddress = walletClient.signer.address expect(walletAddress).toEqual(ACCOUNTS.a.address) await provider.connect() - expect(provider.permittedChain).toEqual('alephium:devnet/0') + expect(provider.permittedChain).toEqual('alephium:0_devnet') const selectetAddress = (await provider.getSelectedAccount()).address expect(selectetAddress).toEqual(signerA.address) await waitWalletConnected(walletClient) @@ -196,7 +218,7 @@ describe('WalletConnectProvider with single addressGroup', function () { // change to account b, which is not supported expectThrowsAsync( async () => await walletClient.changeAccount(ACCOUNTS.b.privateKey), - 'Error changing account, chain alephium:devnet/1 not permitted' + 'Error changing account, chain alephium:1_devnet not permitted' ) }) @@ -221,7 +243,7 @@ describe('WalletConnectProvider with arbitrary addressGroup', function () { walletAddress = walletClient.signer.address expect(walletAddress).toEqual(ACCOUNTS.a.address) await provider.connect() - expect(provider.permittedChain).toEqual('alephium:devnet/-1') + expect(provider.permittedChain).toEqual('alephium:-1_devnet') const selectedAddress = (await provider.getSelectedAccount()).address expect(selectedAddress).toEqual(signerA.address) await waitWalletConnected(walletClient)