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.
- package/LICENSE +20 -0
- package/README.md +292 -0
- package/lib/module/ble.js +103 -0
- package/lib/module/ble.js.map +1 -0
- package/lib/module/client/TossClient.js +324 -0
- package/lib/module/client/TossClient.js.map +1 -0
- package/lib/module/client/index.js +4 -0
- package/lib/module/client/index.js.map +1 -0
- package/lib/module/contexts/WalletContext.js +99 -0
- package/lib/module/contexts/WalletContext.js.map +1 -0
- package/lib/module/discovery.js +434 -0
- package/lib/module/discovery.js.map +1 -0
- package/lib/module/errors.js +47 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/examples/offlinePaymentFlow.js +234 -0
- package/lib/module/examples/offlinePaymentFlow.js.map +1 -0
- package/lib/module/index.js +32 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/intent.js +223 -0
- package/lib/module/intent.js.map +1 -0
- package/lib/module/intentManager.js +145 -0
- package/lib/module/intentManager.js.map +1 -0
- package/lib/module/internal/arciumHelper.js +50 -0
- package/lib/module/internal/arciumHelper.js.map +1 -0
- package/lib/module/nfc.js +54 -0
- package/lib/module/nfc.js.map +1 -0
- package/lib/module/noise.js +14 -0
- package/lib/module/noise.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/qr.js +57 -0
- package/lib/module/qr.js.map +1 -0
- package/lib/module/reconciliation.js +329 -0
- package/lib/module/reconciliation.js.map +1 -0
- package/lib/module/services/authService.js +205 -0
- package/lib/module/services/authService.js.map +1 -0
- package/lib/module/storage/secureStorage.js +89 -0
- package/lib/module/storage/secureStorage.js.map +1 -0
- package/lib/module/storage.js +16 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/sync.js +64 -0
- package/lib/module/sync.js.map +1 -0
- package/lib/module/types/tossUser.js +41 -0
- package/lib/module/types/tossUser.js.map +1 -0
- package/lib/module/utils/nonceUtils.js +38 -0
- package/lib/module/utils/nonceUtils.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/__tests__/index.test.d.ts +1 -0
- package/lib/typescript/src/__tests__/index.test.d.ts.map +1 -0
- package/lib/typescript/src/__tests__/reconciliation.test.d.ts +6 -0
- package/lib/typescript/src/__tests__/reconciliation.test.d.ts.map +1 -0
- package/lib/typescript/src/ble.d.ts +10 -0
- package/lib/typescript/src/ble.d.ts.map +1 -0
- package/lib/typescript/src/client/TossClient.d.ts +110 -0
- package/lib/typescript/src/client/TossClient.d.ts.map +1 -0
- package/lib/typescript/src/client/index.d.ts +3 -0
- package/lib/typescript/src/client/index.d.ts.map +1 -0
- package/lib/typescript/src/contexts/WalletContext.d.ts +20 -0
- package/lib/typescript/src/contexts/WalletContext.d.ts.map +1 -0
- package/lib/typescript/src/discovery.d.ts +188 -0
- package/lib/typescript/src/discovery.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +27 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/examples/offlinePaymentFlow.d.ts +48 -0
- package/lib/typescript/src/examples/offlinePaymentFlow.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +13 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/intent.d.ts +84 -0
- package/lib/typescript/src/intent.d.ts.map +1 -0
- package/lib/typescript/src/intentManager.d.ts +46 -0
- package/lib/typescript/src/intentManager.d.ts.map +1 -0
- package/lib/typescript/src/internal/arciumHelper.d.ts +19 -0
- package/lib/typescript/src/internal/arciumHelper.d.ts.map +1 -0
- package/lib/typescript/src/nfc.d.ts +7 -0
- package/lib/typescript/src/nfc.d.ts.map +1 -0
- package/lib/typescript/src/noise.d.ts +5 -0
- package/lib/typescript/src/noise.d.ts.map +1 -0
- package/lib/typescript/src/qr.d.ts +6 -0
- package/lib/typescript/src/qr.d.ts.map +1 -0
- package/lib/typescript/src/reconciliation.d.ts +65 -0
- package/lib/typescript/src/reconciliation.d.ts.map +1 -0
- package/lib/typescript/src/services/authService.d.ts +55 -0
- package/lib/typescript/src/services/authService.d.ts.map +1 -0
- package/lib/typescript/src/storage/secureStorage.d.ts +7 -0
- package/lib/typescript/src/storage/secureStorage.d.ts.map +1 -0
- package/lib/typescript/src/storage.d.ts +4 -0
- package/lib/typescript/src/storage.d.ts.map +1 -0
- package/lib/typescript/src/sync.d.ts +40 -0
- package/lib/typescript/src/sync.d.ts.map +1 -0
- package/lib/typescript/src/types/tossUser.d.ts +39 -0
- package/lib/typescript/src/types/tossUser.d.ts.map +1 -0
- package/lib/typescript/src/utils/nonceUtils.d.ts +8 -0
- package/lib/typescript/src/utils/nonceUtils.d.ts.map +1 -0
- package/package.json +176 -0
- package/src/__tests__/index.test.tsx +1 -0
- package/src/__tests__/reconciliation.test.tsx +361 -0
- package/src/ble.ts +138 -0
- package/src/client/TossClient.ts +435 -0
- package/src/client/index.ts +2 -0
- package/src/contexts/WalletContext.tsx +127 -0
- package/src/discovery.ts +542 -0
- package/src/errors.ts +51 -0
- package/src/examples/offlinePaymentFlow.ts +331 -0
- package/src/index.tsx +61 -0
- package/src/intent.ts +328 -0
- package/src/intentManager.ts +164 -0
- package/src/internal/arciumHelper.ts +58 -0
- package/src/nfc.ts +57 -0
- package/src/noise.ts +9 -0
- package/src/qr.tsx +65 -0
- package/src/reconciliation.ts +421 -0
- package/src/services/authService.ts +238 -0
- package/src/storage/secureStorage.ts +100 -0
- package/src/storage.ts +17 -0
- package/src/sync.ts +101 -0
- package/src/types/tossUser.ts +81 -0
- package/src/utils/nonceUtils.ts +56 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { Transaction } from '@solana/web3.js';
|
|
2
|
+
import bs58 from 'bs58';
|
|
3
|
+
import type { SolanaIntent, IntentStatus } from './intent';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Verifies the signature of a Solana intent
|
|
7
|
+
* @param intent The intent to verify
|
|
8
|
+
* @returns boolean indicating if the signature is valid
|
|
9
|
+
*/
|
|
10
|
+
export function verifyIntentSignature(intent: SolanaIntent): boolean {
|
|
11
|
+
try {
|
|
12
|
+
// Check if serialized transaction exists
|
|
13
|
+
if (!intent.serialized) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Reconstruct the transaction to verify signatures
|
|
18
|
+
const tx = Transaction.from(Buffer.from(bs58.decode(intent.serialized)));
|
|
19
|
+
|
|
20
|
+
// Verify all signatures in the transaction
|
|
21
|
+
return tx.verifySignatures();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error verifying intent signature:', error);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Checks if an intent has expired
|
|
30
|
+
* @param intent The intent to check
|
|
31
|
+
* @returns boolean indicating if the intent has expired
|
|
32
|
+
*/
|
|
33
|
+
export function isIntentExpired(intent: SolanaIntent): boolean {
|
|
34
|
+
return Date.now() / 1000 > intent.expiry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Updates the status of an intent
|
|
39
|
+
* @param intent The intent to update
|
|
40
|
+
* @param status The new status
|
|
41
|
+
* @param error Optional error message if status is 'failed'
|
|
42
|
+
* @returns A new intent with updated status
|
|
43
|
+
*/
|
|
44
|
+
export function updateIntentStatus(
|
|
45
|
+
intent: SolanaIntent,
|
|
46
|
+
status: IntentStatus,
|
|
47
|
+
error?: string
|
|
48
|
+
): SolanaIntent {
|
|
49
|
+
return {
|
|
50
|
+
...intent,
|
|
51
|
+
status,
|
|
52
|
+
updatedAt: Math.floor(Date.now() / 1000),
|
|
53
|
+
...(error && { error }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates an intent against current blockchain state
|
|
59
|
+
* Note: This is a placeholder - actual implementation would check against a Solana node
|
|
60
|
+
* @param intent The intent to validate
|
|
61
|
+
* @param currentBlockhash Current network blockhash
|
|
62
|
+
* @returns Validation result with success status and optional error message
|
|
63
|
+
*/
|
|
64
|
+
export async function validateIntent(
|
|
65
|
+
intent: SolanaIntent,
|
|
66
|
+
currentBlockhash: string
|
|
67
|
+
): Promise<{ valid: boolean; error?: string }> {
|
|
68
|
+
// Check if intent has expired
|
|
69
|
+
if (isIntentExpired(intent)) {
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
error: 'Intent has expired',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Verify signatures
|
|
77
|
+
if (!verifyIntentSignature(intent)) {
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
error: 'Invalid transaction signature',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if blockhash is still valid
|
|
85
|
+
// In a real implementation, we'd check against the Solana cluster
|
|
86
|
+
// This is a simplified check
|
|
87
|
+
if (intent.blockhash !== currentBlockhash) {
|
|
88
|
+
return {
|
|
89
|
+
valid: false,
|
|
90
|
+
error: 'Stale blockhash',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Additional validation logic would go here
|
|
95
|
+
// - Check account balances
|
|
96
|
+
// - Verify program-specific logic
|
|
97
|
+
// - Check for double-spend attempts
|
|
98
|
+
|
|
99
|
+
return { valid: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Processes a batch of intents for synchronization
|
|
104
|
+
* @param intents Array of intents to process
|
|
105
|
+
* @param connection Connection to Solana network
|
|
106
|
+
* @returns Processed intents with updated statuses
|
|
107
|
+
*/
|
|
108
|
+
export async function processIntentsForSync(
|
|
109
|
+
intents: SolanaIntent[],
|
|
110
|
+
connection: any // Should be Connection from @solana/web3.js in real implementation
|
|
111
|
+
): Promise<SolanaIntent[]> {
|
|
112
|
+
const currentBlockhash = (await connection.getRecentBlockhash()).blockhash;
|
|
113
|
+
|
|
114
|
+
return Promise.all(
|
|
115
|
+
intents.map(async (intent) => {
|
|
116
|
+
// Skip already processed intents
|
|
117
|
+
if (intent.status !== 'pending') return intent;
|
|
118
|
+
|
|
119
|
+
const validation = await validateIntent(intent, currentBlockhash);
|
|
120
|
+
|
|
121
|
+
if (!validation.valid) {
|
|
122
|
+
return updateIntentStatus(intent, 'failed', validation.error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// In a real implementation, we would submit the transaction here
|
|
127
|
+
// const signature = await connection.sendRawTransaction(
|
|
128
|
+
// Buffer.from(bs58.decode(intent.serialized))
|
|
129
|
+
// );
|
|
130
|
+
// await connection.confirmTransaction(signature);
|
|
131
|
+
|
|
132
|
+
return updateIntentStatus(intent, 'settled');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return updateIntentStatus(
|
|
135
|
+
intent,
|
|
136
|
+
'failed',
|
|
137
|
+
error instanceof Error ? error.message : 'Unknown error'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Filters out expired intents and updates their status
|
|
146
|
+
* @param intents Array of intents to check
|
|
147
|
+
* @returns Tuple of [validIntents, expiredIntents]
|
|
148
|
+
*/
|
|
149
|
+
export function filterExpiredIntents(
|
|
150
|
+
intents: SolanaIntent[]
|
|
151
|
+
): [SolanaIntent[], SolanaIntent[]] {
|
|
152
|
+
const valid: SolanaIntent[] = [];
|
|
153
|
+
const expired: SolanaIntent[] = [];
|
|
154
|
+
|
|
155
|
+
for (const intent of intents) {
|
|
156
|
+
if (isIntentExpired(intent)) {
|
|
157
|
+
expired.push(updateIntentStatus(intent, 'expired'));
|
|
158
|
+
} else {
|
|
159
|
+
valid.push(intent);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [valid, expired];
|
|
164
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// import {
|
|
2
|
+
// getArciumEnv,
|
|
3
|
+
// x25519,
|
|
4
|
+
// getMXEPublicKey,
|
|
5
|
+
// RescueCipher,
|
|
6
|
+
// } from "@arcium-hq/client";
|
|
7
|
+
import * as Arcium from '@arcium-hq/client';
|
|
8
|
+
import type { PublicKey } from '@solana/web3.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Output from Arcium encryption (internal only)
|
|
12
|
+
*/
|
|
13
|
+
export type ArciumEncryptedOutput = {
|
|
14
|
+
ciphertext: number[][];
|
|
15
|
+
publicKey: Uint8Array;
|
|
16
|
+
nonce: Uint8Array;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Internal helper to encrypt a set of numeric values with Arcium.
|
|
20
|
+
* Does not leak anything about Arcium to the SDK consumer.
|
|
21
|
+
*
|
|
22
|
+
* @param mxeProgramId PublicKey of the MXE
|
|
23
|
+
* @param plaintextValues numeric values for encryption
|
|
24
|
+
* @param provider Solana provider (e.g., AnchorProvider)
|
|
25
|
+
*/
|
|
26
|
+
export async function encryptForArciumInternal(
|
|
27
|
+
mxeProgramId: PublicKey,
|
|
28
|
+
plaintextValues: bigint[],
|
|
29
|
+
provider: any // AnchorProvider or similar
|
|
30
|
+
): Promise<ArciumEncryptedOutput> {
|
|
31
|
+
// Required by the Arcium client before encryption
|
|
32
|
+
Arcium.getArciumEnv();
|
|
33
|
+
|
|
34
|
+
// 1) Generate a random x25519 keypair
|
|
35
|
+
const privateKey = Arcium.x25519.utils.randomSecretKey();
|
|
36
|
+
const publicKey = Arcium.x25519.getPublicKey(privateKey);
|
|
37
|
+
|
|
38
|
+
// 2) Fetch the MXE's public encryption key using the provided provider
|
|
39
|
+
const mxePubKey = await Arcium.getMXEPublicKey(provider, mxeProgramId);
|
|
40
|
+
|
|
41
|
+
if (!mxePubKey) {
|
|
42
|
+
throw new Error('MXE public key not found for Arcium encryption');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 3) Derive DH shared secret
|
|
46
|
+
const sharedSecret = Arcium.x25519.getSharedSecret(privateKey, mxePubKey);
|
|
47
|
+
|
|
48
|
+
// 4) Build the cipher and encrypt the data
|
|
49
|
+
const cipher = new Arcium.RescueCipher(sharedSecret);
|
|
50
|
+
const nonce = crypto.getRandomValues(new Uint8Array(16));
|
|
51
|
+
const ciphertext = cipher.encrypt(plaintextValues, nonce);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ciphertext,
|
|
55
|
+
publicKey,
|
|
56
|
+
nonce,
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/nfc.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/nfc.ts
|
|
2
|
+
import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager";
|
|
3
|
+
import type { TossUser } from './types/tossUser';
|
|
4
|
+
import type { SolanaIntent } from './intent';
|
|
5
|
+
|
|
6
|
+
// Start the manager
|
|
7
|
+
export function initNFC() {
|
|
8
|
+
return NfcManager.start();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Read NFC tag containing a TossUser
|
|
12
|
+
export async function readNFCUser(): Promise<TossUser> {
|
|
13
|
+
try {
|
|
14
|
+
await NfcManager.requestTechnology(NfcTech.Ndef);
|
|
15
|
+
const tag = await NfcManager.getTag();
|
|
16
|
+
await NfcManager.cancelTechnologyRequest();
|
|
17
|
+
|
|
18
|
+
if (!tag?.ndefMessage?.[0]?.payload) {
|
|
19
|
+
throw new Error('No NDEF message found');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const message = Ndef.uri.decodePayload(tag.ndefMessage[0].payload as any);
|
|
23
|
+
return JSON.parse(message) as TossUser;
|
|
24
|
+
} catch (ex: unknown) {
|
|
25
|
+
await NfcManager.cancelTechnologyRequest();
|
|
26
|
+
throw new Error(`Failed to read user from NFC: ${String(ex)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function writeUserToNFC(user: TossUser): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
await NfcManager.requestTechnology(NfcTech.Ndef);
|
|
33
|
+
const jsonUser = JSON.stringify(user);
|
|
34
|
+
const bytes = Ndef.encodeMessage([Ndef.uriRecord(jsonUser)]);
|
|
35
|
+
await NfcManager.ndefHandler.writeNdefMessage(bytes);
|
|
36
|
+
await NfcManager.cancelTechnologyRequest();
|
|
37
|
+
return true;
|
|
38
|
+
} catch (ex: unknown) {
|
|
39
|
+
await NfcManager.cancelTechnologyRequest();
|
|
40
|
+
throw new Error(`Failed to write user to NFC: ${String(ex)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Write SolanaIntent to NFC tag
|
|
45
|
+
export async function writeIntentToNFC(intent: SolanaIntent): Promise<boolean> {
|
|
46
|
+
try {
|
|
47
|
+
await NfcManager.requestTechnology(NfcTech.Ndef);
|
|
48
|
+
const jsonIntent = JSON.stringify(intent);
|
|
49
|
+
const bytes = Ndef.encodeMessage([Ndef.uriRecord(jsonIntent)]);
|
|
50
|
+
await NfcManager.ndefHandler.writeNdefMessage(bytes);
|
|
51
|
+
await NfcManager.cancelTechnologyRequest();
|
|
52
|
+
return true;
|
|
53
|
+
} catch (ex: unknown) {
|
|
54
|
+
await NfcManager.cancelTechnologyRequest();
|
|
55
|
+
throw new Error(`Failed to write intent to NFC: ${String(ex)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/noise.ts
ADDED
package/src/qr.tsx
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { View, StyleSheet, Text } from 'react-native';
|
|
2
|
+
import {
|
|
3
|
+
Camera,
|
|
4
|
+
useCameraDevice,
|
|
5
|
+
useCameraPermission,
|
|
6
|
+
useCodeScanner,
|
|
7
|
+
type Code,
|
|
8
|
+
} from 'react-native-vision-camera';
|
|
9
|
+
|
|
10
|
+
type QRScannerProps = {
|
|
11
|
+
onScan: (data: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function QRScanner({ onScan }: QRScannerProps) {
|
|
15
|
+
const device = useCameraDevice('back');
|
|
16
|
+
const permission = useCameraPermission();
|
|
17
|
+
|
|
18
|
+
const codeScanner = useCodeScanner({
|
|
19
|
+
codeTypes: ['qr'], // ✅ correct CodeType
|
|
20
|
+
onCodeScanned: (codes: Code[]) => {
|
|
21
|
+
const code = codes[0];
|
|
22
|
+
if (!code?.value) return; // ✅ undefined-safe
|
|
23
|
+
|
|
24
|
+
onScan(code.value);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!permission.hasPermission) {
|
|
29
|
+
return (
|
|
30
|
+
<View style={styles.center}>
|
|
31
|
+
<Text>Camera permission not granted</Text>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!device) {
|
|
37
|
+
return (
|
|
38
|
+
<View style={styles.center}>
|
|
39
|
+
<Text>Camera not available</Text>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View style={styles.container}>
|
|
46
|
+
<Camera
|
|
47
|
+
style={StyleSheet.absoluteFill}
|
|
48
|
+
device={device}
|
|
49
|
+
isActive
|
|
50
|
+
codeScanner={codeScanner}
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const styles = StyleSheet.create({
|
|
57
|
+
container: {
|
|
58
|
+
flex: 1,
|
|
59
|
+
},
|
|
60
|
+
center: {
|
|
61
|
+
flex: 1,
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
justifyContent: 'center',
|
|
64
|
+
},
|
|
65
|
+
});
|