@iam-protocol/pulse-sdk 0.3.2 → 0.3.3
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 +1 -1
- package/dist/index.d.mts +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +107 -13153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +87 -13125
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
- package/src/extraction/statistics.ts +27 -3
- package/src/identity/anchor.ts +1 -1
- package/src/proof/prover.ts +3 -4
- package/src/pulse.ts +19 -25
- package/src/submit/relayer.ts +2 -0
- package/src/submit/wallet.ts +20 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iam-protocol/pulse-sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Client-side SDK for IAM Protocol — sensor capture, TBH generation, ZK proof construction",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@coral-xyz/anchor": "^0.32.1",
|
|
29
|
+
"@solana/spl-token": "^0.4.0",
|
|
29
30
|
"@solana/wallet-adapter-base": "^0.9.0",
|
|
30
31
|
"@solana/web3.js": "^1.98.0"
|
|
31
32
|
},
|
|
@@ -33,6 +34,9 @@
|
|
|
33
34
|
"@coral-xyz/anchor": {
|
|
34
35
|
"optional": true
|
|
35
36
|
},
|
|
37
|
+
"@solana/spl-token": {
|
|
38
|
+
"optional": true
|
|
39
|
+
},
|
|
36
40
|
"@solana/web3.js": {
|
|
37
41
|
"optional": true
|
|
38
42
|
},
|
|
@@ -57,8 +57,12 @@ export function condense(values: number[]): StatsSummary {
|
|
|
57
57
|
*/
|
|
58
58
|
export function entropy(values: number[], bins: number = 16): number {
|
|
59
59
|
if (values.length < 2) return 0;
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
let min = values[0]!;
|
|
61
|
+
let max = values[0]!;
|
|
62
|
+
for (let i = 1; i < values.length; i++) {
|
|
63
|
+
if (values[i]! < min) min = values[i]!;
|
|
64
|
+
if (values[i]! > max) max = values[i]!;
|
|
65
|
+
}
|
|
62
66
|
if (min === max) return 0;
|
|
63
67
|
|
|
64
68
|
const counts = new Array(bins).fill(0);
|
|
@@ -96,10 +100,30 @@ export function autocorrelation(values: number[], lag: number = 1): number {
|
|
|
96
100
|
return sum / ((values.length - lag) * v);
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a feature group to zero mean and unit variance.
|
|
105
|
+
* Ensures each modality (audio, motion, touch) contributes equally
|
|
106
|
+
* to SimHash hyperplane projections regardless of raw magnitude scale.
|
|
107
|
+
*/
|
|
108
|
+
function normalizeGroup(features: number[]): number[] {
|
|
109
|
+
if (features.length === 0) return features;
|
|
110
|
+
|
|
111
|
+
let sum = 0;
|
|
112
|
+
for (const v of features) sum += v;
|
|
113
|
+
const mean = sum / features.length;
|
|
114
|
+
|
|
115
|
+
let sqSum = 0;
|
|
116
|
+
for (const v of features) sqSum += (v - mean) * (v - mean);
|
|
117
|
+
const std = Math.sqrt(sqSum / features.length);
|
|
118
|
+
|
|
119
|
+
if (std === 0) return features.map(() => 0);
|
|
120
|
+
return features.map((v) => (v - mean) / std);
|
|
121
|
+
}
|
|
122
|
+
|
|
99
123
|
export function fuseFeatures(
|
|
100
124
|
audio: number[],
|
|
101
125
|
motion: number[],
|
|
102
126
|
touch: number[]
|
|
103
127
|
): number[] {
|
|
104
|
-
return [...audio, ...motion, ...touch];
|
|
128
|
+
return [...normalizeGroup(audio), ...normalizeGroup(motion), ...normalizeGroup(touch)];
|
|
105
129
|
}
|
package/src/identity/anchor.ts
CHANGED
|
@@ -16,7 +16,7 @@ export async function fetchIdentityState(
|
|
|
16
16
|
|
|
17
17
|
const programId = new PublicKey(PROGRAM_IDS.iamAnchor);
|
|
18
18
|
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
19
|
-
[
|
|
19
|
+
[new TextEncoder().encode("identity"), new PublicKey(walletPubkey).toBuffer()],
|
|
20
20
|
programId
|
|
21
21
|
);
|
|
22
22
|
|
package/src/proof/prover.ts
CHANGED
|
@@ -76,14 +76,13 @@ export async function generateSolanaProof(
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Verify a proof locally using snarkjs (for debugging/testing).
|
|
79
|
+
* Caller is responsible for loading the verification key.
|
|
79
80
|
*/
|
|
80
81
|
export async function verifyProofLocally(
|
|
81
82
|
proof: any,
|
|
82
83
|
publicSignals: string[],
|
|
83
|
-
|
|
84
|
+
vkey: Record<string, unknown>
|
|
84
85
|
): Promise<boolean> {
|
|
85
86
|
const snarkjs = await getSnarkjs();
|
|
86
|
-
|
|
87
|
-
const vk = JSON.parse(fs.readFileSync(vkeyPath, "utf-8"));
|
|
88
|
-
return snarkjs.groth16.verify(vk, publicSignals, proof);
|
|
87
|
+
return snarkjs.groth16.verify(vkey, publicSignals, proof);
|
|
89
88
|
}
|
package/src/pulse.ts
CHANGED
|
@@ -34,9 +34,10 @@ type ResolvedConfig = Required<Pick<PulseConfig, "cluster" | "threshold">> &
|
|
|
34
34
|
* Extract features from sensor data and fuse into a single vector.
|
|
35
35
|
*/
|
|
36
36
|
async function extractFeatures(data: SensorData): Promise<number[]> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
if (!data.audio) {
|
|
38
|
+
throw new Error("Audio data required for feature extraction");
|
|
39
|
+
}
|
|
40
|
+
const audioFeatures = await extractSpeakerFeatures(data.audio);
|
|
40
41
|
|
|
41
42
|
const hasMotion = data.motion.length >= MIN_MOTION_SAMPLES;
|
|
42
43
|
const motionFeatures = hasMotion
|
|
@@ -59,6 +60,7 @@ const MIN_TOUCH_SAMPLES = 10;
|
|
|
59
60
|
async function processSensorData(
|
|
60
61
|
sensorData: SensorData,
|
|
61
62
|
config: ResolvedConfig,
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
|
|
62
64
|
wallet?: any,
|
|
63
65
|
connection?: any
|
|
64
66
|
): Promise<VerificationResult> {
|
|
@@ -271,11 +273,8 @@ export class PulseSession {
|
|
|
271
273
|
return this.audioData;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
skipAudio()
|
|
275
|
-
|
|
276
|
-
throw new Error("Audio capture already started");
|
|
277
|
-
this.audioStageState = "skipped";
|
|
278
|
-
}
|
|
276
|
+
// Audio is mandatory — no skipAudio() method.
|
|
277
|
+
// If startAudio() fails, the verification cannot proceed.
|
|
279
278
|
|
|
280
279
|
// --- Motion ---
|
|
281
280
|
|
|
@@ -335,6 +334,7 @@ export class PulseSession {
|
|
|
335
334
|
|
|
336
335
|
// --- Complete ---
|
|
337
336
|
|
|
337
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Solana types are optional peer deps
|
|
338
338
|
async complete(wallet?: any, connection?: any): Promise<VerificationResult> {
|
|
339
339
|
const active: string[] = [];
|
|
340
340
|
if (this.audioStageState === "capturing") active.push("audio");
|
|
@@ -404,25 +404,21 @@ export class PulseSDK {
|
|
|
404
404
|
try {
|
|
405
405
|
await session.startAudio();
|
|
406
406
|
stopPromises.push(
|
|
407
|
-
new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
|
|
408
|
-
() =>
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
)
|
|
407
|
+
new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
|
|
408
|
+
.then(() => session.stopAudio())
|
|
409
|
+
.then(() => {})
|
|
412
410
|
);
|
|
413
|
-
} catch {
|
|
414
|
-
|
|
411
|
+
} catch (err: any) {
|
|
412
|
+
throw new Error(`Audio capture failed: ${err?.message ?? "microphone unavailable"}`);
|
|
415
413
|
}
|
|
416
414
|
|
|
417
415
|
// Motion
|
|
418
416
|
try {
|
|
419
417
|
await session.startMotion();
|
|
420
418
|
stopPromises.push(
|
|
421
|
-
new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
|
|
422
|
-
() =>
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
)
|
|
419
|
+
new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
|
|
420
|
+
.then(() => session.stopMotion())
|
|
421
|
+
.then(() => {})
|
|
426
422
|
);
|
|
427
423
|
} catch {
|
|
428
424
|
session.skipMotion();
|
|
@@ -433,11 +429,9 @@ export class PulseSDK {
|
|
|
433
429
|
try {
|
|
434
430
|
await session.startTouch();
|
|
435
431
|
stopPromises.push(
|
|
436
|
-
new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
|
|
437
|
-
() =>
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
)
|
|
432
|
+
new Promise<void>((r) => setTimeout(r, DEFAULT_CAPTURE_MS))
|
|
433
|
+
.then(() => session.stopTouch())
|
|
434
|
+
.then(() => {})
|
|
441
435
|
);
|
|
442
436
|
} catch {
|
|
443
437
|
session.skipTouch();
|
package/src/submit/relayer.ts
CHANGED
package/src/submit/wallet.ts
CHANGED
|
@@ -14,10 +14,9 @@ export async function submitViaWallet(
|
|
|
14
14
|
proof: SolanaProof,
|
|
15
15
|
commitment: Uint8Array,
|
|
16
16
|
options: {
|
|
17
|
-
wallet: any;
|
|
18
|
-
connection: any;
|
|
17
|
+
wallet: any;
|
|
18
|
+
connection: any;
|
|
19
19
|
isFirstVerification: boolean;
|
|
20
|
-
trustScore?: number;
|
|
21
20
|
}
|
|
22
21
|
): Promise<SubmissionResult> {
|
|
23
22
|
try {
|
|
@@ -39,18 +38,18 @@ export async function submitViaWallet(
|
|
|
39
38
|
// Derive PDAs
|
|
40
39
|
const [challengePda] = PublicKey.findProgramAddressSync(
|
|
41
40
|
[
|
|
42
|
-
|
|
41
|
+
new TextEncoder().encode("challenge"),
|
|
43
42
|
provider.wallet.publicKey.toBuffer(),
|
|
44
|
-
|
|
43
|
+
new Uint8Array(nonce),
|
|
45
44
|
],
|
|
46
45
|
verifierProgramId
|
|
47
46
|
);
|
|
48
47
|
|
|
49
48
|
const [verificationPda] = PublicKey.findProgramAddressSync(
|
|
50
49
|
[
|
|
51
|
-
|
|
50
|
+
new TextEncoder().encode("verification"),
|
|
52
51
|
provider.wallet.publicKey.toBuffer(),
|
|
53
|
-
|
|
52
|
+
new Uint8Array(nonce),
|
|
54
53
|
],
|
|
55
54
|
verifierProgramId
|
|
56
55
|
);
|
|
@@ -83,7 +82,7 @@ export async function submitViaWallet(
|
|
|
83
82
|
// 2. Verify proof
|
|
84
83
|
const txSig = await verifierProgram.methods
|
|
85
84
|
.verifyProof(
|
|
86
|
-
|
|
85
|
+
Array.from(proof.proofBytes),
|
|
87
86
|
proof.publicInputs.map((pi) => Array.from(pi)),
|
|
88
87
|
nonce
|
|
89
88
|
)
|
|
@@ -106,15 +105,15 @@ export async function submitViaWallet(
|
|
|
106
105
|
|
|
107
106
|
if (options.isFirstVerification) {
|
|
108
107
|
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
109
|
-
[
|
|
108
|
+
[new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
|
|
110
109
|
anchorProgramId
|
|
111
110
|
);
|
|
112
111
|
const [mintPda] = PublicKey.findProgramAddressSync(
|
|
113
|
-
[
|
|
112
|
+
[new TextEncoder().encode("mint"), provider.wallet.publicKey.toBuffer()],
|
|
114
113
|
anchorProgramId
|
|
115
114
|
);
|
|
116
115
|
const [mintAuthority] = PublicKey.findProgramAddressSync(
|
|
117
|
-
[
|
|
116
|
+
[new TextEncoder().encode("mint_authority")],
|
|
118
117
|
anchorProgramId
|
|
119
118
|
);
|
|
120
119
|
|
|
@@ -150,15 +149,23 @@ export async function submitViaWallet(
|
|
|
150
149
|
.rpc();
|
|
151
150
|
} else {
|
|
152
151
|
const [identityPda] = PublicKey.findProgramAddressSync(
|
|
153
|
-
[
|
|
152
|
+
[new TextEncoder().encode("identity"), provider.wallet.publicKey.toBuffer()],
|
|
154
153
|
anchorProgramId
|
|
155
154
|
);
|
|
156
155
|
|
|
156
|
+
// Derive iam-registry ProtocolConfig PDA for trust score computation
|
|
157
|
+
const registryProgramId = new PublicKey(PROGRAM_IDS.iamRegistry);
|
|
158
|
+
const [protocolConfigPda] = PublicKey.findProgramAddressSync(
|
|
159
|
+
[new TextEncoder().encode("protocol_config")],
|
|
160
|
+
registryProgramId
|
|
161
|
+
);
|
|
162
|
+
|
|
157
163
|
await anchorProgram.methods
|
|
158
|
-
.updateAnchor(Array.from(commitment)
|
|
164
|
+
.updateAnchor(Array.from(commitment))
|
|
159
165
|
.accounts({
|
|
160
166
|
authority: provider.wallet.publicKey,
|
|
161
167
|
identityState: identityPda,
|
|
168
|
+
protocolConfig: protocolConfigPda,
|
|
162
169
|
})
|
|
163
170
|
.rpc();
|
|
164
171
|
}
|