toss-expo-sdk 0.1.2 → 1.0.2

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 (93) hide show
  1. package/README.md +380 -25
  2. package/lib/module/ble.js +59 -4
  3. package/lib/module/ble.js.map +1 -1
  4. package/lib/module/client/BLETransactionHandler.js +277 -0
  5. package/lib/module/client/BLETransactionHandler.js.map +1 -0
  6. package/lib/module/client/NonceAccountManager.js +364 -0
  7. package/lib/module/client/NonceAccountManager.js.map +1 -0
  8. package/lib/module/client/TossClient.js +1 -1
  9. package/lib/module/client/TossClient.js.map +1 -1
  10. package/lib/module/examples/enhancedFeaturesFlow.js +233 -0
  11. package/lib/module/examples/enhancedFeaturesFlow.js.map +1 -0
  12. package/lib/module/examples/offlinePaymentFlow.js +27 -27
  13. package/lib/module/examples/offlinePaymentFlow.js.map +1 -1
  14. package/lib/module/hooks/useOfflineBLETransactions.js +314 -0
  15. package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
  16. package/lib/module/index.js +18 -8
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/intent.js +129 -0
  19. package/lib/module/intent.js.map +1 -1
  20. package/lib/module/noise.js +175 -0
  21. package/lib/module/noise.js.map +1 -1
  22. package/lib/module/qr.js +2 -2
  23. package/lib/module/reconciliation.js +155 -0
  24. package/lib/module/reconciliation.js.map +1 -1
  25. package/lib/module/services/authService.js +166 -3
  26. package/lib/module/services/authService.js.map +1 -1
  27. package/lib/module/storage/secureStorage.js +102 -0
  28. package/lib/module/storage/secureStorage.js.map +1 -1
  29. package/lib/module/sync.js +25 -1
  30. package/lib/module/sync.js.map +1 -1
  31. package/lib/module/types/nonceAccount.js +2 -0
  32. package/lib/module/types/nonceAccount.js.map +1 -0
  33. package/lib/module/types/tossUser.js +16 -1
  34. package/lib/module/types/tossUser.js.map +1 -1
  35. package/lib/module/utils/compression.js +210 -0
  36. package/lib/module/utils/compression.js.map +1 -0
  37. package/lib/module/wifi.js +311 -0
  38. package/lib/module/wifi.js.map +1 -0
  39. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
  40. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
  41. package/lib/typescript/src/ble.d.ts +31 -2
  42. package/lib/typescript/src/ble.d.ts.map +1 -1
  43. package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
  44. package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
  45. package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
  46. package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
  47. package/lib/typescript/src/examples/enhancedFeaturesFlow.d.ts +45 -0
  48. package/lib/typescript/src/examples/enhancedFeaturesFlow.d.ts.map +1 -0
  49. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
  50. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
  51. package/lib/typescript/src/index.d.ts +11 -4
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/lib/typescript/src/intent.d.ts +15 -0
  54. package/lib/typescript/src/intent.d.ts.map +1 -1
  55. package/lib/typescript/src/noise.d.ts +62 -0
  56. package/lib/typescript/src/noise.d.ts.map +1 -1
  57. package/lib/typescript/src/reconciliation.d.ts +6 -0
  58. package/lib/typescript/src/reconciliation.d.ts.map +1 -1
  59. package/lib/typescript/src/services/authService.d.ts +26 -1
  60. package/lib/typescript/src/services/authService.d.ts.map +1 -1
  61. package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
  62. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
  63. package/lib/typescript/src/sync.d.ts +6 -1
  64. package/lib/typescript/src/sync.d.ts.map +1 -1
  65. package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
  66. package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
  67. package/lib/typescript/src/types/tossUser.d.ts +16 -0
  68. package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
  69. package/lib/typescript/src/utils/compression.d.ts +52 -0
  70. package/lib/typescript/src/utils/compression.d.ts.map +1 -0
  71. package/lib/typescript/src/wifi.d.ts +116 -0
  72. package/lib/typescript/src/wifi.d.ts.map +1 -0
  73. package/package.json +1 -1
  74. package/src/__tests__/solana-program-simple.test.ts +256 -0
  75. package/src/ble.ts +105 -4
  76. package/src/client/BLETransactionHandler.ts +364 -0
  77. package/src/client/NonceAccountManager.ts +444 -0
  78. package/src/client/TossClient.ts +1 -1
  79. package/src/examples/enhancedFeaturesFlow.ts +272 -0
  80. package/src/examples/offlinePaymentFlow.ts +27 -27
  81. package/src/hooks/useOfflineBLETransactions.ts +438 -0
  82. package/src/index.tsx +52 -6
  83. package/src/intent.ts +166 -0
  84. package/src/noise.ts +238 -0
  85. package/src/qr.tsx +2 -2
  86. package/src/reconciliation.ts +184 -0
  87. package/src/services/authService.ts +190 -3
  88. package/src/storage/secureStorage.ts +138 -0
  89. package/src/sync.ts +40 -0
  90. package/src/types/nonceAccount.ts +75 -0
  91. package/src/types/tossUser.ts +35 -2
  92. package/src/utils/compression.ts +247 -0
  93. package/src/wifi.ts +401 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * WiFi Direct Transport for TOSS
