toss-expo-sdk 0.1.2 → 1.0.1

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 (73) hide show
  1. package/README.md +368 -15
  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/hooks/useOfflineBLETransactions.js +314 -0
  11. package/lib/module/hooks/useOfflineBLETransactions.js.map +1 -0
  12. package/lib/module/index.js +12 -8
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/module/intent.js +129 -0
  15. package/lib/module/intent.js.map +1 -1
  16. package/lib/module/noise.js +175 -0
  17. package/lib/module/noise.js.map +1 -1
  18. package/lib/module/reconciliation.js +155 -0
  19. package/lib/module/reconciliation.js.map +1 -1
  20. package/lib/module/services/authService.js +164 -1
  21. package/lib/module/services/authService.js.map +1 -1
  22. package/lib/module/storage/secureStorage.js +102 -0
  23. package/lib/module/storage/secureStorage.js.map +1 -1
  24. package/lib/module/sync.js +25 -1
  25. package/lib/module/sync.js.map +1 -1
  26. package/lib/module/types/nonceAccount.js +2 -0
  27. package/lib/module/types/nonceAccount.js.map +1 -0
  28. package/lib/module/types/tossUser.js +16 -1
  29. package/lib/module/types/tossUser.js.map +1 -1
  30. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts +8 -0
  31. package/lib/typescript/src/__tests__/solana-program-simple.test.d.ts.map +1 -0
  32. package/lib/typescript/src/ble.d.ts +31 -2
  33. package/lib/typescript/src/ble.d.ts.map +1 -1
  34. package/lib/typescript/src/client/BLETransactionHandler.d.ts +98 -0
  35. package/lib/typescript/src/client/BLETransactionHandler.d.ts.map +1 -0
  36. package/lib/typescript/src/client/NonceAccountManager.d.ts +82 -0
  37. package/lib/typescript/src/client/NonceAccountManager.d.ts.map +1 -0
  38. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts +91 -0
  39. package/lib/typescript/src/hooks/useOfflineBLETransactions.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +9 -4
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/intent.d.ts +15 -0
  43. package/lib/typescript/src/intent.d.ts.map +1 -1
  44. package/lib/typescript/src/noise.d.ts +62 -0
  45. package/lib/typescript/src/noise.d.ts.map +1 -1
  46. package/lib/typescript/src/reconciliation.d.ts +6 -0
  47. package/lib/typescript/src/reconciliation.d.ts.map +1 -1
  48. package/lib/typescript/src/services/authService.d.ts +26 -1
  49. package/lib/typescript/src/services/authService.d.ts.map +1 -1
  50. package/lib/typescript/src/storage/secureStorage.d.ts +16 -0
  51. package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -1
  52. package/lib/typescript/src/sync.d.ts +6 -1
  53. package/lib/typescript/src/sync.d.ts.map +1 -1
  54. package/lib/typescript/src/types/nonceAccount.d.ts +59 -0
  55. package/lib/typescript/src/types/nonceAccount.d.ts.map +1 -0
  56. package/lib/typescript/src/types/tossUser.d.ts +16 -0
  57. package/lib/typescript/src/types/tossUser.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/__tests__/solana-program-simple.test.ts +256 -0
  60. package/src/ble.ts +105 -4
  61. package/src/client/BLETransactionHandler.ts +364 -0
  62. package/src/client/NonceAccountManager.ts +444 -0
  63. package/src/client/TossClient.ts +1 -1
  64. package/src/hooks/useOfflineBLETransactions.ts +438 -0
  65. package/src/index.tsx +40 -6
  66. package/src/intent.ts +166 -0
  67. package/src/noise.ts +238 -0
  68. package/src/reconciliation.ts +184 -0
  69. package/src/services/authService.ts +188 -1
  70. package/src/storage/secureStorage.ts +138 -0
  71. package/src/sync.ts +40 -0
  72. package/src/types/nonceAccount.ts +75 -0
  73. package/src/types/tossUser.ts +35 -2
@@ -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
  }