Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/walletconnect/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/
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.
Expand Down
93 changes: 79 additions & 14 deletions packages/walletconnect/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string>
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<string>
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)
Expand Down Expand Up @@ -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)')
}
}
38 changes: 30 additions & 8 deletions packages/walletconnect/test/walletconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://www.gnu.org/licenses/>.
*/
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'
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
)
})

Expand All @@ -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)
Expand Down
Loading