toss-expo-sdk 0.1.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 (116) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +292 -0
  3. package/lib/module/ble.js +103 -0
  4. package/lib/module/ble.js.map +1 -0
  5. package/lib/module/client/TossClient.js +324 -0
  6. package/lib/module/client/TossClient.js.map +1 -0
  7. package/lib/module/client/index.js +4 -0
  8. package/lib/module/client/index.js.map +1 -0
  9. package/lib/module/contexts/WalletContext.js +99 -0
  10. package/lib/module/contexts/WalletContext.js.map +1 -0
  11. package/lib/module/discovery.js +434 -0
  12. package/lib/module/discovery.js.map +1 -0
  13. package/lib/module/errors.js +47 -0
  14. package/lib/module/errors.js.map +1 -0
  15. package/lib/module/examples/offlinePaymentFlow.js +234 -0
  16. package/lib/module/examples/offlinePaymentFlow.js.map +1 -0
  17. package/lib/module/index.js +32 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/module/intent.js +223 -0
  20. package/lib/module/intent.js.map +1 -0
  21. package/lib/module/intentManager.js +145 -0
  22. package/lib/module/intentManager.js.map +1 -0
  23. package/lib/module/internal/arciumHelper.js +50 -0
  24. package/lib/module/internal/arciumHelper.js.map +1 -0
  25. package/lib/module/nfc.js +54 -0
  26. package/lib/module/nfc.js.map +1 -0
  27. package/lib/module/noise.js +14 -0
  28. package/lib/module/noise.js.map +1 -0
  29. package/lib/module/package.json +1 -0
  30. package/lib/module/qr.js +57 -0
  31. package/lib/module/qr.js.map +1 -0
  32. package/lib/module/reconciliation.js +329 -0
  33. package/lib/module/reconciliation.js.map +1 -0
  34. package/lib/module/services/authService.js +205 -0
  35. package/lib/module/services/authService.js.map +1 -0
  36. package/lib/module/storage/secureStorage.js +89 -0
  37. package/lib/module/storage/secureStorage.js.map +1 -0
  38. package/lib/module/storage.js +16 -0
  39. package/lib/module/storage.js.map +1 -0
  40. package/lib/module/sync.js +64 -0
  41. package/lib/module/sync.js.map +1 -0
  42. package/lib/module/types/tossUser.js +41 -0
  43. package/lib/module/types/tossUser.js.map +1 -0
  44. package/lib/module/utils/nonceUtils.js +38 -0
  45. package/lib/module/utils/nonceUtils.js.map +1 -0
  46. package/lib/typescript/package.json +1 -0
  47. package/lib/typescript/src/__tests__/index.test.d.ts +1 -0
  48. package/lib/typescript/src/__tests__/index.test.d.ts.map +1 -0
  49. package/lib/typescript/src/__tests__/reconciliation.test.d.ts +6 -0
  50. package/lib/typescript/src/__tests__/reconciliation.test.d.ts.map +1 -0
  51. package/lib/typescript/src/ble.d.ts +10 -0
  52. package/lib/typescript/src/ble.d.ts.map +1 -0
  53. package/lib/typescript/src/client/TossClient.d.ts +110 -0
  54. package/lib/typescript/src/client/TossClient.d.ts.map +1 -0
  55. package/lib/typescript/src/client/index.d.ts +3 -0
  56. package/lib/typescript/src/client/index.d.ts.map +1 -0
  57. package/lib/typescript/src/contexts/WalletContext.d.ts +20 -0
  58. package/lib/typescript/src/contexts/WalletContext.d.ts.map +1 -0
  59. package/lib/typescript/src/discovery.d.ts +188 -0
  60. package/lib/typescript/src/discovery.d.ts.map +1 -0
  61. package/lib/typescript/src/errors.d.ts +27 -0
  62. package/lib/typescript/src/errors.d.ts.map +1 -0
  63. package/lib/typescript/src/examples/offlinePaymentFlow.d.ts +48 -0
  64. package/lib/typescript/src/examples/offlinePaymentFlow.d.ts.map +1 -0
  65. package/lib/typescript/src/index.d.ts +13 -0
  66. package/lib/typescript/src/index.d.ts.map +1 -0
  67. package/lib/typescript/src/intent.d.ts +84 -0
  68. package/lib/typescript/src/intent.d.ts.map +1 -0
  69. package/lib/typescript/src/intentManager.d.ts +46 -0
  70. package/lib/typescript/src/intentManager.d.ts.map +1 -0
  71. package/lib/typescript/src/internal/arciumHelper.d.ts +19 -0
  72. package/lib/typescript/src/internal/arciumHelper.d.ts.map +1 -0
  73. package/lib/typescript/src/nfc.d.ts +7 -0
  74. package/lib/typescript/src/nfc.d.ts.map +1 -0
  75. package/lib/typescript/src/noise.d.ts +5 -0
  76. package/lib/typescript/src/noise.d.ts.map +1 -0
  77. package/lib/typescript/src/qr.d.ts +6 -0
  78. package/lib/typescript/src/qr.d.ts.map +1 -0
  79. package/lib/typescript/src/reconciliation.d.ts +65 -0
  80. package/lib/typescript/src/reconciliation.d.ts.map +1 -0
  81. package/lib/typescript/src/services/authService.d.ts +55 -0
  82. package/lib/typescript/src/services/authService.d.ts.map +1 -0
  83. package/lib/typescript/src/storage/secureStorage.d.ts +7 -0
  84. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -0
  85. package/lib/typescript/src/storage.d.ts +4 -0
  86. package/lib/typescript/src/storage.d.ts.map +1 -0
  87. package/lib/typescript/src/sync.d.ts +40 -0
  88. package/lib/typescript/src/sync.d.ts.map +1 -0
  89. package/lib/typescript/src/types/tossUser.d.ts +39 -0
  90. package/lib/typescript/src/types/tossUser.d.ts.map +1 -0
  91. package/lib/typescript/src/utils/nonceUtils.d.ts +8 -0
  92. package/lib/typescript/src/utils/nonceUtils.d.ts.map +1 -0
  93. package/package.json +176 -0
  94. package/src/__tests__/index.test.tsx +1 -0
  95. package/src/__tests__/reconciliation.test.tsx +361 -0
  96. package/src/ble.ts +138 -0
  97. package/src/client/TossClient.ts +435 -0
  98. package/src/client/index.ts +2 -0
  99. package/src/contexts/WalletContext.tsx +127 -0
  100. package/src/discovery.ts +542 -0
  101. package/src/errors.ts +51 -0
  102. package/src/examples/offlinePaymentFlow.ts +331 -0
  103. package/src/index.tsx +61 -0
  104. package/src/intent.ts +328 -0
  105. package/src/intentManager.ts +164 -0
  106. package/src/internal/arciumHelper.ts +58 -0
  107. package/src/nfc.ts +57 -0
  108. package/src/noise.ts +9 -0
  109. package/src/qr.tsx +65 -0
  110. package/src/reconciliation.ts +421 -0
  111. package/src/services/authService.ts +238 -0
  112. package/src/storage/secureStorage.ts +100 -0
  113. package/src/storage.ts +17 -0
  114. package/src/sync.ts +101 -0
  115. package/src/types/tossUser.ts +81 -0
  116. package/src/utils/nonceUtils.ts +56 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Unit Tests for TOSS Reconciliation Module
