@meshconnect/uwc-wallet-connect-connector 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/services/connection-service.d.ts +60 -0
  6. package/dist/services/connection-service.d.ts.map +1 -0
  7. package/dist/services/connection-service.js +235 -0
  8. package/dist/services/connection-service.js.map +1 -0
  9. package/dist/services/network-switch-service.d.ts +16 -0
  10. package/dist/services/network-switch-service.d.ts.map +1 -0
  11. package/dist/services/network-switch-service.js +71 -0
  12. package/dist/services/network-switch-service.js.map +1 -0
  13. package/dist/services/signature-service.d.ts +15 -0
  14. package/dist/services/signature-service.d.ts.map +1 -0
  15. package/dist/services/signature-service.js +102 -0
  16. package/dist/services/signature-service.js.map +1 -0
  17. package/dist/services/transaction-service.d.ts +33 -0
  18. package/dist/services/transaction-service.d.ts.map +1 -0
  19. package/dist/services/transaction-service.js +287 -0
  20. package/dist/services/transaction-service.js.map +1 -0
  21. package/dist/services/walletconnect-client.d.ts +28 -0
  22. package/dist/services/walletconnect-client.d.ts.map +1 -0
  23. package/dist/services/walletconnect-client.js +92 -0
  24. package/dist/services/walletconnect-client.js.map +1 -0
  25. package/dist/utils/account-utils.d.ts +41 -0
  26. package/dist/utils/account-utils.d.ts.map +1 -0
  27. package/dist/utils/account-utils.js +81 -0
  28. package/dist/utils/account-utils.js.map +1 -0
  29. package/dist/utils/encoding-utils.d.ts +6 -0
  30. package/dist/utils/encoding-utils.d.ts.map +1 -0
  31. package/dist/utils/encoding-utils.js +27 -0
  32. package/dist/utils/encoding-utils.js.map +1 -0
  33. package/dist/utils/error-utils.d.ts +6 -0
  34. package/dist/utils/error-utils.d.ts.map +1 -0
  35. package/dist/utils/error-utils.js +55 -0
  36. package/dist/utils/error-utils.js.map +1 -0
  37. package/dist/utils/solana-transactions.d.ts +4 -0
  38. package/dist/utils/solana-transactions.d.ts.map +1 -0
  39. package/dist/utils/solana-transactions.js +15 -0
  40. package/dist/utils/solana-transactions.js.map +1 -0
  41. package/dist/wallet-connect-connector.d.ts +17 -0
  42. package/dist/wallet-connect-connector.d.ts.map +1 -0
  43. package/dist/wallet-connect-connector.js +47 -0
  44. package/dist/wallet-connect-connector.js.map +1 -0
  45. package/package.json +41 -0
  46. package/src/index.ts +1 -0
  47. package/src/services/connection-service.ts +304 -0
  48. package/src/services/network-switch-service.ts +83 -0
  49. package/src/services/signature-service.ts +128 -0
  50. package/src/services/transaction-service.ts +397 -0
  51. package/src/services/walletconnect-client.ts +103 -0
  52. package/src/utils/account-utils.ts +116 -0
  53. package/src/utils/encoding-utils.ts +32 -0
  54. package/src/utils/error-utils.ts +62 -0
  55. package/src/utils/solana-transactions.ts +24 -0
  56. package/src/wallet-connect-connector.ts +83 -0
