@baerae/zkap-zkp-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/README.md +26 -0
- package/android/build.gradle +30 -0
- package/android/libs/arm64-v8a/libzkap_zkp_rn.so +0 -0
- package/android/libs/armeabi-v7a/libzkap_zkp_rn.so +0 -0
- package/android/libs/x86_64/libzkap_zkp_rn.so +0 -0
- package/android/src/main/java/expo/modules/zkap/ZkapSdkModule.kt +64 -0
- package/expo-module.config.json +9 -0
- package/ios/ZkapSdkModule.podspec +34 -0
- package/ios/ZkapSdkModule.swift +105 -0
- package/ios/ZkapZkp.xcframework/Info.plist +48 -0
- package/ios/ZkapZkp.xcframework/ios-arm64/Headers/zkap_zkp_rn.h +15 -0
- package/ios/ZkapZkp.xcframework/ios-arm64/libzkap_zkp_rn.a +0 -0
- package/ios/ZkapZkp.xcframework/ios-arm64_x86_64-simulator/Headers/zkap_zkp_rn.h +15 -0
- package/ios/ZkapZkp.xcframework/ios-arm64_x86_64-simulator/libzkap_zkp_rn_sim.a +0 -0
- package/ios/include/zkap_zkp_rn.h +15 -0
- package/package.json +58 -0
- package/src/artifact-manager.ts +231 -0
- package/src/index.ts +147 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# `@baerae/zkap-zkp-react-native`
|
|
2
|
+
|
|
3
|
+
React Native SDK for zkap-zkp with Expo modules integration.
|
|
4
|
+
|
|
5
|
+
Install:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @baerae/zkap-zkp-react-native
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Expo New Architecture and the following peer dependencies:
|
|
12
|
+
|
|
13
|
+
- `expo`
|
|
14
|
+
- `expo-crypto`
|
|
15
|
+
- `expo-file-system`
|
|
16
|
+
- `expo-modules-core`
|
|
17
|
+
- `react-native`
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { generateHash, generateAnchor } from '@baerae/zkap-zkp-react-native'
|
|
21
|
+
|
|
22
|
+
const hash = await generateHash(['0x1', '0x2'])
|
|
23
|
+
const anchor = await generateAnchor(config, secrets)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The npm package bundles the iOS XCFramework and Android `.so` libraries produced by CI.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
|
|
4
|
+
android {
|
|
5
|
+
compileSdkVersion 34
|
|
6
|
+
namespace "expo.modules.zkap"
|
|
7
|
+
|
|
8
|
+
defaultConfig {
|
|
9
|
+
minSdkVersion 24
|
|
10
|
+
targetSdkVersion 34
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
sourceSets {
|
|
14
|
+
main {
|
|
15
|
+
java.srcDirs += ['src/main/java']
|
|
16
|
+
jniLibs.srcDirs = ['libs']
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
repositories {
|
|
22
|
+
mavenCentral()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
dependencies {
|
|
26
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0"
|
|
27
|
+
// expo-modules-core provides Module, ModuleDefinition, AsyncFunction, etc.
|
|
28
|
+
// compileOnly: provided by the consumer app at runtime
|
|
29
|
+
compileOnly project(':expo-modules-core')
|
|
30
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package expo.modules.zkap
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
|
|
6
|
+
class ZkapSdkModule : Module() {
|
|
7
|
+
|
|
8
|
+
// Load the native Rust library (built as libzkap_zkp_rn.so)
|
|
9
|
+
companion object {
|
|
10
|
+
init {
|
|
11
|
+
System.loadLibrary("zkap_zkp_rn")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// JNI-compatible native methods (no underscores → no name-mangling issues).
|
|
15
|
+
// Corresponding Rust symbol: Java_expo_modules_zkap_ZkapSdkModule_native<X>
|
|
16
|
+
// Each JNI wrapper in Rust calls the C-ABI function, copies the result into
|
|
17
|
+
// a Java String, and frees the Rust allocation — no memory leak.
|
|
18
|
+
@JvmStatic external fun nativeGenerateHash(inputJson: String): String
|
|
19
|
+
@JvmStatic external fun nativeGenerateAnchor(inputJson: String): String
|
|
20
|
+
@JvmStatic external fun nativeGenerateAudHash(inputJson: String): String
|
|
21
|
+
@JvmStatic external fun nativeGenerateLeafHash(inputJson: String): String
|
|
22
|
+
@JvmStatic external fun nativeProve(inputJson: String): String
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
override fun definition() = ModuleDefinition {
|
|
26
|
+
Name("ZkapSdk")
|
|
27
|
+
|
|
28
|
+
AsyncFunction("generateHash") { inputJson: String ->
|
|
29
|
+
unwrapResult(nativeGenerateHash(inputJson))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
AsyncFunction("generateAnchor") { inputJson: String ->
|
|
33
|
+
unwrapResult(nativeGenerateAnchor(inputJson))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
AsyncFunction("generateAudHash") { inputJson: String ->
|
|
37
|
+
unwrapResult(nativeGenerateAudHash(inputJson))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
AsyncFunction("generateLeafHash") { inputJson: String ->
|
|
41
|
+
unwrapResult(nativeGenerateLeafHash(inputJson))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
AsyncFunction("prove") { inputJson: String ->
|
|
45
|
+
unwrapResult(nativeProve(inputJson))
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Parse JSON result from Rust FFI. Throws if the result contains an "error" field. */
|
|
50
|
+
@Suppress("UNCHECKED_CAST")
|
|
51
|
+
private fun unwrapResult(json: String): String {
|
|
52
|
+
try {
|
|
53
|
+
val obj = org.json.JSONObject(json)
|
|
54
|
+
if (obj.has("error")) {
|
|
55
|
+
throw ZkapException(obj.getString("error"))
|
|
56
|
+
}
|
|
57
|
+
} catch (e: org.json.JSONException) {
|
|
58
|
+
// Not a JSON object — return as-is
|
|
59
|
+
}
|
|
60
|
+
return json
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class ZkapException(message: String) : Exception("[zkap] $message")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ZkapSdkModule'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.authors = package['author']
|
|
11
|
+
s.homepage = package['homepage']
|
|
12
|
+
s.platform = :ios, '15.0'
|
|
13
|
+
s.swift_version = '5.9'
|
|
14
|
+
s.source = { git: package['repository']['url'] }
|
|
15
|
+
|
|
16
|
+
s.source_files = 'ios/**/*.{swift,h,m}'
|
|
17
|
+
|
|
18
|
+
# Pre-built Rust static libraries (generated by CI build-react-native.yml)
|
|
19
|
+
s.vendored_frameworks = 'ios/ZkapZkp.xcframework'
|
|
20
|
+
|
|
21
|
+
# Expose C headers for the Rust FFI symbols
|
|
22
|
+
s.preserve_paths = 'ios/include/*.h'
|
|
23
|
+
s.xcconfig = {
|
|
24
|
+
'HEADER_SEARCH_PATHS' => '$(PODS_TARGET_SRCROOT)/ios/include',
|
|
25
|
+
'OTHER_LDFLAGS' => '-lc++'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
s.dependency 'ExpoModulesCore'
|
|
29
|
+
|
|
30
|
+
s.pod_target_xcconfig = {
|
|
31
|
+
'DEFINES_MODULE' => 'YES',
|
|
32
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
33
|
+
}
|
|
34
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ZkapSdkModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
Name("ZkapSdk")
|
|
6
|
+
|
|
7
|
+
// ──────────────────────────────────────────
|
|
8
|
+
// generateHash
|
|
9
|
+
// ──────────────────────────────────────────
|
|
10
|
+
AsyncFunction("generateHash") { (inputJson: String) -> String in
|
|
11
|
+
guard let cInput = inputJson.cString(using: .utf8) else {
|
|
12
|
+
throw ZkapError.invalidInput("Failed to encode inputJson as UTF-8")
|
|
13
|
+
}
|
|
14
|
+
let result = zkap_generate_hash(cInput)
|
|
15
|
+
defer { zkap_free_string(result) }
|
|
16
|
+
return try ZkapSdkModule.unwrapResult(result)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ──────────────────────────────────────────
|
|
20
|
+
// generateAnchor
|
|
21
|
+
// ──────────────────────────────────────────
|
|
22
|
+
AsyncFunction("generateAnchor") { (inputJson: String) -> String in
|
|
23
|
+
guard let cInput = inputJson.cString(using: .utf8) else {
|
|
24
|
+
throw ZkapError.invalidInput("Failed to encode inputJson as UTF-8")
|
|
25
|
+
}
|
|
26
|
+
let result = zkap_generate_anchor(cInput)
|
|
27
|
+
defer { zkap_free_string(result) }
|
|
28
|
+
return try ZkapSdkModule.unwrapResult(result)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ──────────────────────────────────────────
|
|
32
|
+
// generateAudHash
|
|
33
|
+
// ──────────────────────────────────────────
|
|
34
|
+
AsyncFunction("generateAudHash") { (inputJson: String) -> String in
|
|
35
|
+
guard let cInput = inputJson.cString(using: .utf8) else {
|
|
36
|
+
throw ZkapError.invalidInput("Failed to encode inputJson as UTF-8")
|
|
37
|
+
}
|
|
38
|
+
let result = zkap_generate_aud_hash(cInput)
|
|
39
|
+
defer { zkap_free_string(result) }
|
|
40
|
+
return try ZkapSdkModule.unwrapResult(result)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ──────────────────────────────────────────
|
|
44
|
+
// generateLeafHash
|
|
45
|
+
// ──────────────────────────────────────────
|
|
46
|
+
AsyncFunction("generateLeafHash") { (inputJson: String) -> String in
|
|
47
|
+
guard let cInput = inputJson.cString(using: .utf8) else {
|
|
48
|
+
throw ZkapError.invalidInput("Failed to encode inputJson as UTF-8")
|
|
49
|
+
}
|
|
50
|
+
let result = zkap_generate_leaf_hash(cInput)
|
|
51
|
+
defer { zkap_free_string(result) }
|
|
52
|
+
return try ZkapSdkModule.unwrapResult(result)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ──────────────────────────────────────────
|
|
56
|
+
// prove
|
|
57
|
+
// ──────────────────────────────────────────
|
|
58
|
+
AsyncFunction("prove") { (inputJson: String) -> String in
|
|
59
|
+
guard let cInput = inputJson.cString(using: .utf8) else {
|
|
60
|
+
throw ZkapError.invalidInput("Failed to encode inputJson as UTF-8")
|
|
61
|
+
}
|
|
62
|
+
let result = zkap_prove(cInput)
|
|
63
|
+
defer { zkap_free_string(result) }
|
|
64
|
+
return try ZkapSdkModule.unwrapResult(result)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ──────────────────────────────────────────────────────────────
|
|
69
|
+
// Helpers
|
|
70
|
+
// ──────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/// Convert a raw C string pointer returned by the Rust FFI into a Swift String.
|
|
73
|
+
/// Throws ZkapError.rustError if the JSON contains an "error" field.
|
|
74
|
+
private static func unwrapResult(_ ptr: UnsafeMutablePointer<CChar>?) throws -> String {
|
|
75
|
+
guard let ptr = ptr else {
|
|
76
|
+
throw ZkapError.rustError("Rust FFI returned null pointer")
|
|
77
|
+
}
|
|
78
|
+
let json = String(cString: ptr)
|
|
79
|
+
|
|
80
|
+
// Check for error response: {"error": "..."}
|
|
81
|
+
if let data = json.data(using: .utf8),
|
|
82
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
83
|
+
let errMsg = obj["error"] as? String {
|
|
84
|
+
throw ZkapError.rustError(errMsg)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return json
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ──────────────────────────────────────────────────────────────────
|
|
92
|
+
// Error types
|
|
93
|
+
// ──────────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
enum ZkapError: Error, CustomStringConvertible {
|
|
96
|
+
case invalidInput(String)
|
|
97
|
+
case rustError(String)
|
|
98
|
+
|
|
99
|
+
var description: String {
|
|
100
|
+
switch self {
|
|
101
|
+
case .invalidInput(let msg): return "[zkap] Invalid input: \(msg)"
|
|
102
|
+
case .rustError(let msg): return "[zkap] Rust error: \(msg)"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>AvailableLibraries</key>
|
|
6
|
+
<array>
|
|
7
|
+
<dict>
|
|
8
|
+
<key>BinaryPath</key>
|
|
9
|
+
<string>libzkap_zkp_rn_sim.a</string>
|
|
10
|
+
<key>HeadersPath</key>
|
|
11
|
+
<string>Headers</string>
|
|
12
|
+
<key>LibraryIdentifier</key>
|
|
13
|
+
<string>ios-arm64_x86_64-simulator</string>
|
|
14
|
+
<key>LibraryPath</key>
|
|
15
|
+
<string>libzkap_zkp_rn_sim.a</string>
|
|
16
|
+
<key>SupportedArchitectures</key>
|
|
17
|
+
<array>
|
|
18
|
+
<string>arm64</string>
|
|
19
|
+
<string>x86_64</string>
|
|
20
|
+
</array>
|
|
21
|
+
<key>SupportedPlatform</key>
|
|
22
|
+
<string>ios</string>
|
|
23
|
+
<key>SupportedPlatformVariant</key>
|
|
24
|
+
<string>simulator</string>
|
|
25
|
+
</dict>
|
|
26
|
+
<dict>
|
|
27
|
+
<key>BinaryPath</key>
|
|
28
|
+
<string>libzkap_zkp_rn.a</string>
|
|
29
|
+
<key>HeadersPath</key>
|
|
30
|
+
<string>Headers</string>
|
|
31
|
+
<key>LibraryIdentifier</key>
|
|
32
|
+
<string>ios-arm64</string>
|
|
33
|
+
<key>LibraryPath</key>
|
|
34
|
+
<string>libzkap_zkp_rn.a</string>
|
|
35
|
+
<key>SupportedArchitectures</key>
|
|
36
|
+
<array>
|
|
37
|
+
<string>arm64</string>
|
|
38
|
+
</array>
|
|
39
|
+
<key>SupportedPlatform</key>
|
|
40
|
+
<string>ios</string>
|
|
41
|
+
</dict>
|
|
42
|
+
</array>
|
|
43
|
+
<key>CFBundlePackageType</key>
|
|
44
|
+
<string>XFWK</string>
|
|
45
|
+
<key>XCFrameworkFormatVersion</key>
|
|
46
|
+
<string>1.0</string>
|
|
47
|
+
</dict>
|
|
48
|
+
</plist>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#ifdef __cplusplus
|
|
3
|
+
extern "C" {
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
char* zkap_generate_hash(const char* input_json);
|
|
7
|
+
char* zkap_generate_anchor(const char* input_json);
|
|
8
|
+
char* zkap_generate_aud_hash(const char* input_json);
|
|
9
|
+
char* zkap_generate_leaf_hash(const char* input_json);
|
|
10
|
+
char* zkap_prove(const char* input_json);
|
|
11
|
+
void zkap_free_string(char* ptr);
|
|
12
|
+
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
}
|
|
15
|
+
#endif
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#ifdef __cplusplus
|
|
3
|
+
extern "C" {
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
char* zkap_generate_hash(const char* input_json);
|
|
7
|
+
char* zkap_generate_anchor(const char* input_json);
|
|
8
|
+
char* zkap_generate_aud_hash(const char* input_json);
|
|
9
|
+
char* zkap_generate_leaf_hash(const char* input_json);
|
|
10
|
+
char* zkap_prove(const char* input_json);
|
|
11
|
+
void zkap_free_string(char* ptr);
|
|
12
|
+
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
}
|
|
15
|
+
#endif
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#ifdef __cplusplus
|
|
3
|
+
extern "C" {
|
|
4
|
+
#endif
|
|
5
|
+
|
|
6
|
+
char* zkap_generate_hash(const char* input_json);
|
|
7
|
+
char* zkap_generate_anchor(const char* input_json);
|
|
8
|
+
char* zkap_generate_aud_hash(const char* input_json);
|
|
9
|
+
char* zkap_generate_leaf_hash(const char* input_json);
|
|
10
|
+
char* zkap_prove(const char* input_json);
|
|
11
|
+
void zkap_free_string(char* ptr);
|
|
12
|
+
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
}
|
|
15
|
+
#endif
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@baerae/zkap-zkp-react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native SDK for zkap-zkp — on-device ZK proving with Groth16 (BN254) and Poseidon hash. Requires Expo New Architecture.",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"source": "./src/index.ts",
|
|
8
|
+
"react-native": "./src/index.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"README.md",
|
|
11
|
+
"src/",
|
|
12
|
+
"ios/ZkapSdkModule.podspec",
|
|
13
|
+
"ios/ZkapSdkModule.swift",
|
|
14
|
+
"ios/include/",
|
|
15
|
+
"ios/ZkapZkp.xcframework/",
|
|
16
|
+
"android/src/",
|
|
17
|
+
"android/build.gradle",
|
|
18
|
+
"android/libs/",
|
|
19
|
+
"expo-module.config.json"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"zk",
|
|
29
|
+
"groth16",
|
|
30
|
+
"poseidon",
|
|
31
|
+
"zkap",
|
|
32
|
+
"react-native",
|
|
33
|
+
"expo",
|
|
34
|
+
"zero-knowledge"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/baerae-zkap/zkap-zkp.git",
|
|
39
|
+
"directory": "packages/sdk-react-native"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/baerae-zkap/zkap-zkp",
|
|
42
|
+
"bugs": "https://github.com/baerae-zkap/zkap-zkp/issues",
|
|
43
|
+
"license": "MIT OR Apache-2.0",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"expo": ">=50.0.0",
|
|
46
|
+
"expo-crypto": ">=13.0.0",
|
|
47
|
+
"expo-file-system": ">=17.0.0",
|
|
48
|
+
"expo-modules-core": ">=1.12.0",
|
|
49
|
+
"react-native": ">=0.73.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"typescript": "^5.4.5"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"registry": "https://registry.npmjs.org/",
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact-manager — MVP implementation
|
|
3
|
+
*
|
|
4
|
+
* Included: download, cache path management, SHA256 verification,
|
|
5
|
+
* atomic rename, max 3 retries, progress callback
|
|
6
|
+
* Excluded: HTTP Range resume, background download, advanced cache cleanup
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as FileSystem from 'expo-file-system';
|
|
10
|
+
import * as Crypto from 'expo-crypto';
|
|
11
|
+
|
|
12
|
+
// ──────────────────────────────────────────────────────────────────
|
|
13
|
+
// Types
|
|
14
|
+
// ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface ArtifactManifest {
|
|
17
|
+
version: string;
|
|
18
|
+
circuit: string;
|
|
19
|
+
artifacts: {
|
|
20
|
+
proving_key: {
|
|
21
|
+
url: string;
|
|
22
|
+
sha256: string;
|
|
23
|
+
size_bytes: number;
|
|
24
|
+
compressed_size_bytes?: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
min_sdk_version: string;
|
|
28
|
+
max_sdk_version: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InitProveArtifactsOptions {
|
|
32
|
+
/** URL of the manifest.json that describes the proving key location */
|
|
33
|
+
manifestUrl: string;
|
|
34
|
+
/**
|
|
35
|
+
* Local directory for caching the proving key.
|
|
36
|
+
* Defaults to `<FileSystem.cacheDirectory>/zkap/`
|
|
37
|
+
*/
|
|
38
|
+
cacheDir?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Called periodically during download.
|
|
41
|
+
* `downloaded` and `total` are in bytes.
|
|
42
|
+
*/
|
|
43
|
+
onProgress?: (downloaded: number, total: number) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ──────────────────────────────────────────────────────────────────
|
|
47
|
+
// Constants
|
|
48
|
+
// ──────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const MAX_RETRIES = 3;
|
|
51
|
+
const RETRY_BASE_DELAY_MS = 1000;
|
|
52
|
+
|
|
53
|
+
// ──────────────────────────────────────────────────────────────────
|
|
54
|
+
// Public API
|
|
55
|
+
// ──────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Download (if not cached) and verify the Groth16 proving key.
|
|
59
|
+
*
|
|
60
|
+
* Returns the local file path to the cached proving key,
|
|
61
|
+
* which should be passed to `prove()` as `request.pk_path`.
|
|
62
|
+
*
|
|
63
|
+
* @throws if download fails after 3 retries, or SHA256 verification fails
|
|
64
|
+
*/
|
|
65
|
+
export async function initProveArtifacts(
|
|
66
|
+
options: InitProveArtifactsOptions
|
|
67
|
+
): Promise<string> {
|
|
68
|
+
const cacheDir =
|
|
69
|
+
(options.cacheDir ?? `${FileSystem.cacheDirectory}zkap/`)
|
|
70
|
+
.replace(/^file:\/\//, '');
|
|
71
|
+
|
|
72
|
+
// Ensure cache directory exists
|
|
73
|
+
await ensureDir(cacheDir);
|
|
74
|
+
|
|
75
|
+
// 1. Download manifest
|
|
76
|
+
const manifest = await fetchManifest(options.manifestUrl);
|
|
77
|
+
|
|
78
|
+
// 2. Determine cache file path (version-namespaced to avoid stale cache)
|
|
79
|
+
const pkInfo = manifest.artifacts.proving_key;
|
|
80
|
+
const fileName = `zkap-${manifest.version}-pk.bin`;
|
|
81
|
+
const pkCachePath = `${cacheDir}${fileName}`;
|
|
82
|
+
const pkTmpPath = `${pkCachePath}.tmp`;
|
|
83
|
+
|
|
84
|
+
// 3. Check if already cached and valid
|
|
85
|
+
const fileInfo = await FileSystem.getInfoAsync(pkCachePath);
|
|
86
|
+
if (fileInfo.exists) {
|
|
87
|
+
const valid = await verifySha256(pkCachePath, pkInfo.sha256);
|
|
88
|
+
if (valid) {
|
|
89
|
+
return pkCachePath;
|
|
90
|
+
}
|
|
91
|
+
// Cache is corrupted — delete and re-download
|
|
92
|
+
await FileSystem.deleteAsync(pkCachePath, { idempotent: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 4. Download with retries
|
|
96
|
+
await downloadWithRetry({
|
|
97
|
+
url: pkInfo.url,
|
|
98
|
+
tmpPath: pkTmpPath,
|
|
99
|
+
totalBytes: pkInfo.size_bytes,
|
|
100
|
+
onProgress: options.onProgress,
|
|
101
|
+
maxRetries: MAX_RETRIES,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 5. Verify SHA256
|
|
105
|
+
const valid = await verifySha256(pkTmpPath, pkInfo.sha256);
|
|
106
|
+
if (!valid) {
|
|
107
|
+
await FileSystem.deleteAsync(pkTmpPath, { idempotent: true });
|
|
108
|
+
throw new Error(
|
|
109
|
+
'[zkap/artifact-manager] SHA256 verification failed. ' +
|
|
110
|
+
'The downloaded file may be corrupted. Please try again.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 6. Atomic rename: tmp → final cache path
|
|
115
|
+
await FileSystem.moveAsync({ from: pkTmpPath, to: pkCachePath });
|
|
116
|
+
|
|
117
|
+
return pkCachePath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ──────────────────────────────────────────────────────────────────
|
|
121
|
+
// Internal helpers
|
|
122
|
+
// ──────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function ensureDir(dir: string): Promise<void> {
|
|
125
|
+
const info = await FileSystem.getInfoAsync(dir);
|
|
126
|
+
if (!info.exists) {
|
|
127
|
+
await FileSystem.makeDirectoryAsync(dir, { intermediates: true });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function fetchManifest(url: string): Promise<ArtifactManifest> {
|
|
132
|
+
const response = await fetch(url);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`[zkap/artifact-manager] Failed to fetch manifest: HTTP ${response.status}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return response.json() as Promise<ArtifactManifest>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface DownloadOptions {
|
|
142
|
+
url: string;
|
|
143
|
+
tmpPath: string;
|
|
144
|
+
totalBytes: number;
|
|
145
|
+
onProgress?: (downloaded: number, total: number) => void;
|
|
146
|
+
maxRetries: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function downloadWithRetry(opts: DownloadOptions): Promise<void> {
|
|
150
|
+
let lastError: Error | null = null;
|
|
151
|
+
|
|
152
|
+
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
|
|
153
|
+
try {
|
|
154
|
+
// Clean up any partial download from a previous attempt
|
|
155
|
+
await FileSystem.deleteAsync(opts.tmpPath, { idempotent: true });
|
|
156
|
+
|
|
157
|
+
const downloadResumable = FileSystem.createDownloadResumable(
|
|
158
|
+
opts.url,
|
|
159
|
+
opts.tmpPath,
|
|
160
|
+
{},
|
|
161
|
+
(progress) => {
|
|
162
|
+
if (opts.onProgress) {
|
|
163
|
+
opts.onProgress(
|
|
164
|
+
progress.totalBytesWritten,
|
|
165
|
+
progress.totalBytesExpectedToWrite > 0
|
|
166
|
+
? progress.totalBytesExpectedToWrite
|
|
167
|
+
: opts.totalBytes
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const result = await downloadResumable.downloadAsync();
|
|
174
|
+
if (!result || result.status !== 200) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`[zkap/artifact-manager] Download failed with status ${result?.status ?? 'unknown'}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return; // Success
|
|
181
|
+
} catch (err) {
|
|
182
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
183
|
+
if (attempt < opts.maxRetries) {
|
|
184
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
|
|
185
|
+
await sleep(delay);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error(
|
|
191
|
+
`[zkap/artifact-manager] Download failed after ${opts.maxRetries} attempts. ` +
|
|
192
|
+
`Last error: ${lastError?.message ?? 'unknown'}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function verifySha256(filePath: string, expectedHex: string): Promise<boolean> {
|
|
197
|
+
try {
|
|
198
|
+
// Read file as base64, convert to raw binary Uint8Array, then SHA256 the binary.
|
|
199
|
+
// NOTE: digestStringAsync hashes the string's UTF-8 bytes, not the file's binary
|
|
200
|
+
// content. We must use Crypto.digest(algorithm, Uint8Array) for correct results.
|
|
201
|
+
const base64 = await FileSystem.readAsStringAsync(filePath, {
|
|
202
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// base64 → raw binary bytes
|
|
206
|
+
const binaryStr = atob(base64);
|
|
207
|
+
const bytes = new Uint8Array(binaryStr.length);
|
|
208
|
+
for (let i = 0; i < binaryStr.length; i++) {
|
|
209
|
+
bytes[i] = binaryStr.charCodeAt(i);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Hash the raw binary
|
|
213
|
+
const digestBuffer = await Crypto.digest(
|
|
214
|
+
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
215
|
+
bytes
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// ArrayBuffer → hex string
|
|
219
|
+
const digestHex = Array.from(new Uint8Array(digestBuffer))
|
|
220
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
221
|
+
.join('');
|
|
222
|
+
|
|
223
|
+
return digestHex.toLowerCase() === expectedHex.toLowerCase();
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sleep(ms: number): Promise<void> {
|
|
230
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
231
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
const ZkapSdk = requireNativeModule('ZkapSdk');
|
|
4
|
+
|
|
5
|
+
// ──────────────────────────────────────────────────────────────────
|
|
6
|
+
// Input / Output types
|
|
7
|
+
// ──────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface CircuitConfig {
|
|
10
|
+
max_jwt_b64_len: number;
|
|
11
|
+
max_payload_b64_len: number;
|
|
12
|
+
max_aud_len: number;
|
|
13
|
+
max_exp_len: number;
|
|
14
|
+
max_iss_len: number;
|
|
15
|
+
max_nonce_len: number;
|
|
16
|
+
max_sub_len: number;
|
|
17
|
+
n: number;
|
|
18
|
+
k: number;
|
|
19
|
+
tree_height: number;
|
|
20
|
+
num_audience_limit: number;
|
|
21
|
+
claims: string[];
|
|
22
|
+
forbidden_string: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Secret {
|
|
26
|
+
sub: string;
|
|
27
|
+
iss: string;
|
|
28
|
+
aud: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ProveRequest {
|
|
32
|
+
pk_path: string;
|
|
33
|
+
jwts: string[];
|
|
34
|
+
pk_ops: string[];
|
|
35
|
+
merkle_paths: string[][];
|
|
36
|
+
leaf_indices: number[];
|
|
37
|
+
root: string;
|
|
38
|
+
anchor: string[];
|
|
39
|
+
h_sign_user_op: string;
|
|
40
|
+
random: string;
|
|
41
|
+
aud_list: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProveResult {
|
|
45
|
+
proofs: string[];
|
|
46
|
+
public_inputs: string[][];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ──────────────────────────────────────────────────────────────────
|
|
50
|
+
// Supported API
|
|
51
|
+
// ──────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compute a Poseidon hash of one or more field-element strings (hex or decimal).
|
|
55
|
+
* Returns the result as a 0x-prefixed hex string.
|
|
56
|
+
*/
|
|
57
|
+
export async function generateHash(messages: string[]): Promise<string> {
|
|
58
|
+
const result: string = await ZkapSdk.generateHash(
|
|
59
|
+
JSON.stringify({ messages })
|
|
60
|
+
);
|
|
61
|
+
return JSON.parse(result).result as string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate a Poseidon threshold anchor from JWT credential secrets.
|
|
66
|
+
* Returns anchor polynomial evaluation points as hex strings.
|
|
67
|
+
*/
|
|
68
|
+
export async function generateAnchor(
|
|
69
|
+
config: CircuitConfig,
|
|
70
|
+
secrets: Secret[]
|
|
71
|
+
): Promise<{ evaluations: string[] }> {
|
|
72
|
+
const result: string = await ZkapSdk.generateAnchor(
|
|
73
|
+
JSON.stringify({ config, secrets })
|
|
74
|
+
);
|
|
75
|
+
return JSON.parse(result) as { evaluations: string[] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute per-audience hashes and the combined audience-list hash.
|
|
80
|
+
*/
|
|
81
|
+
export async function generateAudHash(
|
|
82
|
+
config: CircuitConfig,
|
|
83
|
+
aud_list: string[]
|
|
84
|
+
): Promise<{ aud_hashes: string[]; h_aud_list: string }> {
|
|
85
|
+
const result: string = await ZkapSdk.generateAudHash(
|
|
86
|
+
JSON.stringify({ config, aud_list })
|
|
87
|
+
);
|
|
88
|
+
return JSON.parse(result) as { aud_hashes: string[]; h_aud_list: string };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compute the Merkle leaf hash for an issuer and RSA public-key modulus (base64-encoded).
|
|
93
|
+
*/
|
|
94
|
+
export async function generateLeafHash(
|
|
95
|
+
config: CircuitConfig,
|
|
96
|
+
iss: string,
|
|
97
|
+
pk_b64: string
|
|
98
|
+
): Promise<string> {
|
|
99
|
+
const result: string = await ZkapSdk.generateLeafHash(
|
|
100
|
+
JSON.stringify({ config, iss, pk_b64 })
|
|
101
|
+
);
|
|
102
|
+
return JSON.parse(result).result as string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generate Groth16 proofs (on-device proving).
|
|
107
|
+
* Requires PK to be downloaded and cached via initProveArtifacts().
|
|
108
|
+
*/
|
|
109
|
+
export async function prove(
|
|
110
|
+
config: CircuitConfig,
|
|
111
|
+
request: ProveRequest
|
|
112
|
+
): Promise<ProveResult> {
|
|
113
|
+
const result: string = await ZkapSdk.prove(
|
|
114
|
+
JSON.stringify({ config, request })
|
|
115
|
+
);
|
|
116
|
+
return JSON.parse(result) as ProveResult;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ──────────────────────────────────────────────────────────────────
|
|
120
|
+
// Unsupported API — explicit error messages
|
|
121
|
+
// ──────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* NOT supported on React Native.
|
|
125
|
+
* groth16Setup() requires a server environment with large CRS data.
|
|
126
|
+
* Use @baerae/zkap-zkp (Node.js) for setup.
|
|
127
|
+
*/
|
|
128
|
+
export function groth16Setup(): never {
|
|
129
|
+
throw new Error(
|
|
130
|
+
'[zkap/sdk-react-native] groth16Setup() is not supported on mobile. ' +
|
|
131
|
+
'Use @baerae/zkap-zkp (Node.js) for server-side trusted setup.'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* NOT supported on React Native.
|
|
137
|
+
* verify() uses a verifying key hardcoded in a smart contract.
|
|
138
|
+
* Use @baerae/zkap-zkp (Node.js) for server-side verification.
|
|
139
|
+
*/
|
|
140
|
+
export function verify(): never {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'[zkap/sdk-react-native] verify() is not supported on mobile. ' +
|
|
143
|
+
'Use @baerae/zkap-zkp (Node.js) for server-side verification.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { initProveArtifacts } from './artifact-manager';
|