3
+ *
4
+ * Higher-bandwidth alternative to BLE for device-to-device communication
5
+ * Fallback to BLE if WiFi Direct unavailable
6
+ *
7
+ * Uses native React Native APIs for production-ready implementation
8
+ */
9
+ import type { SolanaIntent } from './intent';
10
+ import type { OfflineTransaction } from './types/nonceAccount';
11
+ /**
12
+ * WiFi Direct connection state
13
+ */
14
+ export interface WiFiDirectPeer {
15
+ deviceName: string;
16
+ deviceAddress: string;
17
+ isGroupOwner: boolean;
18
+ signalStrength?: number;
19
+ }
20
+ /**
21
+ * WiFi Direct socket for data transmission
22
+ */
23
+ export interface WiFiDirectSocket {
24
+ peerId: string;
25
+ connected: boolean;
26
+ bytesTransferred: number;
27
+ lastActivityTime: number;
28
+ }
29
+ /**
30
+ * WiFi Direct Transport Handler
31
+ * Wrapper around native WiFi Direct capabilities
32
+ *
33
+ * Supports higher MTU (1200+ bytes) than BLE (480 bytes)
34
+ * Useful for batch transmission of intents
35
+ */
36
+ export declare class WiFiDirectTransport {
37
+ private connectedPeers;
38
+ private readonly SOCKET_TIMEOUT;
39
+ private readonly WIFI_MTU;
40
+ constructor(_platform?: 'android' | 'ios');
41
+ /**
42
+ * Check if WiFi Direct is available on device
43
+ */
44
+ isAvailable(): Promise<boolean>;
45
+ /**
46
+ * Enable WiFi Direct on device
47
+ */
48
+ enable(): Promise<void>;
49
+ /**
50
+ * Discover nearby WiFi Direct peers
51
+ */
52
+ discoverPeers(timeoutSeconds?: number): Promise<WiFiDirectPeer[]>;
53
+ /**
54
+ * Connect to a specific WiFi Direct peer
55
+ */
56
+ connectToPeer(deviceAddress: string): Promise<WiFiDirectSocket>;
57
+ /**
58
+ * Send intent via WiFi Direct connection
59
+ * Uses larger MTU than BLE for efficiency
60
+ */
61
+ sendIntent(socket: WiFiDirectSocket, intent: SolanaIntent): Promise<{
62
+ success: boolean;
63
+ bytesTransferred: number;
64
+ chunks: number;
65
+ }>;
66
+ /**
67
+ * Send offline transaction via WiFi Direct
68
+ */
69
+ sendOfflineTransaction(socket: WiFiDirectSocket, transaction: OfflineTransaction): Promise<{
70
+ success: boolean;
71
+ bytesTransferred: number;
72
+ chunks: number;
73
+ }>;
74
+ /**
75
+ * Receive data from WiFi Direct peer
76
+ */
77
+ receiveData(socket: WiFiDirectSocket, expectedChunks: number): Promise<Buffer>;
78
+ /**
79
+ * Disconnect from WiFi Direct peer
80
+ */
81
+ disconnect(peerId: string): Promise<void>;
82
+ /**
83
+ * Get all connected peers
84
+ */
85
+ getConnectedPeers(): WiFiDirectSocket[];
86
+ /**
87
+ * Get MTU size for this transport
88
+ */
89
+ getMTU(): number;
90
+ /**
91
+ * Clean up expired connections
92
+ */
93
+ cleanupExpiredConnections(): void;
94
+ }
95
+ /**
96
+ * Smart transport selector
97
+ * Automatically chooses best transport for given context
98
+ */
99
+ export declare class SmartTransportSelector {
100
+ private wifiDirect;
101
+ constructor();
102
+ /**
103
+ * Select best available transport for intent transmission
104
+ *
105
+ * Preference order:
106
+ * 1. WiFi Direct (fastest, 1200 MTU)
107
+ * 2. BLE (fallback, 480 MTU)
108
+ */
109
+ selectTransport(): Promise<'wifi' | 'ble'>;
110
+ /**
111
+ * Check if WiFi Direct should be used
112
+ * Factors: availability, proximity, battery level
113
+ */
114
+ shouldUseWiFi(checkBattery?: boolean): Promise<boolean>;
115
+ }
116
+ //# sourceMappingURL=wifi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wifi.d.ts","sourceRoot":"","sources":["../../../src/wifi.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAK/D;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;gBAErB,SAAS,GAAE,SAAS,GAAG,KAAiB;IAQpD;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAiBrC;;OAEG;IACG,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAoB7B;;OAEG;IACG,aAAa,CAAC,cAAc,GAAE,MAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAmB3E;;OAEG;IACG,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA8BrE;;;OAGG;IACG,UAAU,CACd,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,YAAY,GACnB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAmDF;;OAEG;IACG,sBAAsB,CAC1B,MAAM,EAAE,gBAAgB,EACxB,WAAW,EAAE,kBAAkB,GAC9B,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAiCF;;OAEG;IACG,WAAW,CACf,MAAM,EAAE,gBAAgB,EACxB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC;IAqClB;;OAEG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB/C;;OAEG;IACH,iBAAiB,IAAI,gBAAgB,EAAE;IAMvC;;OAEG;IACH,MAAM,IAAI,MAAM;IAIhB;;OAEG;IACH,yBAAyB,IAAI,IAAI;CASlC;AAED;;;GAGG;AACH,qBAAa,sBAAsB;IACjC,OAAO,CAAC,UAAU,CAAsB;;IAMxC;;;;;;OAMG;IACG,eAAe,IAAI,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC;IAUhD;;;OAGG;IACG,aAAa,CAAC,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,OAAO,CAAC;CAgBrE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toss-expo-sdk",
3
- "version": "0.1.2",
3
+ "version": "1.0.2",
4
4
  "description": "The official React Native SDK for The Offline Solana Stack (TOSS)",