3
+ * Tests the core reconciliation and settlement logic from Section 9-10
4
+ */
5
+
6
+ import { Keypair, PublicKey } from '@solana/web3.js';
7
+ import {
8
+ validateIntentOnchain,
9
+ buildTransactionFromIntent,
10
+ } from '../reconciliation';
11
+ import { createSignedIntent } from '../intent';
12
+ import type { SolanaIntent } from '../intent';
13
+
14
+ // Mock connection for testing
15
+ const mockConnection = {
16
+ getAccountInfo: jest.fn(),
17
+ getLatestBlockhash: jest.fn().mockResolvedValue({
18
+ blockhash: 'EksnHYAxZEqkLC6tsa4LmSKv67m6qPWtKwMEHUxvLfs7',
19
+ lastValidBlockHeight: 180000000,
20
+ }),
21
+ getSignaturesForAddress: jest.fn().mockResolvedValue([]),
22
+ getParsedTransaction: jest.fn(),
23
+ getSlot: jest.fn().mockResolvedValue(180000000),
24
+ sendRawTransaction: jest.fn(),
25
+ confirmTransaction: jest.fn(),
26
+ } as any;
27
+
28
+ describe('Reconciliation Module', () => {
29
+ let senderKeypair: Keypair;
30
+ let recipientAddress: PublicKey;
31
+ let testIntent: SolanaIntent;
32
+
33
+ beforeEach(async () => {
34
+ senderKeypair = Keypair.generate();
35
+ recipientAddress = new PublicKey('11111111111111111111111111111111');
36
+
37
+ // Create a test intent
38
+ testIntent = await createSignedIntent(
39
+ senderKeypair,
40
+ recipientAddress,
41
+ 1000000, // 1 SOL
42
+ mockConnection
43
+ );
44
+ });
45
+
46
+ describe('validateIntentOnchain', () => {
47
+ it('should validate a valid intent', async () => {
48
+ mockConnection.getAccountInfo.mockResolvedValueOnce({
49
+ lamports: 5000000, // 5 SOL
50
+ owner: new PublicKey('11111111111111111111111111111111'),
51
+ executable: false,
52
+ rentEpoch: 0,
53
+ data: Buffer.alloc(0),
54
+ });
55
+
56
+ const result = await validateIntentOnchain(testIntent, mockConnection);
57
+
58
+ expect(result.valid).toBe(true);
59
+ expect(result.error).toBeUndefined();
60
+ });
61
+
62
+ it('should reject expired intent', async () => {
63
+ const expiredIntent = {
64
+ ...testIntent,
65
+ expiry: Math.floor(Date.now() / 1000) - 100, // Expired
66
+ };
67
+
68
+ const result = await validateIntentOnchain(expiredIntent, mockConnection);
69
+
70
+ expect(result.valid).toBe(false);
71
+ expect(result.error).toContain('expired');
72
+ });
73
+
74
+ it('should reject intent with insufficient balance', async () => {
75
+ mockConnection.getAccountInfo.mockResolvedValueOnce({
76
+ lamports: 100000, // 0.1 SOL (less than intent amount)
77
+ owner: new PublicKey('11111111111111111111111111111111'),
78
+ executable: false,
79
+ rentEpoch: 0,
80
+ data: Buffer.alloc(0),
81
+ });
82
+
83
+ const result = await validateIntentOnchain(testIntent, mockConnection);
84
+
85
+ expect(result.valid).toBe(false);
86
+ expect(result.error).toContain('Insufficient balance');
87
+ });
88
+
89
+ it('should reject intent from non-existent account', async () => {
90
+ mockConnection.getAccountInfo.mockResolvedValueOnce(null);
91
+
92
+ const result = await validateIntentOnchain(testIntent, mockConnection);
93
+
94
+ expect(result.valid).toBe(false);
95
+ expect(result.error).toContain('does not exist');
96
+ });
97
+ });
98
+
99
+ describe('buildTransactionFromIntent', () => {
100
+ it('should build a valid transaction from intent', async () => {
101
+ mockConnection.getLatestBlockhash.mockResolvedValueOnce({
102
+ blockhash: 'EksnHYAxZEqkLC6tsa4LmSKv67m6qPWtKwMEHUxvLfs7',
103
+ lastValidBlockHeight: 180000000,
104
+ });
105
+
106
+ const transaction = await buildTransactionFromIntent(
107
+ testIntent,
108
+ mockConnection
109
+ );
110
+
111
+ expect(transaction).toBeDefined();
112
+ expect(transaction.instructions.length).toBeGreaterThan(0);
113
+ expect(transaction.feePayer).toEqual(senderKeypair.publicKey);
114
+ });
115
+
116
+ it('should include nonce instruction if nonce account provided', async () => {
117
+ const nonceKeypair = Keypair.generate();
118
+ const intentWithNonce = {
119
+ ...testIntent,
120
+ nonceAccount: nonceKeypair.publicKey.toBase58(),
121
+ nonceAuth: senderKeypair.publicKey.toBase58(),
122
+ };
123
+
124
+ mockConnection.getLatestBlockhash.mockResolvedValueOnce({
125
+ blockhash: 'EksnHYAxZEqkLC6tsa4LmSKv67m6qPWtKwMEHUxvLfs7',
126
+ lastValidBlockHeight: 180000000,
127
+ });
128
+
129
+ const transaction = await buildTransactionFromIntent(
130
+ intentWithNonce,
131
+ mockConnection
132
+ );
133
+
134
+ // Should have both transfer and nonce advance instructions
135
+ expect(transaction.instructions.length).toBeGreaterThanOrEqual(1);
136
+ });
137
+ });
138
+ });
139
+
140
+ /**
141
+ * Unit Tests for Discovery Module
142
+ */
143
+
144
+ import {
145
+ DeviceDiscoveryService,
146
+ IntentExchangeProtocol,
147
+ MultiDeviceConflictResolver,
148
+ type PeerDevice,
149
+ } from '../discovery';
150
+
151
+ describe('Discovery Module', () => {
152
+ describe('DeviceDiscoveryService', () => {
153
+ let discovery: DeviceDiscoveryService;
154
+
155
+ beforeEach(() => {
156
+ discovery = new DeviceDiscoveryService();
157
+ });
158
+
159
+ it('should register a peer device', () => {
160
+ const peer: PeerDevice = {
161
+ id: 'peer_001',
162
+ lastSeen: Date.now(),
163
+ transport: 'ble',
164
+ };
165
+
166
+ discovery.registerPeer(peer);
167
+
168
+ const active = discovery.getActivePeers();
169
+ expect(active).toContainEqual(
170
+ expect.objectContaining({ id: 'peer_001' })
171
+ );
172
+ });
173
+
174
+ it('should remove timed out peers', (done) => {
175
+ const peer: PeerDevice = {
176
+ id: 'peer_timeout',
177
+ lastSeen: Date.now() - 6 * 60 * 1000, // 6 minutes ago
178
+ transport: 'ble',
179
+ };
180
+
181
+ // Manually set old timestamp
182
+ discovery.registerPeer(peer);
183
+ discovery['discoveredPeers'].get('peer_timeout')!.lastSeen =
184
+ Date.now() - 6 * 60 * 1000;
185
+
186
+ const active = discovery.getActivePeers();
187
+ expect(active).not.toContainEqual(
188
+ expect.objectContaining({ id: 'peer_timeout' })
189
+ );
190
+
191
+ done();
192
+ });
193
+
194
+ it('should update trust score', () => {
195
+ const peer: PeerDevice = {
196
+ id: 'peer_trust',
197
+ lastSeen: Date.now(),
198
+ transport: 'ble',
199
+ trustScore: 50,
200
+ };
201
+
202
+ discovery.registerPeer(peer);
203
+ discovery.updateTrustScore('peer_trust', 25);
204
+
205
+ const updated = discovery.getPeer('peer_trust');
206
+ expect(updated?.trustScore).toBe(75);
207
+ });
208
+ });
209
+
210
+ describe('IntentExchangeProtocol', () => {
211
+ let protocol: IntentExchangeProtocol;
212
+
213
+ beforeEach(() => {
214
+ protocol = new IntentExchangeProtocol();
215
+ });
216
+
217
+ it('should create an exchange request', () => {
218
+ const intent: SolanaIntent = {
219
+ id: 'intent_test',
220
+ from: 'sender_addr',
221
+ to: 'recipient_addr',
222
+ amount: 1000000,
223
+ nonce: 1,
224
+ expiry: Math.floor(Date.now() / 1000) + 3600,
225
+ signature: 'sig123',
226
+ status: 'pending',
227
+ createdAt: Math.floor(Date.now() / 1000),
228
+ updatedAt: Math.floor(Date.now() / 1000),
229
+ };
230
+
231
+ const request = protocol.createRequest(intent, 'device_001');
232
+
233
+ expect(request).toBeDefined();
234
+ expect(request.intent).toEqual(intent);
235
+ expect(request.requesterId).toBe('device_001');
236
+ });
237
+
238
+ it('should create a response to exchange request', () => {
239
+ const intent: SolanaIntent = {
240
+ id: 'intent_test',
241
+ from: 'sender_addr',
242
+ to: 'recipient_addr',
243
+ amount: 1000000,
244
+ nonce: 1,
245
+ expiry: Math.floor(Date.now() / 1000) + 3600,
246
+ signature: 'sig123',
247
+ status: 'pending',
248
+ createdAt: Math.floor(Date.now() / 1000),
249
+ updatedAt: Math.floor(Date.now() / 1000),
250
+ };
251
+
252
+ const request = protocol.createRequest(intent, 'device_001');
253
+
254
+ const response = protocol.createResponse(
255
+ request.requestId,
256
+ 'device_002',
257
+ 'accepted',
258
+ undefined,
259
+ [intent.id]
260
+ );
261
+
262
+ expect(response.status).toBe('accepted');
263
+ expect(response.responderId).toBe('device_002');
264
+ expect(response.acknowledgedIntentIds).toContain(intent.id);
265
+ });
266
+ });
267
+
268
+ describe('MultiDeviceConflictResolver', () => {
269
+ it('should detect conflicting intents', () => {
270
+ const intent1: SolanaIntent = {
271
+ id: 'intent_1',
272
+ from: 'sender_addr',
273
+ to: 'recipient_addr',
274
+ amount: 1000000,
275
+ nonce: 1,
276
+ expiry: Math.floor(Date.now() / 1000) + 3600,
277
+ signature: 'sig1',
278
+ status: 'pending',
279
+ createdAt: Math.floor(Date.now() / 1000),
280
+ updatedAt: Math.floor(Date.now() / 1000),
281
+ };
282
+
283
+ const intent2: SolanaIntent = {
284
+ ...intent1,
285
+ id: 'intent_2',
286
+ signature: 'sig2',
287
+ };
288
+
289
+ const conflicts = MultiDeviceConflictResolver.detectConflicts([
290
+ intent1,
291
+ intent2,
292
+ ]);
293
+
294
+ expect(conflicts.length).toBe(1);
295
+ expect(conflicts[0]).toContainEqual(intent1);
296
+ expect(conflicts[0]).toContainEqual(intent2);
297
+ });
298
+
299
+ it('should resolve conflicts deterministically by nonce', () => {
300
+ const intent1: SolanaIntent = {
301
+ id: 'intent_1',
302
+ from: 'sender_addr',
303
+ to: 'recipient_addr',
304
+ amount: 1000000,
305
+ nonce: 2, // Higher nonce
306
+ expiry: Math.floor(Date.now() / 1000) + 3600,
307
+ signature: 'sig1',
308
+ status: 'pending',
309
+ createdAt: Math.floor(Date.now() / 1000),
310
+ updatedAt: Math.floor(Date.now() / 1000),
311
+ };
312
+
313
+ const intent2: SolanaIntent = {
314
+ ...intent1,
315
+ id: 'intent_2',
316
+ nonce: 1, // Lower nonce (wins)
317
+ signature: 'sig2',
318
+ };
319
+
320
+ const resolution = MultiDeviceConflictResolver.resolveConflicts([
321
+ intent1,
322
+ intent2,
323
+ ]);
324
+
325
+ expect(resolution.winner.id).toBe('intent_2');
326
+ expect(resolution.losers[0]?.id).toBe('intent_1');
327
+ });
328
+
329
+ it('should resolve conflicts by timestamp if nonces equal', () => {
330
+ const now = Math.floor(Date.now() / 1000);
331
+
332
+ const intent1: SolanaIntent = {
333
+ id: 'intent_1',
334
+ from: 'sender_addr',
335
+ to: 'recipient_addr',
336
+ amount: 1000000,
337
+ nonce: 1,
338
+ expiry: now + 3600,
339
+ signature: 'sig1',
340
+ status: 'pending',
341
+ createdAt: now + 100, // Later
342
+ updatedAt: now,
343
+ };
344
+
345
+ const intent2: SolanaIntent = {
346
+ ...intent1,
347
+ id: 'intent_2',
348
+ createdAt: now, // Earlier (wins)
349
+ signature: 'sig2',
350
+ };
351
+
352
+ const resolution = MultiDeviceConflictResolver.resolveConflicts([
353
+ intent1,
354
+ intent2,
355
+ ]);
356
+
357
+ expect(resolution.winner.id).toBe('intent_2');
358
+ expect(resolution.losers[0]?.id).toBe('intent_1');
359
+ });
360
+ });
361
+ });
package/src/ble.ts ADDED
@@ -0,0 +1,138 @@
1
+ // src/ble.ts
2
+ import { BleManager, Device } from 'react-native-ble-plx';
3
+ import { PermissionsAndroid, Platform } from 'react-native';
4
+ import type { TossUser } from './types/tossUser';
5
+ import type { SolanaIntent } from './intent';
6
+
7
+ const SERVICE_UUID = '0000ff00-0000-1000-8000-00805f9b34fb';
8
+ const USER_CHARACTERISTIC = '0000ff01-0000-1000-8000-00805f9b34fb';
9
+ const INTENT_CHARACTERISTIC = '0000ff02-0000-1000-8000-00805f9b34fb';
10
+
11
+ const manager = new BleManager();
12
+
13
+ export async function requestBLEPermissions() {
14
+ if (Platform.OS === 'android') {
15
+ await PermissionsAndroid.requestMultiple([
16
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
17
+ PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
18
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
19
+ ]);
20
+ }
21
+ }
22
+
23
+ // Connect to a BLE device
24
+ async function connect(device: Device) {
25
+ const connectedDevice = await manager.connectToDevice(device.id);
26
+ await connectedDevice.discoverAllServicesAndCharacteristics();
27
+ return connectedDevice;
28
+ }
29
+
30
+ // Scan for BLE devices advertising TOSS service
31
+ export function startTossScan(
32
+ onUserFound: (user: TossUser, device: Device) => void,
33
+ onIntentFound: (intent: SolanaIntent, device: Device) => void
34
+ ) {
35
+ manager.startDeviceScan([SERVICE_UUID], null, async (error, device) => {
36
+ if (error) {
37
+ console.warn('BLE scan error', error.message);
38
+ return;
39
+ }
40
+
41
+ if (device) {
42
+ try {
43
+ const connectedDevice = await connect(device);
44
+ const services =
45
+ await connectedDevice.discoverAllServicesAndCharacteristics();
46
+
47
+ // Check for user data
48
+ const userData = await services.readCharacteristicForService(
49
+ device.id,
50
+ SERVICE_UUID,
51
+ USER_CHARACTERISTIC
52
+ );
53
+
54
+ if (userData?.value) {
55
+ const user = JSON.parse(userData.value) as TossUser;
56
+ onUserFound(user, device);
57
+ }
58
+
59
+ // Check for intent data
60
+ const intentData = await services.readCharacteristicForService(
61
+ device.id,
62
+ SERVICE_UUID,
63
+ INTENT_CHARACTERISTIC
64
+ );
65
+
66
+ if (intentData?.value) {
67
+ const intent = JSON.parse(intentData.value) as SolanaIntent;
68
+ onIntentFound(intent, device);
69
+ }
70
+ } catch (err) {
71
+ console.warn('Error reading device data:', err);
72
+ }
73
+ }
74
+ });
75
+ }
76
+
77
+ import { State } from 'react-native-ble-plx';
78
+
79
+ // Advertise user data via BLE
80
+ export async function advertiseUser(user: TossUser) {
81
+ try {
82
+ // On Android, we need to use a different approach since react-native-ble-plx
83
+ // doesn't support BLE advertising directly on the client side
84
+ if (Platform.OS === 'android') {
85
+ console.warn(
86
+ 'BLE advertising is not directly supported on Android. Consider using react-native-ble-advertise for this functionality.'
87
+ );
88
+ return;
89
+ }
90
+
91
+ // For iOS, we can use the state change to know when advertising starts
92
+ const subscription = manager.onStateChange((state) => {
93
+ if (state === State.PoweredOn) {
94
+ console.log(`Advertising user ${user.userId} via BLE`);
95
+ subscription.remove();
96
+ }
97
+ }, true);
98
+ } catch (error) {
99
+ const errorMessage = error instanceof Error ? error.message : String(error);
100
+ console.error('Error in BLE advertising:', errorMessage);
101
+ throw new Error(`Failed to advertise user: ${errorMessage}`);
102
+ }
103
+ }
104
+
105
+ export async function stopAdvertising() {
106
+ try {
107
+ // On iOS, we can stop scanning which will effectively stop advertising
108
+ manager.stopDeviceScan();
109
+ console.log('Stopped BLE advertising');
110
+ } catch (error) {
111
+ const errorMessage = error instanceof Error ? error.message : String(error);
112
+ console.error('Error stopping BLE advertising:', errorMessage);
113
+ }
114
+ }
115
+
116
+ // Send intent to a specific device
117
+ export async function sendIntentToDevice(
118
+ deviceId: string,
119
+ intent: SolanaIntent
120
+ ) {
121
+ const jsonIntent = JSON.stringify(intent);
122
+ const device = await manager.connectToDevice(deviceId);
123
+ await device.discoverAllServicesAndCharacteristics();
124
+
125
+ await device.writeCharacteristicWithResponseForService(
126
+ device.id,
127
+ SERVICE_UUID,
128
+ INTENT_CHARACTERISTIC,
129
+ Buffer.from(jsonIntent).toString('base64')
130
+ );
131
+
132
+ await device.cancelConnection();
133
+ }
134
+
135
+ // Stop scan
136
+ export function stopScan() {
137
+ manager.stopDeviceScan();
138
+ }