diff --git a/.changeset/fair-badgers-approve.md b/.changeset/fair-badgers-approve.md new file mode 100644 index 0000000000..a46b039fe6 --- /dev/null +++ b/.changeset/fair-badgers-approve.md @@ -0,0 +1,24 @@ +--- +'@reown/appkit-adapter-solana': patch +'@reown/appkit-siwx': patch +'@apps/demo': patch +'@apps/gallery': patch +'@apps/laboratory': patch +'@reown/appkit-adapter-ethers': patch +'@reown/appkit-adapter-ethers5': patch +'@reown/appkit-adapter-polkadot': patch +'@reown/appkit-adapter-wagmi': patch +'@reown/appkit': patch +'@reown/appkit-utils': patch +'@reown/appkit-cdn': patch +'@reown/appkit-common': patch +'@reown/appkit-core': patch +'@reown/appkit-experimental': patch +'@reown/appkit-polyfills': patch +'@reown/appkit-scaffold-ui': patch +'@reown/appkit-siwe': patch +'@reown/appkit-ui': patch +'@reown/appkit-wallet': patch +--- + +Adds SIWX Solana verifier diff --git a/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts index 085ef21ba4..e438180024 100644 --- a/packages/adapters/solana/src/client.ts +++ b/packages/adapters/solana/src/client.ts @@ -47,6 +47,7 @@ import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider' import { ConstantsUtil, ErrorUtil } from '@reown/appkit-utils' import { createSendTransaction } from './utils/createSendTransaction.js' import { CoinbaseWalletProvider } from './providers/CoinbaseWalletProvider.js' +import base58 from 'bs58' export interface AdapterOptions { connectionSettings?: Commitment | ConnectionConfig @@ -197,7 +198,7 @@ export class SolanaAdapter implements ChainAdapter { const signature = await provider.signMessage(new TextEncoder().encode(message)) - return new TextDecoder().decode(signature) + return base58.encode(signature) }, estimateGas: async params => { diff --git a/packages/siwx/package.json b/packages/siwx/package.json index 78ea0bf0cc..ef0347db1f 100644 --- a/packages/siwx/package.json +++ b/packages/siwx/package.json @@ -19,6 +19,8 @@ "dependencies": { "@reown/appkit-common": "workspace:*", "@reown/appkit-core": "workspace:*", + "bs58": "6.0.0", + "tweetnacl": "1.0.3", "viem": "2.21.34" }, "keywords": [ diff --git a/packages/siwx/src/configs/DefaultSIWX.ts b/packages/siwx/src/configs/DefaultSIWX.ts index 975f5a28b4..9905f4d332 100644 --- a/packages/siwx/src/configs/DefaultSIWX.ts +++ b/packages/siwx/src/configs/DefaultSIWX.ts @@ -1,7 +1,7 @@ import { SIWXConfig } from '../core/SIWXConfig.js' import { InformalMessenger } from '../messengers/index.js' import { LocalStorage } from '../storages/index.js' -import { EIP155Verifier } from '../verifiers/index.js' +import { EIP155Verifier, SolanaVerifier } from '../verifiers/index.js' const DEFAULTS = { getDefaultMessenger: () => @@ -11,7 +11,7 @@ const DEFAULTS = { getNonce: async () => Promise.resolve(Math.round(Math.random() * 10000).toString()) }), - getDefaultVerifiers: () => [new EIP155Verifier()], + getDefaultVerifiers: () => [new EIP155Verifier(), new SolanaVerifier()], getDefaultStorage: () => new LocalStorage({ key: '@appkit/siwx' }) } diff --git a/packages/siwx/src/core/SIWXVerifier.ts b/packages/siwx/src/core/SIWXVerifier.ts index 1481283c34..47cf4ce94c 100644 --- a/packages/siwx/src/core/SIWXVerifier.ts +++ b/packages/siwx/src/core/SIWXVerifier.ts @@ -3,13 +3,9 @@ import type { SIWXSession } from '@reown/appkit-core' export abstract class SIWXVerifier { public abstract readonly chainNamespace: ChainNamespace - public abstract readonly messageVersion: string public shouldVerify(session: SIWXSession): boolean { - return ( - session.message.version === this.messageVersion && - session.message.chainId.startsWith(this.chainNamespace) - ) + return session.message.chainId.startsWith(this.chainNamespace) } abstract verify(session: SIWXSession): Promise diff --git a/packages/siwx/src/verifiers/EIP155Verifier.ts b/packages/siwx/src/verifiers/EIP155Verifier.ts index 5e59927b4a..2d75f315e7 100644 --- a/packages/siwx/src/verifiers/EIP155Verifier.ts +++ b/packages/siwx/src/verifiers/EIP155Verifier.ts @@ -1,10 +1,10 @@ import type { SIWXSession } from '@reown/appkit-core' import { SIWXVerifier } from '../core/SIWXVerifier.js' import { verifyMessage } from 'viem' +import { ConstantsUtil } from '@reown/appkit-common' export class EIP155Verifier extends SIWXVerifier { - public readonly chainNamespace = 'eip155' - public readonly messageVersion = '1' + public readonly chainNamespace = ConstantsUtil.CHAIN.EVM public async verify(session: SIWXSession): Promise { try { diff --git a/packages/siwx/src/verifiers/SolanaVerifier.ts b/packages/siwx/src/verifiers/SolanaVerifier.ts new file mode 100644 index 0000000000..0af58925dd --- /dev/null +++ b/packages/siwx/src/verifiers/SolanaVerifier.ts @@ -0,0 +1,23 @@ +import { SIWXVerifier } from '../core/SIWXVerifier.js' +import type { SIWXSession } from '@reown/appkit-core' +import nacl from 'tweetnacl' +import bs58 from 'bs58' +import { ConstantsUtil } from '@reown/appkit-common' + +export class SolanaVerifier extends SIWXVerifier { + public readonly chainNamespace = ConstantsUtil.CHAIN.SOLANA + + public async verify(session: SIWXSession): Promise { + try { + const publicKey = bs58.decode(session.message.accountAddress) + const signature = bs58.decode(session.signature) + const message = new TextEncoder().encode(session.message.toString()) + + const isValid = nacl.sign.detached.verify(message, signature, publicKey) + + return Promise.resolve(isValid) + } catch (error) { + return Promise.resolve(false) + } + } +} diff --git a/packages/siwx/src/verifiers/index.ts b/packages/siwx/src/verifiers/index.ts index b8fc467b1b..ebd6749107 100644 --- a/packages/siwx/src/verifiers/index.ts +++ b/packages/siwx/src/verifiers/index.ts @@ -1 +1,2 @@ export * from './EIP155Verifier.js' +export * from './SolanaVerifier.js' diff --git a/packages/siwx/tests/verifiers/EIP155Verifier.test.ts b/packages/siwx/tests/verifiers/EIP155Verifier.test.ts index 6f66885f47..d44b5b1e80 100644 --- a/packages/siwx/tests/verifiers/EIP155Verifier.test.ts +++ b/packages/siwx/tests/verifiers/EIP155Verifier.test.ts @@ -40,6 +40,28 @@ describe('EIP155Verifier', () => { expect(verifier.chainNamespace).toBe('eip155') }) + test('should verify only eip155 chain id', () => { + expect( + verifier.shouldVerify( + mockSession({ + message: { + chainId: 'eip155:1' + } + }) + ) + ).toBe(true) + + expect( + verifier.shouldVerify( + mockSession({ + message: { + chainId: 'solana:mainnet' + } + }) + ) + ).toBe(false) + }) + test.each(cases)(`should verify $reason`, async ({ session, expected }) => { expect(await verifier.verify(session)).toBe(expected) }) diff --git a/packages/siwx/tests/verifiers/SolanaVerifier.test.ts b/packages/siwx/tests/verifiers/SolanaVerifier.test.ts new file mode 100644 index 0000000000..c0e79b9302 --- /dev/null +++ b/packages/siwx/tests/verifiers/SolanaVerifier.test.ts @@ -0,0 +1,80 @@ +import { describe, test, expect } from 'vitest' +import { type SIWXSession } from '@reown/appkit-core' +import { mockSession } from '../mocks/mockSession.js' +import { SolanaVerifier } from '../../src/verifiers/SolanaVerifier.js' + +type Case = { + reason: string + session: SIWXSession + expected: boolean +} + +const cases: Case[] = [ + { + reason: 'valid session', + session: mockSession({ + message: { + accountAddress: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgpU' + }, + signature: + '2ZpgpUKF6RtmbrE8uBmPwRiBqRnsCKiBKkjsPSpf6c64r4XdDoevjhjNX35X7GeuSwwRhmbB2Ro4NfHWAeXWNhDL' + }), + expected: true + }, + { + reason: 'invalid session with an invalid signature', + session: mockSession({ + message: { + accountAddress: '2VqKhjZ766ZN3uBtBpb7Ls3cN4HrocP1rzxzekhVEgpU' + }, + signature: + '3ErkFZkvhSJVR7E1uakGwj8icgfxvRSS6AwW5bq4CZsXPZ83XrT1H9xcCWLvhsYCLYzFc7WSMQEJxGgpZvtgqbdE' + }), + expected: false + }, + { + reason: 'invalid session with an invalid account address', + session: mockSession({ + message: { + accountAddress: 'C6ydkvKcRdXz3ZTEYy6uWAAyZgyUF49qP4XPdaDB2nqS' + }, + signature: + '2ZpgpUKF6RtmbrE8uBmPwRiBqRnsCKiBKkjsPSpf6c64r4XdDoevjhjNX35X7GeuSwwRhmbB2Ro4NfHWAeXWNhDL' + }), + expected: false + } +] + +describe('SolanaVerifier', () => { + const verifier = new SolanaVerifier() + + test('should have solana as the chain namespace', () => { + expect(verifier.chainNamespace).toBe('solana') + }) + + test('should verify only solana chain id', () => { + expect( + verifier.shouldVerify( + mockSession({ + message: { + chainId: 'solana:mainnet' + } + }) + ) + ).toBe(true) + + expect( + verifier.shouldVerify( + mockSession({ + message: { + chainId: 'eip155:1' + } + }) + ) + ).toBe(false) + }) + + test.each(cases)(`should verify $reason`, async ({ session, expected }) => { + expect(await verifier.verify(session)).toBe(expected) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b28ace07d..f1867b94e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1427,6 +1427,12 @@ importers: '@reown/appkit-core': specifier: workspace:* version: link:../core + bs58: + specifier: 6.0.0 + version: 6.0.0 + tweetnacl: + specifier: 1.0.3 + version: 1.0.3 viem: specifier: 2.21.34 version: 2.21.34(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@5.0.10)(zod@3.22.4) @@ -11948,6 +11954,9 @@ packages: resolution: {integrity: sha512-35bevmsAkogFCfzil46dsM9vk1QVhMC63vqcXSSgzkQerDVYjrdfl/eMrMoqxzJI8ZHZYIBHAsiHMK7TteOCGw==} hasBin: true + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -27655,6 +27664,8 @@ snapshots: turbo-windows-64: 2.0.6-canary.0 turbo-windows-arm64: 2.0.6-canary.0 + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1