5
5
  "type": "module",
6
6
  "main": "./lib/module/index.js",
@@ -0,0 +1,256 @@
1
+ /**
2
+ * TOSS Solana Program Integration Tests (Simplified)
3
+ *
4
+ * Tests for the toss-intent-processor program
5
+ * Gap #5: Onchain Intent Verification
6
+ */
7
+
8
+ import { Connection, Keypair } from '@solana/web3.js';
9
+ import { createIntent, verifyIntent } from '../intent';
10
+ import { NonceAccountManager } from '../client/NonceAccountManager';
11
+
12
+ describe('TOSS Solana Intent Processor Program', () => {
13
+ let connection: Connection;
14
+ let senderKeypair: Keypair;
15
+ let recipientKeypair: Keypair;
16
+ let nonceManager: NonceAccountManager;
17
+
18
+ beforeAll(() => {
19
+ // Use Devnet for testing
20
+ connection = new Connection('https://api.devnet.solana.com', 'confirmed');
21
+ senderKeypair = Keypair.generate();
22
+ recipientKeypair = Keypair.generate();
23
+ nonceManager = new NonceAccountManager(connection);
24
+ });
25
+
26
+ describe('Intent Signature Verification', () => {
27
+ it('should create a valid, verifiable intent', async () => {
28
+ // Create intent
29
+ const intent = await createIntent(
30
+ senderKeypair,
31
+ recipientKeypair.publicKey,
32
+ 1000000,
33
+ connection,
34
+ { expiresIn: 60 * 60 }
35
+ );
36
+
37
+ // Verify the intent can be verified
38
+ expect(intent.signature).toBeDefined();
39
+ expect(intent.signature.length).toBeGreaterThan(0);
40
+
41
+ // Verify intent signature locally (client-side)
42
+ const isValid = await verifyIntent(intent, connection);
43
+ expect(isValid).toBe(true);
44
+ });
45
+
46
+ it('should reject modified intent', async () => {
47
+ const intent = await createIntent(
48
+ senderKeypair,
49
+ recipientKeypair.publicKey,
50
+ 1000000,
51
+ connection
52
+ );
53
+
54
+ // Modify the intent (would fail signature check)
55
+ const modifiedIntent = { ...intent, amount: 2000000 };
56
+
57
+ // Modified intent should fail verification
58
+ const isValid = await verifyIntent(modifiedIntent, connection);
59
+ expect(isValid).toBe(false);
60
+ });
61
+
62
+ it('should reject expired intent', async () => {
63
+ // Create an intent that's already expired
64
+ const intent = await createIntent(
65
+ senderKeypair,
66
+ recipientKeypair.publicKey,
67
+ 1000000,
68
+ connection,
69
+ { expiresIn: -100 } // Already expired
70
+ );
71
+
72
+ // Expired intent should fail
73
+ const isValid = await verifyIntent(intent, connection);
74
+ expect(isValid).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe('Intent Program Data Structure', () => {
79
+ it('should verify intent data structure matches program expectations', async () => {
80
+ const intent = await createIntent(
81
+ senderKeypair,
82
+ recipientKeypair.publicKey,
83
+ 5000000,
84
+ connection
85
+ );
86
+
87
+ // Verify all required fields for onchain program
88
+ expect(intent.from).toBeDefined();
89
+ expect(intent.to).toBeDefined();
90
+ expect(intent.amount).toBeGreaterThan(0);
91
+ expect(intent.nonce).toBeGreaterThanOrEqual(0);
92
+ expect(intent.expiry).toBeGreaterThan(Date.now() / 1000);
93
+ expect(intent.signature).toBeDefined();
94
+ expect(intent.blockhash).toBeDefined();
95
+ });
96
+
97
+ it('should enforce expiry constraints', async () => {
98
+ const now = Math.floor(Date.now() / 1000);
99
+ const intent = await createIntent(
100
+ senderKeypair,
101
+ recipientKeypair.publicKey,
102
+ 1000000,
103
+ connection,
104
+ { expiresIn: 3600 } // 1 hour
105
+ );
106
+
107
+ // Expiry should be in future
108
+ expect(intent.expiry).toBeGreaterThan(now);
109
+ expect(intent.expiry - now).toBeCloseTo(3600, -1);
110
+ });
111
+ });
112
+
113
+ describe('Deterministic Settlement', () => {
114
+ it('should handle settlement with proper sequencing', async () => {
115
+ // Create multiple intents
116
+ const intent1 = await createIntent(
117
+ senderKeypair,
118
+ recipientKeypair.publicKey,
119
+ 1000000,
120
+ connection
121
+ );
122
+
123
+ const intent2 = await createIntent(
124
+ senderKeypair,
125
+ recipientKeypair.publicKey,
126
+ 2000000,
127
+ connection
128
+ );
129
+
130
+ // Both should be valid
131
+ const isValid1 = await verifyIntent(intent1, connection);
132
+ const isValid2 = await verifyIntent(intent2, connection);
133
+
134
+ expect(isValid1).toBe(true);
135
+ expect(isValid2).toBe(true);
136
+
137
+ // Different nonces ensure ordering
138
+ expect(intent1.nonce).not.toBe(intent2.nonce);
139
+ });
140
+
141
+ it('should reject duplicate nonce', async () => {
142
+ const intent = await createIntent(
143
+ senderKeypair,
144
+ recipientKeypair.publicKey,
145
+ 1000000,
146
+ connection
147
+ );
148
+
149
+ // Try to create another intent with same nonce (would fail in practice)
150
+ const intentDupe = { ...intent };
151
+
152
+ // Both have same nonce - would be rejected onchain
153
+ expect(intent.nonce).toBe(intentDupe.nonce);
154
+ });
155
+ });
156
+
157
+ describe('Program Constraints', () => {
158
+ it('should validate amount is positive', async () => {
159
+ const intent = await createIntent(
160
+ senderKeypair,
161
+ recipientKeypair.publicKey,
162
+ 1000000,
163
+ connection
164
+ );
165
+
166
+ // Intent amount should be positive
167
+ expect(intent.amount).toBeGreaterThan(0);
168
+ });
169
+
170
+ it('should allow large amounts within u64 bounds', async () => {
171
+ const largeAmount = Math.floor(Number.MAX_SAFE_INTEGER / 2);
172
+ const intent = await createIntent(
173
+ senderKeypair,
174
+ recipientKeypair.publicKey,
175
+ largeAmount,
176
+ connection
177
+ );
178
+
179
+ expect(intent.amount).toBeLessThanOrEqual(Number.MAX_SAFE_INTEGER);
180
+ });
181
+ });
182
+
183
+ describe('Nonce Account Integration', () => {
184
+ it('should create nonce account for replay protection', async () => {
185
+ const nonceAuthority = Keypair.generate();
186
+
187
+ const mockTossUser: any = {
188
+ userId: 'test-user-nonce',
189
+ username: 'testusernonce',
190
+ wallet: {
191
+ publicKey: senderKeypair.publicKey.toBase58(),
192
+ isVerified: true,
193
+ },
194
+ security: {
195
+ biometricEnabled: true,
196
+ nonceAccountRequiresBiometric: true,
197
+ },
198
+ tossFeatures: {
199
+ canSend: true,
200
+ canReceive: true,
201
+ isPrivateTxEnabled: false,
202
+ maxTransactionAmount: 10000000,
203
+ offlineTransactionsEnabled: true,
204
+ nonceAccountEnabled: true,
205
+ },
206
+ };
207
+
208
+ const nonceAccountInfo = await nonceManager.createNonceAccount(
209
+ mockTossUser,
210
+ nonceAuthority,
211
+ senderKeypair.publicKey,
212
+ { requireBiometric: true }
213
+ );
214
+
215
+ expect(nonceAccountInfo).toBeDefined();
216
+ expect(nonceAccountInfo.address).toBeDefined();
217
+ expect(nonceAccountInfo.isBiometricProtected).toBe(true);
218
+ });
219
+
220
+ it('should validate nonce account is active', async () => {
221
+ const nonceAuthority = Keypair.generate();
222
+
223
+ const mockTossUser: any = {
224
+ userId: 'test-user-validate',
225
+ username: 'testuservalidate',
226
+ wallet: {
227
+ publicKey: senderKeypair.publicKey.toBase58(),
228
+ isVerified: true,
229
+ },
230
+ security: {
231
+ biometricEnabled: true,
232
+ nonceAccountRequiresBiometric: true,
233
+ },
234
+ tossFeatures: {
235
+ canSend: true,
236
+ canReceive: true,
237
+ isPrivateTxEnabled: false,
238
+ maxTransactionAmount: 10000000,
239
+ offlineTransactionsEnabled: true,
240
+ nonceAccountEnabled: true,
241
+ },
242
+ };
243
+
244
+ const nonceAccountInfo = await nonceManager.createNonceAccount(
245
+ mockTossUser,
246
+ nonceAuthority,
247
+ senderKeypair.publicKey,
248
+ { requireBiometric: true }
249
+ );
250
+
251
+ // Nonce account should be valid
252
+ const isValid = nonceManager.isNonceAccountValid(nonceAccountInfo);
253
+ expect(isValid).toBe(true);
254
+ });
255
+ });
256
+ });
package/src/ble.ts CHANGED
@@ -3,12 +3,18 @@ import { BleManager, Device } from 'react-native-ble-plx';
3
3
  import { PermissionsAndroid, Platform } from 'react-native';
4
4
  import type { TossUser } from './types/tossUser';
5
5
  import type { SolanaIntent } from './intent';
6
+ import type { OfflineTransaction } from './types/nonceAccount';
7
+ import { BLETransactionHandler } from './client/BLETransactionHandler';
6
8
 
7
9
  const SERVICE_UUID = '0000ff00-0000-1000-8000-00805f9b34fb';
8
10
  const USER_CHARACTERISTIC = '0000ff01-0000-1000-8000-00805f9b34fb';
9
11
  const INTENT_CHARACTERISTIC = '0000ff02-0000-1000-8000-00805f9b34fb';
12
+ const OFFLINE_TX_CHARACTERISTIC = '0000ff03-0000-1000-8000-00805f9b34fb'; // New for offline transactions
10
13
 
11
14
  const manager = new BleManager();
15
+ const bleTransactionHandler = new BLETransactionHandler(
16
+ Platform.OS === 'ios' ? 'ios' : 'android'
17
+ );
12
18
 
13
19
  export async function requestBLEPermissions() {
14
20
  if (Platform.OS === 'android') {
@@ -30,7 +36,8 @@ async function connect(device: Device) {
30
36
  // Scan for BLE devices advertising TOSS service
31
37
  export function startTossScan(
32
38
  onUserFound: (user: TossUser, device: Device) => void,
33
- onIntentFound: (intent: SolanaIntent, device: Device) => void
39
+ onIntentFound: (intent: SolanaIntent, device: Device) => void,
40
+ onOfflineTransactionFound?: (tx: OfflineTransaction, device: Device) => void
34
41
  ) {
35
42
  manager.startDeviceScan([SERVICE_UUID], null, async (error, device) => {
36
43
  if (error) {
@@ -67,6 +74,25 @@ export function startTossScan(
67
74
  const intent = JSON.parse(intentData.value) as SolanaIntent;
68
75
  onIntentFound(intent, device);
69
76
  }
77
+
78
+ // Check for offline transaction data (fragmented)
79
+ if (onOfflineTransactionFound) {
80
+ try {
81
+ const txData = await services.readCharacteristicForService(
82
+ device.id,
83
+ SERVICE_UUID,
84
+ OFFLINE_TX_CHARACTERISTIC
85
+ );
86
+
87
+ if (txData?.value) {
88
+ const tx = JSON.parse(txData.value) as OfflineTransaction;
89
+ onOfflineTransactionFound(tx, device);
90
+ }
91
+ } catch {
92
+ // Offline TX characteristic may not be available
93
+ console.debug('Offline transaction characteristic not found');
94
+ }
95
+ }
70
96
  } catch (err) {
71
97
  console.warn('Error reading device data:', err);
72
98
  }
@@ -132,7 +158,82 @@ export async function sendIntentToDevice(
132
158
  await device.cancelConnection();
133
159
  }
134
160
 
135
- // Stop scan
136
- export function stopScan() {
137
- manager.stopDeviceScan();
161
+ /**
162
+ * Send fragmented offline transaction over BLE with Noise Protocol encryption
163
+ * Automatically handles MTU limitations and retries
164
+ */
165
+ export async function sendOfflineTransactionFragmented(
166
+ device: Device,
167
+ transaction: OfflineTransaction | SolanaIntent,
168
+ noiseEncryptFn?: (data: Uint8Array) => Promise<any>,
169
+ isIntent: boolean = false
170
+ ): Promise<{
171
+ success: boolean;
172
+ sentFragments: number;
173
+ failedFragments: number[];
174
+ messageId: string;
175
+ }> {
176
+ try {
177
+ const result = await bleTransactionHandler.sendFragmentedTransactionBLE(
178
+ device,
179
+ transaction,
180
+ async (deviceId, charUUID, data) => {
181
+ const dev = await manager.connectToDevice(deviceId);
182
+ await dev.discoverAllServicesAndCharacteristics();
183
+
184
+ const characteristic =
185
+ charUUID === OFFLINE_TX_CHARACTERISTIC
186
+ ? OFFLINE_TX_CHARACTERISTIC
187
+ : INTENT_CHARACTERISTIC;
188
+
189
+ await dev.writeCharacteristicWithResponseForService(
190
+ deviceId,
191
+ SERVICE_UUID,
192
+ characteristic,
193
+ data.toString('base64')
194
+ );
195
+
196
+ await dev.cancelConnection();
197
+ },
198
+ noiseEncryptFn,
199
+ isIntent
200
+ );
201
+
202
+ return result;
203
+ } catch (error) {
204
+ const errorMessage = error instanceof Error ? error.message : String(error);
205
+ console.error('Failed to send offline transaction:', errorMessage);
206
+ throw new Error(`BLE transmission failed: ${errorMessage}`);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Receive and reassemble fragmented message from BLE
212
+ */
213
+ export async function receiveOfflineTransactionFragment(
214
+ fragment: any,
215
+ noiseDecryptFn?: (encrypted: any) => Promise<Uint8Array>
216
+ ): Promise<{
217
+ complete: boolean;
218
+ transaction?: OfflineTransaction | SolanaIntent;
219
+ progress: { received: number; total: number };
220
+ }> {
221
+ return bleTransactionHandler.receiveFragmentedMessage(
222
+ fragment,
223
+ noiseDecryptFn
224
+ );
225
+ }
226
+
227
+ /**
228
+ * Get current BLE MTU configuration
229
+ */
230
+ export function getBLEMTUConfig() {
231
+ return bleTransactionHandler.getMTUConfig();
232
+ }
233
+
234
+ /**
235
+ * Set custom BLE MTU configuration
236
+ */
237
+ export function setBLEMTUConfig(config: Partial<any>) {
238
+ bleTransactionHandler.setMTUConfig(config);
138
239
  }