@sip-protocol/react-native 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 +21 -0
- package/README.md +186 -0
- package/dist/index.d.mts +442 -0
- package/dist/index.d.ts +442 -0
- package/dist/index.js +612 -0
- package/dist/index.mjs +575 -0
- package/package.json +86 -0
- package/src/hooks/index.ts +30 -0
- package/src/hooks/use-scan-payments.ts +288 -0
- package/src/hooks/use-stealth-address.ts +329 -0
- package/src/hooks/use-stealth-transfer.ts +252 -0
- package/src/index.ts +58 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/secure-storage.ts +395 -0
- package/src/utils/clipboard.ts +67 -0
- package/src/utils/index.ts +1 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/hooks/use-stealth-address.ts
|
|
9
|
+
import { useState, useEffect, useCallback } from "react";
|
|
10
|
+
|
|
11
|
+
// src/utils/clipboard.ts
|
|
12
|
+
var Clipboard = null;
|
|
13
|
+
try {
|
|
14
|
+
Clipboard = __require("@react-native-clipboard/clipboard").default;
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
async function copyToClipboard(text) {
|
|
18
|
+
if (!Clipboard) {
|
|
19
|
+
console.warn(
|
|
20
|
+
"@react-native-clipboard/clipboard is not available. Install it for clipboard functionality."
|
|
21
|
+
);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
Clipboard.setString(text);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function readFromClipboard() {
|
|
32
|
+
if (!Clipboard) {
|
|
33
|
+
console.warn(
|
|
34
|
+
"@react-native-clipboard/clipboard is not available. Install it for clipboard functionality."
|
|
35
|
+
);
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return await Clipboard.getString();
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function isClipboardAvailable() {
|
|
45
|
+
return !!Clipboard;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/storage/secure-storage.ts
|
|
49
|
+
var DEFAULT_SERVICE = "com.sip-protocol.keys";
|
|
50
|
+
var KEY_PREFIXES = {
|
|
51
|
+
spending: "sip:spending:",
|
|
52
|
+
viewing: "sip:viewing:",
|
|
53
|
+
ephemeral: "sip:ephemeral:",
|
|
54
|
+
meta: "sip:meta:"
|
|
55
|
+
};
|
|
56
|
+
var memoryStorage = /* @__PURE__ */ new Map();
|
|
57
|
+
var Keychain = null;
|
|
58
|
+
try {
|
|
59
|
+
Keychain = __require("react-native-keychain");
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
function getBackend(options) {
|
|
63
|
+
if (options?.backend) {
|
|
64
|
+
return options.backend;
|
|
65
|
+
}
|
|
66
|
+
return Keychain ? "keychain" : "memory";
|
|
67
|
+
}
|
|
68
|
+
function buildKeyName(type, identifier) {
|
|
69
|
+
return `${KEY_PREFIXES[type]}${identifier}`;
|
|
70
|
+
}
|
|
71
|
+
async function setKey(type, identifier, value, options) {
|
|
72
|
+
const keyName = buildKeyName(type, identifier);
|
|
73
|
+
const backend = getBackend(options);
|
|
74
|
+
if (backend === "memory") {
|
|
75
|
+
memoryStorage.set(keyName, value);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (!Keychain) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
'react-native-keychain is required for secure storage. Install it or use backend: "memory" for testing.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const service = options?.service ?? DEFAULT_SERVICE;
|
|
84
|
+
const keychainOptions = {
|
|
85
|
+
service,
|
|
86
|
+
accessible: mapAccessible(options?.accessible)
|
|
87
|
+
};
|
|
88
|
+
if (options?.requireBiometrics) {
|
|
89
|
+
keychainOptions.accessControl = Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET;
|
|
90
|
+
keychainOptions.authenticationType = Keychain.AUTHENTICATION_TYPE.BIOMETRICS;
|
|
91
|
+
}
|
|
92
|
+
const result = await Keychain.setGenericPassword(
|
|
93
|
+
keyName,
|
|
94
|
+
value,
|
|
95
|
+
keychainOptions
|
|
96
|
+
);
|
|
97
|
+
return !!result;
|
|
98
|
+
}
|
|
99
|
+
async function getKey(type, identifier, options) {
|
|
100
|
+
const keyName = buildKeyName(type, identifier);
|
|
101
|
+
const backend = getBackend(options);
|
|
102
|
+
if (backend === "memory") {
|
|
103
|
+
return memoryStorage.get(keyName) ?? null;
|
|
104
|
+
}
|
|
105
|
+
if (!Keychain) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'react-native-keychain is required for secure storage. Install it or use backend: "memory" for testing.'
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const service = options?.service ?? DEFAULT_SERVICE;
|
|
111
|
+
const keychainOptions = {
|
|
112
|
+
service
|
|
113
|
+
};
|
|
114
|
+
if (options?.requireBiometrics) {
|
|
115
|
+
keychainOptions.authenticationPrompt = {
|
|
116
|
+
title: "Authenticate to access key",
|
|
117
|
+
subtitle: "SIP Protocol requires authentication",
|
|
118
|
+
description: "Use biometrics to unlock your private keys",
|
|
119
|
+
cancel: "Cancel"
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const result = await Keychain.getGenericPassword(keychainOptions);
|
|
123
|
+
if (!result) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (result.username !== keyName) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return result.password;
|
|
130
|
+
}
|
|
131
|
+
async function deleteKey(type, identifier, options) {
|
|
132
|
+
const keyName = buildKeyName(type, identifier);
|
|
133
|
+
const backend = getBackend(options);
|
|
134
|
+
if (backend === "memory") {
|
|
135
|
+
return memoryStorage.delete(keyName);
|
|
136
|
+
}
|
|
137
|
+
if (!Keychain) {
|
|
138
|
+
throw new Error("react-native-keychain is required for secure storage.");
|
|
139
|
+
}
|
|
140
|
+
const service = options?.service ?? DEFAULT_SERVICE;
|
|
141
|
+
return await Keychain.resetGenericPassword({ service });
|
|
142
|
+
}
|
|
143
|
+
async function clearAll(options) {
|
|
144
|
+
const backend = getBackend(options);
|
|
145
|
+
if (backend === "memory") {
|
|
146
|
+
memoryStorage.clear();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (!Keychain) {
|
|
150
|
+
throw new Error("react-native-keychain is required for secure storage.");
|
|
151
|
+
}
|
|
152
|
+
const service = options?.service ?? DEFAULT_SERVICE;
|
|
153
|
+
return await Keychain.resetGenericPassword({ service });
|
|
154
|
+
}
|
|
155
|
+
function mapAccessible(level) {
|
|
156
|
+
if (!Keychain) return void 0;
|
|
157
|
+
switch (level) {
|
|
158
|
+
case "whenUnlocked":
|
|
159
|
+
return Keychain.ACCESSIBLE.WHEN_UNLOCKED;
|
|
160
|
+
case "afterFirstUnlock":
|
|
161
|
+
return Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK;
|
|
162
|
+
case "always":
|
|
163
|
+
return Keychain.ACCESSIBLE.ALWAYS;
|
|
164
|
+
default:
|
|
165
|
+
return Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function getSupportedBiometrics() {
|
|
169
|
+
if (!Keychain) {
|
|
170
|
+
return { available: false, biometryType: "None" };
|
|
171
|
+
}
|
|
172
|
+
const biometryType = await Keychain.getSupportedBiometryType();
|
|
173
|
+
if (!biometryType) {
|
|
174
|
+
return { available: false, biometryType: "None" };
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
available: true,
|
|
178
|
+
biometryType
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function isAvailable() {
|
|
182
|
+
return !!Keychain;
|
|
183
|
+
}
|
|
184
|
+
var SecureStorage = {
|
|
185
|
+
// Key operations
|
|
186
|
+
setKey,
|
|
187
|
+
getKey,
|
|
188
|
+
deleteKey,
|
|
189
|
+
clearAll,
|
|
190
|
+
// Convenience methods for viewing keys
|
|
191
|
+
setViewingKey: (identifier, key, options) => setKey("viewing", identifier, key, options),
|
|
192
|
+
getViewingKey: (identifier, options) => getKey("viewing", identifier, options),
|
|
193
|
+
deleteViewingKey: (identifier, options) => deleteKey("viewing", identifier, options),
|
|
194
|
+
// Convenience methods for spending keys
|
|
195
|
+
setSpendingKey: (identifier, key, options) => setKey("spending", identifier, key, options),
|
|
196
|
+
getSpendingKey: (identifier, options) => getKey("spending", identifier, options),
|
|
197
|
+
deleteSpendingKey: (identifier, options) => deleteKey("spending", identifier, options),
|
|
198
|
+
// Convenience methods for meta addresses
|
|
199
|
+
setMetaAddress: (identifier, meta, options) => setKey("meta", identifier, meta, options),
|
|
200
|
+
getMetaAddress: (identifier, options) => getKey("meta", identifier, options),
|
|
201
|
+
deleteMetaAddress: (identifier, options) => deleteKey("meta", identifier, options),
|
|
202
|
+
// Biometrics support
|
|
203
|
+
getSupportedBiometrics,
|
|
204
|
+
isAvailable
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// src/hooks/use-stealth-address.ts
|
|
208
|
+
function useStealthAddress(chain, options = {}) {
|
|
209
|
+
const { autoSave = false, requireBiometrics = false, walletId = "default" } = options;
|
|
210
|
+
const [metaAddress, setMetaAddress] = useState(null);
|
|
211
|
+
const [stealthAddress, setStealthAddress] = useState(null);
|
|
212
|
+
const [spendingPrivateKey, setSpendingPrivateKey] = useState(null);
|
|
213
|
+
const [viewingPrivateKey, setViewingPrivateKey] = useState(null);
|
|
214
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
215
|
+
const [error, setError] = useState(null);
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
let cancelled = false;
|
|
218
|
+
const generate = async () => {
|
|
219
|
+
setIsGenerating(true);
|
|
220
|
+
try {
|
|
221
|
+
const sdk = await import("@sip-protocol/sdk");
|
|
222
|
+
const generateStealthMetaAddress = sdk.generateStealthMetaAddress;
|
|
223
|
+
const metaAddressData = generateStealthMetaAddress(chain);
|
|
224
|
+
if (cancelled) return;
|
|
225
|
+
const encodeStealthMetaAddress = sdk.encodeStealthMetaAddress;
|
|
226
|
+
const encoded = encodeStealthMetaAddress(metaAddressData.metaAddress);
|
|
227
|
+
setMetaAddress(encoded);
|
|
228
|
+
setSpendingPrivateKey(metaAddressData.spendingPrivateKey);
|
|
229
|
+
setViewingPrivateKey(metaAddressData.viewingPrivateKey);
|
|
230
|
+
const generateStealthAddress = sdk.generateStealthAddress;
|
|
231
|
+
const stealthData = generateStealthAddress(metaAddressData.metaAddress);
|
|
232
|
+
if (cancelled) return;
|
|
233
|
+
setStealthAddress(stealthData.stealthAddress.address);
|
|
234
|
+
setError(null);
|
|
235
|
+
if (autoSave && !cancelled) {
|
|
236
|
+
await SecureStorage.setSpendingKey(walletId, metaAddressData.spendingPrivateKey, {
|
|
237
|
+
requireBiometrics
|
|
238
|
+
});
|
|
239
|
+
await SecureStorage.setViewingKey(walletId, metaAddressData.viewingPrivateKey, {
|
|
240
|
+
requireBiometrics
|
|
241
|
+
});
|
|
242
|
+
await SecureStorage.setMetaAddress(walletId, encoded, { requireBiometrics });
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (cancelled) return;
|
|
246
|
+
const error2 = err instanceof Error ? err : new Error("Failed to generate stealth addresses");
|
|
247
|
+
setError(error2);
|
|
248
|
+
setMetaAddress(null);
|
|
249
|
+
setStealthAddress(null);
|
|
250
|
+
setSpendingPrivateKey(null);
|
|
251
|
+
setViewingPrivateKey(null);
|
|
252
|
+
} finally {
|
|
253
|
+
if (!cancelled) {
|
|
254
|
+
setIsGenerating(false);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
generate();
|
|
259
|
+
return () => {
|
|
260
|
+
cancelled = true;
|
|
261
|
+
};
|
|
262
|
+
}, [chain, autoSave, requireBiometrics, walletId]);
|
|
263
|
+
const regenerate = useCallback(() => {
|
|
264
|
+
if (!metaAddress) return;
|
|
265
|
+
setIsGenerating(true);
|
|
266
|
+
setTimeout(async () => {
|
|
267
|
+
try {
|
|
268
|
+
const parts = metaAddress.split(":");
|
|
269
|
+
if (parts.length < 4) {
|
|
270
|
+
throw new Error("Invalid meta-address format");
|
|
271
|
+
}
|
|
272
|
+
const [, chainId, spendingKey, viewingKey] = parts;
|
|
273
|
+
const metaAddressObj = {
|
|
274
|
+
chain: chainId,
|
|
275
|
+
spendingKey: spendingKey.startsWith("0x") ? spendingKey : `0x${spendingKey}`,
|
|
276
|
+
viewingKey: viewingKey.startsWith("0x") ? viewingKey : `0x${viewingKey}`
|
|
277
|
+
};
|
|
278
|
+
const sdk = await import("@sip-protocol/sdk");
|
|
279
|
+
const generateStealthAddress = sdk.generateStealthAddress;
|
|
280
|
+
const stealthData = generateStealthAddress(metaAddressObj);
|
|
281
|
+
setStealthAddress(stealthData.stealthAddress.address);
|
|
282
|
+
setError(null);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
const error2 = err instanceof Error ? err : new Error("Failed to regenerate stealth address");
|
|
285
|
+
setError(error2);
|
|
286
|
+
} finally {
|
|
287
|
+
setIsGenerating(false);
|
|
288
|
+
}
|
|
289
|
+
}, 0);
|
|
290
|
+
}, [metaAddress]);
|
|
291
|
+
const copyToClipboard2 = useCallback(async () => {
|
|
292
|
+
if (!stealthAddress) return false;
|
|
293
|
+
const success = await copyToClipboard(stealthAddress);
|
|
294
|
+
if (!success) {
|
|
295
|
+
setError(new Error("Failed to copy to clipboard"));
|
|
296
|
+
} else {
|
|
297
|
+
setError(null);
|
|
298
|
+
}
|
|
299
|
+
return success;
|
|
300
|
+
}, [stealthAddress]);
|
|
301
|
+
const saveToKeychain = useCallback(async () => {
|
|
302
|
+
if (!spendingPrivateKey || !viewingPrivateKey || !metaAddress) {
|
|
303
|
+
setError(new Error("No keys to save"));
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
await SecureStorage.setSpendingKey(walletId, spendingPrivateKey, { requireBiometrics });
|
|
308
|
+
await SecureStorage.setViewingKey(walletId, viewingPrivateKey, { requireBiometrics });
|
|
309
|
+
await SecureStorage.setMetaAddress(walletId, metaAddress, { requireBiometrics });
|
|
310
|
+
setError(null);
|
|
311
|
+
return true;
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const error2 = err instanceof Error ? err : new Error("Failed to save to keychain");
|
|
314
|
+
setError(error2);
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}, [spendingPrivateKey, viewingPrivateKey, metaAddress, walletId, requireBiometrics]);
|
|
318
|
+
const loadFromKeychain = useCallback(async () => {
|
|
319
|
+
try {
|
|
320
|
+
const storedMeta = await SecureStorage.getMetaAddress(walletId, { requireBiometrics });
|
|
321
|
+
const storedSpending = await SecureStorage.getSpendingKey(walletId, { requireBiometrics });
|
|
322
|
+
const storedViewing = await SecureStorage.getViewingKey(walletId, { requireBiometrics });
|
|
323
|
+
if (!storedMeta || !storedSpending || !storedViewing) {
|
|
324
|
+
setError(new Error("No keys found in keychain"));
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
setMetaAddress(storedMeta);
|
|
328
|
+
setSpendingPrivateKey(storedSpending);
|
|
329
|
+
setViewingPrivateKey(storedViewing);
|
|
330
|
+
setError(null);
|
|
331
|
+
return true;
|
|
332
|
+
} catch (err) {
|
|
333
|
+
const error2 = err instanceof Error ? err : new Error("Failed to load from keychain");
|
|
334
|
+
setError(error2);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}, [walletId, requireBiometrics]);
|
|
338
|
+
const clearError = useCallback(() => {
|
|
339
|
+
setError(null);
|
|
340
|
+
}, []);
|
|
341
|
+
return {
|
|
342
|
+
metaAddress,
|
|
343
|
+
stealthAddress,
|
|
344
|
+
spendingPrivateKey,
|
|
345
|
+
viewingPrivateKey,
|
|
346
|
+
isGenerating,
|
|
347
|
+
error,
|
|
348
|
+
regenerate,
|
|
349
|
+
copyToClipboard: copyToClipboard2,
|
|
350
|
+
saveToKeychain,
|
|
351
|
+
loadFromKeychain,
|
|
352
|
+
clearError
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/hooks/use-stealth-transfer.ts
|
|
357
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
358
|
+
function useStealthTransfer(params) {
|
|
359
|
+
const { connection, wallet } = params;
|
|
360
|
+
const [status, setStatus] = useState2("idle");
|
|
361
|
+
const [error, setError] = useState2(null);
|
|
362
|
+
const transfer = useCallback2(
|
|
363
|
+
async (transferParams) => {
|
|
364
|
+
const { recipientMetaAddress, amount, mint } = transferParams;
|
|
365
|
+
if (!wallet.publicKey) {
|
|
366
|
+
const err = new Error("Wallet not connected");
|
|
367
|
+
setError(err);
|
|
368
|
+
setStatus("error");
|
|
369
|
+
return { success: false, error: err };
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
setStatus("preparing");
|
|
373
|
+
setError(null);
|
|
374
|
+
const sdk = await import("@sip-protocol/sdk");
|
|
375
|
+
if (!sdk.sendPrivateSPLTransfer) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"sendPrivateSPLTransfer not available. Install @sip-protocol/sdk with Solana support."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const sendPrivateSPLTransfer = sdk.sendPrivateSPLTransfer;
|
|
381
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
382
|
+
const { getAssociatedTokenAddress: getAssociatedTokenAddress2 } = await import("@solana/spl-token");
|
|
383
|
+
const mintPubkey = new PublicKey(mint);
|
|
384
|
+
const senderTokenAccount = await getAssociatedTokenAddress2(
|
|
385
|
+
mintPubkey,
|
|
386
|
+
new PublicKey(wallet.publicKey.toBase58())
|
|
387
|
+
);
|
|
388
|
+
setStatus("signing");
|
|
389
|
+
const result = await sendPrivateSPLTransfer({
|
|
390
|
+
connection,
|
|
391
|
+
sender: new PublicKey(wallet.publicKey.toBase58()),
|
|
392
|
+
senderTokenAccount,
|
|
393
|
+
recipientMetaAddress,
|
|
394
|
+
mint: mintPubkey,
|
|
395
|
+
amount,
|
|
396
|
+
signTransaction: wallet.signTransaction
|
|
397
|
+
});
|
|
398
|
+
setStatus("confirming");
|
|
399
|
+
const confirmation = await connection.confirmTransaction(
|
|
400
|
+
result.signature,
|
|
401
|
+
"confirmed"
|
|
402
|
+
);
|
|
403
|
+
if (confirmation.value.err) {
|
|
404
|
+
throw new Error(`Transaction failed: ${confirmation.value.err}`);
|
|
405
|
+
}
|
|
406
|
+
setStatus("success");
|
|
407
|
+
setError(null);
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
signature: result.signature,
|
|
411
|
+
stealthAddress: result.stealthAddress
|
|
412
|
+
};
|
|
413
|
+
} catch (err) {
|
|
414
|
+
const error2 = err instanceof Error ? err : new Error("Transfer failed");
|
|
415
|
+
setError(error2);
|
|
416
|
+
setStatus("error");
|
|
417
|
+
return { success: false, error: error2 };
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
[connection, wallet]
|
|
421
|
+
);
|
|
422
|
+
const reset = useCallback2(() => {
|
|
423
|
+
setStatus("idle");
|
|
424
|
+
setError(null);
|
|
425
|
+
}, []);
|
|
426
|
+
return {
|
|
427
|
+
transfer,
|
|
428
|
+
status,
|
|
429
|
+
error,
|
|
430
|
+
isLoading: status !== "idle" && status !== "success" && status !== "error",
|
|
431
|
+
reset
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async function getAssociatedTokenAddress(mint, owner) {
|
|
435
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
436
|
+
const { getAssociatedTokenAddress: getATA } = await import("@solana/spl-token");
|
|
437
|
+
const ata = await getATA(new PublicKey(mint), new PublicKey(owner));
|
|
438
|
+
return ata.toBase58();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/hooks/use-scan-payments.ts
|
|
442
|
+
import { useState as useState3, useCallback as useCallback3, useRef } from "react";
|
|
443
|
+
function useScanPayments(params) {
|
|
444
|
+
const { connection } = params;
|
|
445
|
+
const [payments, setPayments] = useState3([]);
|
|
446
|
+
const [status, setStatus] = useState3("idle");
|
|
447
|
+
const [error, setError] = useState3(null);
|
|
448
|
+
const scanningRef = useRef(false);
|
|
449
|
+
const scan = useCallback3(
|
|
450
|
+
async (viewingPrivateKey, spendingPublicKey) => {
|
|
451
|
+
if (scanningRef.current) return;
|
|
452
|
+
scanningRef.current = true;
|
|
453
|
+
try {
|
|
454
|
+
setStatus("scanning");
|
|
455
|
+
setError(null);
|
|
456
|
+
const sdk = await import("@sip-protocol/sdk");
|
|
457
|
+
if (!sdk.scanForPayments) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
"scanForPayments not available. Install @sip-protocol/sdk with Solana support."
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
const scanForPayments = sdk.scanForPayments;
|
|
463
|
+
const result = await scanForPayments({
|
|
464
|
+
connection,
|
|
465
|
+
viewingPrivateKey,
|
|
466
|
+
spendingPublicKey
|
|
467
|
+
});
|
|
468
|
+
const scannedPayments = result.map((p) => ({
|
|
469
|
+
signature: p.signature,
|
|
470
|
+
stealthAddress: p.stealthAddress,
|
|
471
|
+
ephemeralPublicKey: p.ephemeralPublicKey,
|
|
472
|
+
mint: p.mint,
|
|
473
|
+
amount: p.amount,
|
|
474
|
+
timestamp: p.timestamp ?? Date.now(),
|
|
475
|
+
claimed: false
|
|
476
|
+
}));
|
|
477
|
+
setPayments((prev) => {
|
|
478
|
+
const existingSigs = new Set(prev.map((p) => p.signature));
|
|
479
|
+
const newPayments = scannedPayments.filter((p) => !existingSigs.has(p.signature));
|
|
480
|
+
return [...prev, ...newPayments];
|
|
481
|
+
});
|
|
482
|
+
setStatus("idle");
|
|
483
|
+
} catch (err) {
|
|
484
|
+
const error2 = err instanceof Error ? err : new Error("Scan failed");
|
|
485
|
+
setError(error2);
|
|
486
|
+
setStatus("error");
|
|
487
|
+
} finally {
|
|
488
|
+
scanningRef.current = false;
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
[connection]
|
|
492
|
+
);
|
|
493
|
+
const claim = useCallback3(
|
|
494
|
+
async (signature, spendingPrivateKey, destinationAddress) => {
|
|
495
|
+
try {
|
|
496
|
+
setStatus("claiming");
|
|
497
|
+
setError(null);
|
|
498
|
+
const payment = payments.find((p) => p.signature === signature);
|
|
499
|
+
if (!payment) {
|
|
500
|
+
throw new Error("Payment not found");
|
|
501
|
+
}
|
|
502
|
+
const sdk = await import("@sip-protocol/sdk");
|
|
503
|
+
const { PublicKey } = await import("@solana/web3.js");
|
|
504
|
+
if (!sdk.claimStealthPayment) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
"claimStealthPayment not available. Install @sip-protocol/sdk with Solana support."
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const claimStealthPayment = sdk.claimStealthPayment;
|
|
510
|
+
const result = await claimStealthPayment({
|
|
511
|
+
connection,
|
|
512
|
+
stealthAddress: payment.stealthAddress,
|
|
513
|
+
ephemeralPublicKey: payment.ephemeralPublicKey,
|
|
514
|
+
viewingPrivateKey: "",
|
|
515
|
+
// Not needed for claiming
|
|
516
|
+
spendingPrivateKey,
|
|
517
|
+
destinationAddress: new PublicKey(destinationAddress),
|
|
518
|
+
mint: new PublicKey(payment.mint)
|
|
519
|
+
});
|
|
520
|
+
setPayments(
|
|
521
|
+
(prev) => prev.map(
|
|
522
|
+
(p) => p.signature === signature ? { ...p, claimed: true } : p
|
|
523
|
+
)
|
|
524
|
+
);
|
|
525
|
+
setStatus("idle");
|
|
526
|
+
return result.signature;
|
|
527
|
+
} catch (err) {
|
|
528
|
+
const error2 = err instanceof Error ? err : new Error("Claim failed");
|
|
529
|
+
setError(error2);
|
|
530
|
+
setStatus("error");
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
[connection, payments]
|
|
535
|
+
);
|
|
536
|
+
const claimAll = useCallback3(
|
|
537
|
+
async (spendingPrivateKey, destinationAddress) => {
|
|
538
|
+
const unclaimedPayments = payments.filter((p) => !p.claimed);
|
|
539
|
+
const signatures = [];
|
|
540
|
+
for (const payment of unclaimedPayments) {
|
|
541
|
+
const sig = await claim(payment.signature, spendingPrivateKey, destinationAddress);
|
|
542
|
+
if (sig) {
|
|
543
|
+
signatures.push(sig);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return signatures;
|
|
547
|
+
},
|
|
548
|
+
[payments, claim]
|
|
549
|
+
);
|
|
550
|
+
const clear = useCallback3(() => {
|
|
551
|
+
setPayments([]);
|
|
552
|
+
setError(null);
|
|
553
|
+
setStatus("idle");
|
|
554
|
+
}, []);
|
|
555
|
+
return {
|
|
556
|
+
payments,
|
|
557
|
+
status,
|
|
558
|
+
error,
|
|
559
|
+
isScanning: status === "scanning",
|
|
560
|
+
scan,
|
|
561
|
+
claim,
|
|
562
|
+
claimAll,
|
|
563
|
+
clear
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
export {
|
|
567
|
+
SecureStorage,
|
|
568
|
+
copyToClipboard,
|
|
569
|
+
getAssociatedTokenAddress,
|
|
570
|
+
isClipboardAvailable,
|
|
571
|
+
readFromClipboard,
|
|
572
|
+
useScanPayments,
|
|
573
|
+
useStealthAddress,
|
|
574
|
+
useStealthTransfer
|
|
575
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sip-protocol/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native SDK for Shielded Intents Protocol - privacy on iOS/Android",
|
|
5
|
+
"author": "SIP Protocol <hello@sip-protocol.org>",
|
|
6
|
+
"homepage": "https://sip-protocol.org",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/sip-protocol/sip-protocol.git",
|
|
10
|
+
"directory": "packages/react-native"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/sip-protocol/sip-protocol/issues"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.mjs",
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@sip-protocol/sdk": "0.7.3",
|
|
31
|
+
"@sip-protocol/types": "0.2.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.0.0",
|
|
35
|
+
"react-native": ">=0.71.0",
|
|
36
|
+
"@solana/web3.js": "^1.87.0",
|
|
37
|
+
"@solana/spl-token": "^0.4.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"react-native-keychain": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"@react-native-clipboard/clipboard": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"@solana/web3.js": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@solana/spl-token": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@solana/spl-token": "^0.4.0",
|
|
55
|
+
"@solana/web3.js": "^1.87.0",
|
|
56
|
+
"@testing-library/react-native": "^12.0.0",
|
|
57
|
+
"@types/node": "^20.0.0",
|
|
58
|
+
"@types/react": "^18.2.0",
|
|
59
|
+
"@types/react-native": "^0.72.0",
|
|
60
|
+
"react": "^18.2.0",
|
|
61
|
+
"react-native": "^0.73.0",
|
|
62
|
+
"tsup": "^8.0.0",
|
|
63
|
+
"typescript": "^5.3.0",
|
|
64
|
+
"vitest": "^1.1.0"
|
|
65
|
+
},
|
|
66
|
+
"keywords": [
|
|
67
|
+
"sip",
|
|
68
|
+
"privacy",
|
|
69
|
+
"react-native",
|
|
70
|
+
"mobile",
|
|
71
|
+
"solana",
|
|
72
|
+
"ethereum",
|
|
73
|
+
"stealth-addresses",
|
|
74
|
+
"ios",
|
|
75
|
+
"android"
|
|
76
|
+
],
|
|
77
|
+
"license": "MIT",
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --external react --external react-native",
|
|
80
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch --external react --external react-native",
|
|
81
|
+
"lint": "eslint --ext .ts,.tsx src/",
|
|
82
|
+
"typecheck": "tsc --noEmit",
|
|
83
|
+
"clean": "rm -rf dist",
|
|
84
|
+
"test": "vitest"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Hooks for SIP Protocol
|
|
3
|
+
*
|
|
4
|
+
* Mobile-optimized hooks with native storage and clipboard support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
useStealthAddress,
|
|
9
|
+
type UseStealthAddressOptions,
|
|
10
|
+
type UseStealthAddressReturn,
|
|
11
|
+
} from './use-stealth-address'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
useStealthTransfer,
|
|
15
|
+
type MobileWalletAdapter,
|
|
16
|
+
type TransferStatus,
|
|
17
|
+
type TransferParams,
|
|
18
|
+
type TransferResult,
|
|
19
|
+
type UseStealthTransferParams,
|
|
20
|
+
type UseStealthTransferReturn,
|
|
21
|
+
getAssociatedTokenAddress,
|
|
22
|
+
} from './use-stealth-transfer'
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
useScanPayments,
|
|
26
|
+
type ScannedPayment,
|
|
27
|
+
type ScanStatus,
|
|
28
|
+
type UseScanPaymentsParams,
|
|
29
|
+
type UseScanPaymentsReturn,
|
|
30
|
+
} from './use-scan-payments'
|