@@ -0,0 +1,128 @@
1
+ import { WalletConnectClient } from './walletconnect-client'
2
+ import { ConnectionService } from './connection-service'
3
+ import { findAccountInNamespaces, parseAccount } from '../utils/account-utils'
4
+ import { base58Encode } from '../utils/encoding-utils'
5
+ import { parseError } from '../utils/error-utils'
6
+
7
+ /**
8
+ * Service for handling message signing in WalletConnect
9
+ */
10
+ export class SignatureService {
11
+ constructor(
12
+ private walletConnectClient: WalletConnectClient,
13
+ private connectionService: ConnectionService
14
+ ) {}
15
+
16
+ /**
17
+ * Sign a message with the connected wallet
18
+ */
19
+ async signMessage(message: string): Promise<string> {
20
+ try {
21
+ const session = this.connectionService.getSession()
22
+ if (!session) {
23
+ throw new Error('No active session. Please connect first.')
24
+ }
25
+
26
+ const activeNetwork = this.connectionService.getActiveNetwork()
27
+ const activeAddress = this.connectionService.getActiveAddress()
28
+
29
+ if (!activeNetwork) {
30
+ throw new Error('No active network. Please connect first.')
31
+ }
32
+
33
+ if (!activeAddress) {
34
+ throw new Error('No active address. Please connect first.')
35
+ }
36
+
37
+ const client = await this.walletConnectClient.getClient()
38
+
39
+ // Find the account that matches the active network and address
40
+ const targetAccount = findAccountInNamespaces(
41
+ session.namespaces,
42
+ activeNetwork.id,
43
+ activeAddress
44
+ )
45
+
46
+ if (!targetAccount) {
47
+ throw new Error(
48
+ `No account found for network ${activeNetwork.id} and address ${activeAddress}`
49
+ )
50
+ }
51
+
52
+ const {
53
+ namespace: accountNamespace,
54
+ chainId,
55
+ address
56
+ } = parseAccount(targetAccount)
57
+
58
+ // Determine the signing method based on namespace
59
+ let method: string
60
+ let params: unknown[] | unknown
61
+
62
+ switch (accountNamespace) {
63
+ case 'eip155':
64
+ method = 'personal_sign'
65
+ // For personal_sign: [message, address]
66
+ params = [message, address]
67
+ break
68
+
69
+ case 'solana': {
70
+ method = 'solana_signMessage'
71
+ // For Solana: message needs to be base58 encoded
72
+ const base58Message = base58Encode(message)
73
+
74
+ // For Solana, params is an object, not an array
75
+ params = {
76
+ message: base58Message,
77
+ pubkey: address
78
+ }
79
+ break
80
+ }
81
+
82
+ case 'bip122':
83
+ method = 'signMessage'
84
+ params = [
85
+ {
86
+ message,
87
+ address
88
+ }
89
+ ]
90
+ break
91
+
92
+ case 'tron':
93
+ method = 'tron_signMessage'
94
+ params = [
95
+ {
96
+ message,
97
+ address
98
+ }
99
+ ]
100
+ break
101
+
102
+ default:
103
+ throw new Error(
104
+ `Signing not supported for namespace: ${accountNamespace}`
105
+ )
106
+ }
107
+ // Make the signing request
108
+ const result = await client.request({
109
+ topic: session.topic,
110
+ chainId: `${accountNamespace}:${chainId}`,
111
+ request: {
112
+ method,
113
+ params
114
+ }
115
+ })
116
+
117
+ // Handle different response formats
118
+ if (accountNamespace === 'solana') {
119
+ // Solana returns an object with a signature property
120
+ return (result as { signature: string }).signature
121
+ }
122
+
123
+ return result as string
124
+ } catch (error) {
125
+ throw parseError(error)
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,397 @@
1
+ import { ethers } from 'ethers'
2
+ import { Buffer } from 'buffer'
3
+ import {
4
+ Connection,
5
+ PublicKey,
6
+ Transaction,
7
+ VersionedTransaction
8
+ } from '@solana/web3.js'
9
+ import type {
10
+ TransactionRequest,
11
+ TransactionResult,
12
+ EVMNativeTransferRequest,
13
+ EVMContractCallRequest,
14
+ SolanaNativeTransferRequest,
15
+ NetworkId,
16
+ NetworkRpcMap
17
+ } from '@meshconnect/uwc-types'
18
+ import type SignClient from '@walletconnect/sign-client'
19
+ import { WalletConnectClient } from './walletconnect-client'
20
+ import { ConnectionService } from './connection-service'
21
+ import { getChainIdFromNetworkId } from '../utils/account-utils'
22
+ import { parseError } from '../utils/error-utils'
23
+ import { getSolanaTransaction } from '../utils/solana-transactions'
24
+ import type { SessionTypes } from '@walletconnect/types'
25
+ import base58 from 'bs58'
26
+ import { standardizeSignature } from '../utils/encoding-utils'
27
+
28
+ /**
29
+ * Service for handling transactions in WalletConnect
30
+ */
31
+ export class TransactionService {
32
+ constructor(
33
+ private walletConnectClient: WalletConnectClient,
34
+ private connectionService: ConnectionService,
35
+ private networkRpcMap: NetworkRpcMap = {}
36
+ ) {}
37
+
38
+ /**
39
+ * Send a transaction with the connected wallet
40
+ */
41
+ async sendTransaction(
42
+ request: TransactionRequest
43
+ ): Promise<TransactionResult> {
44
+ const session = this.connectionService.getSession()
45
+ if (!session) {
46
+ throw new Error('No active session. Please connect first.')
47
+ }
48
+
49
+ const activeNetwork = this.connectionService.getActiveNetwork()
50
+ const activeAddress = this.connectionService.getActiveAddress()
51
+
52
+ if (!activeNetwork) {
53
+ throw new Error('No active network. Please connect first.')
54
+ }
55
+
56
+ if (!activeAddress) {
57
+ throw new Error('No active address. Please connect first.')
58
+ }
59
+
60
+ const client = await this.walletConnectClient.getClient()
61
+
62
+ // Determine namespace from network ID
63
+ const namespace = activeNetwork.id.split(':')[0]
64
+
65
+ if (namespace === 'eip155') {
66
+ return this.sendEthereumTransaction(
67
+ client,
68
+ session,
69
+ request,
70
+ activeNetwork.id,
71
+ activeAddress
72
+ )
73
+ } else if (namespace === 'solana') {
74
+ return this.sendSolanaTransaction(
75
+ client,
76
+ session,
77
+ request as SolanaNativeTransferRequest,
78
+ activeNetwork.id,
79
+ activeAddress
80
+ )
81
+ } else {
82
+ throw new Error(`Unsupported namespace: ${namespace}`)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Send an Ethereum transaction
88
+ */
89
+ private async sendEthereumTransaction(
90
+ client: SignClient,
91
+ session: SessionTypes.Struct,
92
+ request: TransactionRequest,
93
+ activeNetworkId: string,
94
+ activeAddress: string
95
+ ): Promise<string> {
96
+ try {
97
+ // Handle different types of EVM transactions
98
+ if ('contractAddress' in request && 'abi' in request) {
99
+ // Contract call
100
+ return await this.sendContractCall(
101
+ client,
102
+ session,
103
+ request as EVMContractCallRequest,
104
+ activeNetworkId,
105
+ activeAddress
106
+ )
107
+ } else if ('calls' in request) {
108
+ // Batch transaction - not yet supported
109
+ throw new Error(
110
+ 'Batch transactions are not yet supported for WalletConnect'
111
+ )
112
+ } else if ('amount' in request && typeof request.amount === 'bigint') {
113
+ // Native transfer
114
+ return await this.sendNativeTransfer(
115
+ client,
116
+ session,
117
+ request as EVMNativeTransferRequest,
118
+ activeNetworkId,
119
+ activeAddress
120
+ )
121
+ } else {
122
+ throw new Error('Invalid transaction request type')
123
+ }
124
+ } catch (error) {
125
+ throw parseError(error)
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Send a Solana transaction
131
+ */
132
+ private async sendSolanaTransaction(
133
+ client: SignClient,
134
+ session: SessionTypes.Struct,
135
+ request: SolanaNativeTransferRequest,
136
+ activeNetworkId: string,
137
+ activeAddress: string
138
+ ): Promise<string> {
139
+ try {
140
+ // Verify the from address matches active address
141
+ if (request.from !== activeAddress) {
142
+ throw new Error('From address does not match connected wallet')
143
+ }
144
+
145
+ // Blockhash is required
146
+ if (!request.blockhash) {
147
+ throw new Error('Blockhash is required for Solana transactions')
148
+ }
149
+
150
+ // Create the transaction using the utility function
151
+ let transaction: Transaction | VersionedTransaction =
152
+ getSolanaTransaction(request)
153
+
154
+ // Serialize the transaction for WalletConnect
155
+ const serializedTransaction = transaction
156
+ .serialize({
157
+ verifySignatures: false
158
+ })
159
+ .toString('base64')
160
+
161
+ // Get the chain ID (e.g., "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" for mainnet)
162
+ const chainId = activeNetworkId
163
+
164
+ try {
165
+ // First try signAndSendTransaction (preferred method)
166
+ const result = await client.request({
167
+ topic: session.topic,
168
+ chainId,
169
+ request: {
170
+ method: 'solana_signAndSendTransaction',
171
+ params: {
172
+ transaction: serializedTransaction
173
+ }
174
+ }
175
+ })
176
+
177
+ // WalletConnect returns the signature directly for Solana
178
+ return result as string
179
+ } catch (signAndSendError: unknown) {
180
+ const errorMessage =
181
+ signAndSendError instanceof Error
182
+ ? signAndSendError.message
183
+ : String(signAndSendError)
184
+ // If signAndSendTransaction is not supported, fallback to signTransaction
185
+ if (
186
+ errorMessage.includes('solana_signAndSendTransaction') ||
187
+ errorMessage.includes('Missing or invalid')
188
+ ) {
189
+ // Sign the transaction only
190
+ const result: { transaction: string } | { signature: string } =
191
+ await client.request({
192
+ topic: session.topic,
193
+ chainId,
194
+ request: {
195
+ method: 'solana_signTransaction',
196
+ params: {
197
+ transaction: serializedTransaction
198
+ }
199
+ }
200
+ })
201
+
202
+ if ('signature' in result) {
203
+ const decoded = base58.decode(result.signature)
204
+ transaction.addSignature(
205
+ new PublicKey(request.from),
206
+ Buffer.from(decoded) as Buffer & Uint8Array
207
+ )
208
+ }
209
+
210
+ if ('transaction' in result) {
211
+ const decodedTransaction = Buffer.from(result.transaction, 'base64')
212
+
213
+ if (transaction instanceof VersionedTransaction) {
214
+ transaction = VersionedTransaction.deserialize(
215
+ new Uint8Array(decodedTransaction)
216
+ )
217
+ } else {
218
+ transaction = Transaction.from(decodedTransaction)
219
+ }
220
+ }
221
+
222
+ // Get RPC URL from the network RPC map or fall back to public endpoint
223
+ const rpcUrl =
224
+ this.networkRpcMap[activeNetworkId as NetworkId] ||
225
+ 'https://api.mainnet-beta.solana.com'
226
+ const connection = new Connection(rpcUrl)
227
+ const rawSignature = await connection.sendRawTransaction(
228
+ transaction.serialize(),
229
+ {
230
+ skipPreflight: false,
231
+ maxRetries: 3,
232
+ preflightCommitment: 'confirmed'
233
+ }
234
+ )
235
+
236
+ return standardizeSignature(rawSignature)
237
+ }
238
+
239
+ // If it's a different error, throw it
240
+ throw signAndSendError
241
+ }
242
+ } catch (error) {
243
+ throw parseError(error)
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Send native token transfer (ETH, BNB, MATIC, etc.)
249
+ */
250
+ private async sendNativeTransfer(
251
+ client: SignClient,
252
+ session: SessionTypes.Struct,
253
+ request: EVMNativeTransferRequest,
254
+ activeNetworkId: string,
255
+ activeAddress: string
256
+ ): Promise<string> {
257
+ try {
258
+ // Use the active network chainId
259
+ const chainId = getChainIdFromNetworkId(activeNetworkId as NetworkId)
260
+
261
+ // Build transaction using ethers
262
+ const tx: ethers.TransactionRequest = {
263
+ from: request.from || activeAddress,
264
+ to: request.to,
265
+ value: request.amount
266
+ }
267
+
268
+ // Add gas configuration if provided
269
+ if (request.gasConfig?.gasLimit) {
270
+ tx.gasLimit = BigInt(Math.floor(request.gasConfig.gasLimit))
271
+ }
272
+ if (request.gasConfig?.maxFeePerGas) {
273
+ tx.maxFeePerGas = BigInt(Math.floor(request.gasConfig.maxFeePerGas))
274
+ }
275
+ if (request.gasConfig?.maxPriorityFeePerGas) {
276
+ tx.maxPriorityFeePerGas = BigInt(
277
+ Math.floor(request.gasConfig.maxPriorityFeePerGas)
278
+ )
279
+ }
280
+
281
+ // Convert to hex format for WalletConnect
282
+ const txParams: Record<string, string> = {
283
+ from: tx.from as string,
284
+ to: tx.to as string,
285
+ value: '0x' + (tx.value ? BigInt(tx.value).toString(16) : '0'),
286
+ data: '0x' // Empty data for native transfers
287
+ }
288
+
289
+ // Add gas params if present
290
+ if (tx.gasLimit) {
291
+ txParams['gasLimit'] = '0x' + tx.gasLimit.toString(16)
292
+ }
293
+ if (tx.maxFeePerGas) {
294
+ txParams['maxFeePerGas'] = '0x' + tx.maxFeePerGas.toString(16)
295
+ }
296
+ if (tx.maxPriorityFeePerGas) {
297
+ txParams['maxPriorityFeePerGas'] =
298
+ '0x' + tx.maxPriorityFeePerGas.toString(16)
299
+ }
300
+
301
+ const hash = await client.request({
302
+ topic: session.topic,
303
+ chainId: `eip155:${chainId}`,
304
+ request: {
305
+ method: 'eth_sendTransaction',
306
+ params: [txParams]
307
+ }
308
+ })
309
+
310
+ return hash as string
311
+ } catch (error) {
312
+ throw parseError(error)
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Send smart contract transaction (including ERC-20 transfers)
318
+ */
319
+ private async sendContractCall(
320
+ client: SignClient,
321
+ session: SessionTypes.Struct,
322
+ request: EVMContractCallRequest,
323
+ activeNetworkId: string,
324
+ activeAddress: string
325
+ ): Promise<string> {
326
+ try {
327
+ // Use the active network chainId
328
+ const chainId = getChainIdFromNetworkId(activeNetworkId as NetworkId)
329
+
330
+ // Encode the contract call data using ethers
331
+ const iface = new ethers.Interface(request.abi)
332
+ const data = iface.encodeFunctionData(request.functionName, request.args)
333
+
334
+ // Build transaction using ethers
335
+ const tx: ethers.TransactionRequest = {
336
+ from: request.from || activeAddress,
337
+ to: request.contractAddress,
338
+ data
339
+ }
340
+
341
+ // Add value for payable functions
342
+ if (request.value) {
343
+ tx.value = BigInt(request.value)
344
+ }
345
+
346
+ // Add gas configuration if provided
347
+ if (request.gasConfig?.gasLimit) {
348
+ tx.gasLimit = BigInt(Math.floor(request.gasConfig.gasLimit))
349
+ }
350
+ if (request.gasConfig?.maxFeePerGas) {
351
+ tx.maxFeePerGas = BigInt(Math.floor(request.gasConfig.maxFeePerGas))
352
+ }
353
+ if (request.gasConfig?.maxPriorityFeePerGas) {
354
+ tx.maxPriorityFeePerGas = BigInt(
355
+ Math.floor(request.gasConfig.maxPriorityFeePerGas)
356
+ )
357
+ }
358
+
359
+ // Convert to hex format for WalletConnect
360
+ const txParams: Record<string, string> = {
361
+ from: tx.from as string,
362
+ to: tx.to as string,
363
+ data: tx.data as string
364
+ }
365
+
366
+ // Add value if present
367
+ if (tx.value) {
368
+ txParams['value'] = '0x' + BigInt(tx.value).toString(16)
369
+ }
370
+
371
+ // Add gas params if present
372
+ if (tx.gasLimit) {
373
+ txParams['gasLimit'] = '0x' + tx.gasLimit.toString(16)
374
+ }
375
+ if (tx.maxFeePerGas) {
376
+ txParams['maxFeePerGas'] = '0x' + tx.maxFeePerGas.toString(16)
377
+ }
378
+ if (tx.maxPriorityFeePerGas) {
379
+ txParams['maxPriorityFeePerGas'] =
380
+ '0x' + tx.maxPriorityFeePerGas.toString(16)
381
+ }
382
+
383
+ const hash = await client.request({
384
+ topic: session.topic,
385
+ chainId: `eip155:${chainId}`,
386
+ request: {
387
+ method: 'eth_sendTransaction',
388
+ params: [txParams]
389
+ }
390
+ })
391
+
392
+ return hash as string
393
+ } catch (error) {
394
+ throw parseError(error)
395
+ }
396
+ }
397
+ }
@@ -0,0 +1,103 @@
1
+ import SignClient from '@walletconnect/sign-client'
2
+ import type { WalletConnectConfig } from '@meshconnect/uwc-types'
3
+
4
+ /**
5
+ * Service for managing WalletConnect client instance
6
+ */
7
+ export class WalletConnectClient {
8
+ private client: SignClient | null = null
9
+ private initPromise: Promise<SignClient> | null = null
10
+
11
+ constructor(private config: WalletConnectConfig) {}
12
+
13
+ /**
14
+ * Initialize or get the WalletConnect client
15
+ */
16
+ async getClient(): Promise<SignClient> {
17
+ // If already initialized, return the client
18
+ if (this.client) {
19
+ return this.client
20
+ }
21
+
22
+ // If initialization is in progress, wait for it
23
+ if (this.initPromise) {
24
+ return this.initPromise
25
+ }
26
+
27
+ // Start initialization
28
+ this.initPromise = this.initClient()
29
+ this.client = await this.initPromise
30
+ this.initPromise = null
31
+
32
+ return this.client
33
+ }
34
+
35
+ /**
36
+ * Initialize the WalletConnect client
37
+ */
38
+ private async initClient(): Promise<SignClient> {
39
+ if (!this.config.projectId) {
40
+ throw new Error('WalletConnect projectId is required')
41
+ }
42
+
43
+ let url = this.config.metadata?.url || ''
44
+ if (!url && typeof globalThis !== 'undefined') {
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ const global = globalThis as any
47
+ if (global.location?.origin) {
48
+ url = global.location.origin
49
+ }
50
+ }
51
+
52
+ const metadata = {
53
+ name: this.config.metadata?.name || 'Universal Wallet Connector',
54
+ description: this.config.metadata?.description || 'Connect your wallet',
55
+ url,
56
+ icons: this.config.metadata?.icons || []
57
+ }
58
+
59
+ const client = await SignClient.init({
60
+ projectId: this.config.projectId,
61
+ metadata
62
+ })
63
+
64
+ return client
65
+ }
66
+
67
+ /**
68
+ * Disconnect all sessions
69
+ */
70
+ async disconnectAll(): Promise<void> {
71
+ const client = await this.getClient()
72
+ const sessions = client.session.getAll()
73
+
74
+ for (const session of sessions) {
75
+ try {
76
+ await client.disconnect({
77
+ topic: session.topic,
78
+ reason: {
79
+ code: 6000,
80
+ message: 'User disconnected'
81
+ }
82
+ })
83
+ } catch {
84
+ // Silently ignore disconnect errors
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Disconnect a specific session
91
+ */
92
+ async disconnect(topic: string): Promise<void> {
93
+ const client = await this.getClient()
94
+
95
+ await client.disconnect({
96
+ topic,
97
+ reason: {
98
+ code: 6000,
99
+ message: 'User disconnected'
100
+ }
101
+ })
102
+ }
103
+ }