@oleary-labs/signet-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.
Files changed (95) hide show
  1. package/dist/admin.d.ts +38 -0
  2. package/dist/admin.d.ts.map +1 -0
  3. package/dist/admin.js +112 -0
  4. package/dist/admin.js.map +1 -0
  5. package/dist/authkey-session.d.ts +64 -0
  6. package/dist/authkey-session.d.ts.map +1 -0
  7. package/dist/authkey-session.js +164 -0
  8. package/dist/authkey-session.js.map +1 -0
  9. package/dist/bootstrap.d.ts +30 -0
  10. package/dist/bootstrap.d.ts.map +1 -0
  11. package/dist/bootstrap.js +60 -0
  12. package/dist/bootstrap.js.map +1 -0
  13. package/dist/bundler.d.ts +85 -0
  14. package/dist/bundler.d.ts.map +1 -0
  15. package/dist/bundler.js +160 -0
  16. package/dist/bundler.js.map +1 -0
  17. package/dist/delegate.d.ts +57 -0
  18. package/dist/delegate.d.ts.map +1 -0
  19. package/dist/delegate.js +111 -0
  20. package/dist/delegate.js.map +1 -0
  21. package/dist/frostVerify.d.ts +23 -0
  22. package/dist/frostVerify.d.ts.map +1 -0
  23. package/dist/frostVerify.js +69 -0
  24. package/dist/frostVerify.js.map +1 -0
  25. package/dist/index.d.ts +32 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +38 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/jwks.d.ts +28 -0
  30. package/dist/jwks.d.ts.map +1 -0
  31. package/dist/jwks.js +81 -0
  32. package/dist/jwks.js.map +1 -0
  33. package/dist/jwt.d.ts +27 -0
  34. package/dist/jwt.d.ts.map +1 -0
  35. package/dist/jwt.js +50 -0
  36. package/dist/jwt.js.map +1 -0
  37. package/dist/keygen.d.ts +26 -0
  38. package/dist/keygen.d.ts.map +1 -0
  39. package/dist/keygen.js +60 -0
  40. package/dist/keygen.js.map +1 -0
  41. package/dist/oauth.d.ts +34 -0
  42. package/dist/oauth.d.ts.map +1 -0
  43. package/dist/oauth.js +119 -0
  44. package/dist/oauth.js.map +1 -0
  45. package/dist/request.d.ts +42 -0
  46. package/dist/request.d.ts.map +1 -0
  47. package/dist/request.js +115 -0
  48. package/dist/request.js.map +1 -0
  49. package/dist/scopedSign.d.ts +82 -0
  50. package/dist/scopedSign.d.ts.map +1 -0
  51. package/dist/scopedSign.js +130 -0
  52. package/dist/scopedSign.js.map +1 -0
  53. package/dist/server-prover.d.ts +29 -0
  54. package/dist/server-prover.d.ts.map +1 -0
  55. package/dist/server-prover.js +54 -0
  56. package/dist/server-prover.js.map +1 -0
  57. package/dist/session.d.ts +14 -0
  58. package/dist/session.d.ts.map +1 -0
  59. package/dist/session.js +29 -0
  60. package/dist/session.js.map +1 -0
  61. package/dist/types.d.ts +56 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +5 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/userop.d.ts +104 -0
  66. package/dist/userop.d.ts.map +1 -0
  67. package/dist/userop.js +212 -0
  68. package/dist/userop.js.map +1 -0
  69. package/dist/x402.d.ts +127 -0
  70. package/dist/x402.d.ts.map +1 -0
  71. package/dist/x402.js +167 -0
  72. package/dist/x402.js.map +1 -0
  73. package/package.json +64 -0
  74. package/src/admin.ts +178 -0
  75. package/src/authkey-session.ts +241 -0
  76. package/src/bootstrap.ts +106 -0
  77. package/src/bundler.ts +256 -0
  78. package/src/delegate.ts +163 -0
  79. package/src/frostVerify.ts +79 -0
  80. package/src/generate-inputs.ts +158 -0
  81. package/src/index.ts +43 -0
  82. package/src/jwks.ts +92 -0
  83. package/src/jwt.ts +74 -0
  84. package/src/keygen.ts +89 -0
  85. package/src/oauth.ts +157 -0
  86. package/src/partial-sha.ts +99 -0
  87. package/src/proof.ts +99 -0
  88. package/src/request.ts +174 -0
  89. package/src/scopedSign.ts +184 -0
  90. package/src/server-prover.ts +76 -0
  91. package/src/session.ts +33 -0
  92. package/src/types.ts +63 -0
  93. package/src/userop.ts +368 -0
  94. package/src/witness.ts +132 -0
  95. package/src/x402.ts +275 -0
package/src/userop.ts ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Signet UserOperation pipeline.
3
+ *
4
+ * Framework-agnostic orchestration of the full ERC-4337 flow:
5
+ * build → (paymaster stub) → estimate → (paymaster real) → hash → FROST sign → submit → confirm
6
+ *
7
+ * The caller provides an already-encoded callData (the app-specific part);
8
+ * everything else is generic Signet protocol logic.
9
+ */
10
+
11
+ import {
12
+ type Address,
13
+ type Hex,
14
+ encodeFunctionData,
15
+ encodeAbiParameters,
16
+ keccak256,
17
+ concat,
18
+ createPublicClient,
19
+ http,
20
+ } from "viem";
21
+ import type { SessionKeypair, IdTokenClaims } from "./types";
22
+ import { signSignRequest } from "./request";
23
+ import {
24
+ sendUserOp as bundlerSendUserOp,
25
+ getUserOpReceipt as bundlerGetUserOpReceipt,
26
+ estimateUserOpGas,
27
+ getPaymasterStubData,
28
+ getPaymasterData,
29
+ applyPaymasterSponsorship,
30
+ type PaymasterContext,
31
+ } from "./bundler";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Types
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * ERC-4337 PackedUserOperation.
39
+ *
40
+ * This is the format expected by the EntryPoint and validated
41
+ * by SignetAccount.validateUserOp. The signature field carries
42
+ * a 65-byte FROST Schnorr signature (Rx || z || v).
43
+ */
44
+ export interface PackedUserOperation {
45
+ sender: Address;
46
+ nonce: bigint;
47
+ initCode: Hex;
48
+ callData: Hex;
49
+ accountGasLimits: Hex;
50
+ preVerificationGas: bigint;
51
+ gasFees: Hex;
52
+ paymasterAndData: Hex;
53
+ signature: Hex;
54
+ }
55
+
56
+ export type UserOpStatus =
57
+ | "building"
58
+ | "sponsoring-stub"
59
+ | "estimating"
60
+ | "sponsoring"
61
+ | "signing"
62
+ | "submitting"
63
+ | "confirming";
64
+
65
+ export interface SignetWriteConfig {
66
+ rpcUrl: string;
67
+ chainId: number;
68
+ entryPointAddress: Address;
69
+ bundlerProxyUrl: string;
70
+ nodeProxyUrl: string;
71
+ bootstrapGroup: Address;
72
+ bootstrapNodes: string[];
73
+ accountFactoryAddress: Address;
74
+ accountFactoryAbi: readonly Record<string, unknown>[];
75
+ usePaymaster: boolean;
76
+ paymasterContext?: PaymasterContext;
77
+ }
78
+
79
+ export interface SignetWriteParams {
80
+ account: Address;
81
+ groupPublicKey: Hex;
82
+ dest: Address;
83
+ value?: bigint;
84
+ callData: Hex;
85
+ sessionKeypair: SessionKeypair;
86
+ claims: IdTokenClaims;
87
+ onStatus?: (status: UserOpStatus) => void;
88
+ }
89
+
90
+ export interface SignetWriteResult {
91
+ userOpHash: Hex;
92
+ transactionHash: Hex;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Main pipeline
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /**
100
+ * Submit a UserOperation through the full Signet pipeline.
101
+ *
102
+ * Ordering is strict — see CLAUDE.md "Write flow ordering" for rationale:
103
+ * 1. Build unsigned UserOp (with initCode if account not deployed)
104
+ * 2. (if paymaster) Attach stub paymasterAndData for gas estimation
105
+ * 3. Estimate gas via bundler
106
+ * 4. (if paymaster) Replace stub with real signed paymaster blob
107
+ * 5. Compute UserOp hash
108
+ * 6. FROST threshold sign via bootstrap group
109
+ * 7. Submit to bundler
110
+ * 8. Poll for receipt
111
+ */
112
+ export async function submitUserOp(
113
+ config: SignetWriteConfig,
114
+ params: SignetWriteParams
115
+ ): Promise<SignetWriteResult> {
116
+ const { onStatus } = params;
117
+
118
+ // 1. Build the UserOperation
119
+ onStatus?.("building");
120
+ const nonce = await fetchNonce(config.rpcUrl, config.entryPointAddress, params.account);
121
+ const initCode = await buildInitCode(config, params.account, params.groupPublicKey);
122
+
123
+ let userOp = buildUserOp({
124
+ sender: params.account,
125
+ nonce,
126
+ initCode,
127
+ dest: params.dest,
128
+ value: params.value,
129
+ callData: params.callData,
130
+ });
131
+
132
+ // 2. (optional) Attach paymaster stub before gas estimation
133
+ if (config.usePaymaster) {
134
+ onStatus?.("sponsoring-stub");
135
+ const stub = await getPaymasterStubData(
136
+ config.bundlerProxyUrl, config.entryPointAddress, config.chainId, userOp,
137
+ config.paymasterContext,
138
+ );
139
+ userOp = applyPaymasterSponsorship(userOp, stub);
140
+ }
141
+
142
+ // 3. Estimate gas
143
+ onStatus?.("estimating");
144
+ const gasEstimate = await estimateUserOpGas(
145
+ config.bundlerProxyUrl, config.entryPointAddress, userOp
146
+ );
147
+ userOp.accountGasLimits =
148
+ `0x${BigInt(gasEstimate.verificationGasLimit).toString(16).padStart(32, "0")}${BigInt(gasEstimate.callGasLimit).toString(16).padStart(32, "0")}` as Hex;
149
+ userOp.preVerificationGas = BigInt(gasEstimate.preVerificationGas);
150
+
151
+ // 4. (optional) Replace stub with real signed paymaster blob
152
+ if (config.usePaymaster) {
153
+ onStatus?.("sponsoring");
154
+ const real = await getPaymasterData(
155
+ config.bundlerProxyUrl, config.entryPointAddress, config.chainId, userOp,
156
+ config.paymasterContext,
157
+ );
158
+ userOp = applyPaymasterSponsorship(userOp, real);
159
+ }
160
+
161
+ // 5. Hash + 6. FROST threshold sign
162
+ onStatus?.("signing");
163
+ const opHash = getUserOpHash(userOp, config.entryPointAddress, config.chainId);
164
+ const messageHash = hexToBytes(opHash);
165
+
166
+ const signReq = await signSignRequest(
167
+ params.sessionKeypair,
168
+ params.claims,
169
+ config.bootstrapGroup,
170
+ messageHash,
171
+ );
172
+
173
+ const signRes = await fetch(config.nodeProxyUrl, {
174
+ method: "POST",
175
+ headers: {
176
+ "Content-Type": "application/json",
177
+ "x-node-url": config.bootstrapNodes[0],
178
+ "x-node-path": "/v1/sign",
179
+ },
180
+ body: JSON.stringify(signReq),
181
+ });
182
+
183
+ if (!signRes.ok) {
184
+ const body = await signRes.text();
185
+ throw new Error(`Threshold signing failed: ${signRes.status} — ${body}`);
186
+ }
187
+
188
+ const { ethereum_signature } = await signRes.json();
189
+ userOp.signature = ethereum_signature as Hex;
190
+
191
+ // 7. Submit to bundler
192
+ onStatus?.("submitting");
193
+ const { userOpHash } = await bundlerSendUserOp(
194
+ config.bundlerProxyUrl, config.entryPointAddress, userOp
195
+ );
196
+
197
+ // 8. Poll for receipt
198
+ onStatus?.("confirming");
199
+ let receipt = null;
200
+ for (let i = 0; i < 60; i++) {
201
+ await new Promise((r) => setTimeout(r, 2000));
202
+ receipt = await bundlerGetUserOpReceipt(config.bundlerProxyUrl, userOpHash);
203
+ if (receipt) break;
204
+ }
205
+
206
+ if (!receipt) throw new Error("Transaction confirmation timed out");
207
+ if (!receipt.success) throw new Error("UserOperation reverted on-chain");
208
+
209
+ return { userOpHash, transactionHash: receipt.transactionHash };
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Utilities
214
+ // ---------------------------------------------------------------------------
215
+
216
+ /**
217
+ * Build an unsigned UserOperation for a SignetAccount.execute call.
218
+ */
219
+ export function buildUserOp(params: {
220
+ sender: Address;
221
+ nonce: bigint;
222
+ initCode?: Hex;
223
+ dest: Address;
224
+ value?: bigint;
225
+ callData: Hex;
226
+ }): PackedUserOperation {
227
+ const executeCallData = encodeFunctionData({
228
+ abi: [
229
+ {
230
+ name: "execute",
231
+ type: "function",
232
+ inputs: [
233
+ { name: "dest", type: "address" },
234
+ { name: "value", type: "uint256" },
235
+ { name: "data", type: "bytes" },
236
+ ],
237
+ outputs: [],
238
+ },
239
+ ],
240
+ functionName: "execute",
241
+ args: [params.dest, params.value ?? 0n, params.callData],
242
+ });
243
+
244
+ return {
245
+ sender: params.sender,
246
+ nonce: params.nonce,
247
+ initCode: params.initCode ?? "0x",
248
+ callData: executeCallData,
249
+ accountGasLimits: "0x000000000000000000000000000f4240000000000000000000000000001e8480",
250
+ preVerificationGas: 50000n,
251
+ gasFees: "0x000000000000000000000000000000010000000000000000000000003b9aca00",
252
+ paymasterAndData: "0x",
253
+ signature: "0x",
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Compute the UserOperation hash for signing.
259
+ *
260
+ * Matches EntryPoint v0.7 packed format:
261
+ * keccak256(abi.encode(keccak256(packedFields), entryPoint, chainId))
262
+ */
263
+ export function getUserOpHash(
264
+ userOp: PackedUserOperation,
265
+ entryPoint: Address,
266
+ chainId: number
267
+ ): Hex {
268
+ const packedHash = keccak256(
269
+ encodeAbiParameters(
270
+ [
271
+ { type: "address" },
272
+ { type: "uint256" },
273
+ { type: "bytes32" },
274
+ { type: "bytes32" },
275
+ { type: "bytes32" },
276
+ { type: "uint256" },
277
+ { type: "bytes32" },
278
+ { type: "bytes32" },
279
+ ],
280
+ [
281
+ userOp.sender,
282
+ userOp.nonce,
283
+ keccak256(userOp.initCode),
284
+ keccak256(userOp.callData),
285
+ userOp.accountGasLimits as Hex,
286
+ userOp.preVerificationGas,
287
+ userOp.gasFees as Hex,
288
+ keccak256(userOp.paymasterAndData),
289
+ ]
290
+ )
291
+ );
292
+
293
+ return keccak256(
294
+ encodeAbiParameters(
295
+ [{ type: "bytes32" }, { type: "address" }, { type: "uint256" }],
296
+ [packedHash, entryPoint, BigInt(chainId)]
297
+ )
298
+ );
299
+ }
300
+
301
+ /**
302
+ * Fetch the current nonce for an account from the EntryPoint.
303
+ */
304
+ export async function fetchNonce(
305
+ rpcUrl: string,
306
+ entryPointAddress: Address,
307
+ account: Address
308
+ ): Promise<bigint> {
309
+ const client = createPublicClient({ transport: http(rpcUrl) });
310
+ return client.readContract({
311
+ address: entryPointAddress,
312
+ abi: [{
313
+ name: "getNonce",
314
+ type: "function",
315
+ inputs: [
316
+ { name: "sender", type: "address" },
317
+ { name: "key", type: "uint192" },
318
+ ],
319
+ outputs: [{ type: "uint256" }],
320
+ stateMutability: "view",
321
+ }],
322
+ functionName: "getNonce",
323
+ args: [account, 0n],
324
+ }) as Promise<bigint>;
325
+ }
326
+
327
+ /**
328
+ * Check if a SignetAccount is deployed at the given address.
329
+ */
330
+ export async function isAccountDeployed(
331
+ rpcUrl: string,
332
+ account: Address
333
+ ): Promise<boolean> {
334
+ const client = createPublicClient({ transport: http(rpcUrl) });
335
+ const code = await client.getCode({ address: account });
336
+ return !!code && code !== "0x";
337
+ }
338
+
339
+ /**
340
+ * Build initCode for deploying a SignetAccount via the account factory.
341
+ * Returns "0x" if the account is already deployed.
342
+ */
343
+ export async function buildInitCode(
344
+ config: Pick<SignetWriteConfig, "rpcUrl" | "accountFactoryAddress" | "accountFactoryAbi" | "entryPointAddress">,
345
+ account: Address,
346
+ groupPublicKey: Hex
347
+ ): Promise<Hex> {
348
+ const deployed = await isAccountDeployed(config.rpcUrl, account);
349
+ if (deployed) return "0x";
350
+
351
+ const factoryCallData = encodeFunctionData({
352
+ abi: config.accountFactoryAbi,
353
+ functionName: "createAccount",
354
+ args: [config.entryPointAddress, groupPublicKey, 0n],
355
+ } as Parameters<typeof encodeFunctionData>[0]);
356
+
357
+ return concat([config.accountFactoryAddress, factoryCallData]);
358
+ }
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Internal helpers
362
+ // ---------------------------------------------------------------------------
363
+
364
+ function hexToBytes(hex: Hex): Uint8Array {
365
+ return new Uint8Array(
366
+ (hex.slice(2).match(/.{2}/g) ?? []).map((b) => parseInt(b, 16))
367
+ );
368
+ }
package/src/witness.ts ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * ZK proof witness construction for the jwt_auth noir circuit.
3
+ *
4
+ * Uses generateInputs from noir-jwt for the core RSA/JWT witness,
5
+ * then adds the claim assertions and session_pub binding that our
6
+ * circuit requires.
7
+ */
8
+
9
+ import { generateInputs } from "./generate-inputs";
10
+ import type { IdTokenClaims } from "./types";
11
+
12
+ /** Full witness for the jwt_auth circuit (Prover.toml format). */
13
+ export interface FullCircuitWitness {
14
+ // From generateInputs (RSA + JWT data)
15
+ data: { storage: number[]; len: number };
16
+ base64_decode_offset: number;
17
+ pubkey_modulus_limbs: string[];
18
+ redc_params_limbs: string[];
19
+ signature_limbs: string[];
20
+
21
+ // Claim assertions (public inputs)
22
+ expected_iss: { storage: number[]; len: number };
23
+ expected_sub: { storage: number[]; len: number };
24
+ expected_exp: number;
25
+ expected_aud: { storage: number[]; len: number };
26
+ expected_azp: { storage: number[]; len: number };
27
+
28
+ // Session binding (public input)
29
+ _session_pub: number[];
30
+ }
31
+
32
+ /**
33
+ * Build full circuit witness from a JWT, JWKS key, and session public key.
34
+ *
35
+ * @param jwt — raw JWT string
36
+ * @param jwksKey — the RSA public key from Google JWKS (as JsonWebKey)
37
+ * @param claims — decoded JWT claims
38
+ * @param sessionPubBytes — 33-byte compressed secp256k1 session public key
39
+ */
40
+ export async function buildFullWitness(
41
+ jwt: string,
42
+ jwksKey: JsonWebKey,
43
+ claims: IdTokenClaims,
44
+ sessionPubBytes: number[]
45
+ ): Promise<FullCircuitWitness> {
46
+ // Generate core JWT/RSA inputs using noir-jwt library
47
+ const inputs = await generateInputs({
48
+ jwt,
49
+ pubkey: jwksKey,
50
+ maxSignedDataLength: 1024,
51
+ });
52
+
53
+ if (!inputs.data) {
54
+ throw new Error("Expected full data mode (no partial SHA)");
55
+ }
56
+
57
+ return {
58
+ // Core JWT/RSA witness
59
+ data: inputs.data,
60
+ base64_decode_offset: inputs.base64_decode_offset,
61
+ pubkey_modulus_limbs: inputs.pubkey_modulus_limbs,
62
+ redc_params_limbs: inputs.redc_params_limbs,
63
+ signature_limbs: inputs.signature_limbs,
64
+
65
+ // Claim assertions
66
+ expected_iss: toBoundedVec(claims.iss, 128),
67
+ expected_sub: toBoundedVec(claims.sub, 128),
68
+ expected_exp: claims.exp,
69
+ expected_aud: toBoundedVec(claims.aud, 128),
70
+ expected_azp: toBoundedVec(claims.azp, 128),
71
+
72
+ // Session binding
73
+ _session_pub: sessionPubBytes,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Serialize a FullCircuitWitness to Prover.toml format for nargo.
79
+ */
80
+ export function witnessToProverToml(w: FullCircuitWitness): string {
81
+ const lines: string[] = [];
82
+
83
+ // Bare keys must come before [table] sections in TOML
84
+ lines.push(`base64_decode_offset = ${w.base64_decode_offset}`);
85
+ lines.push(`expected_exp = ${w.expected_exp}`);
86
+ lines.push(
87
+ `redc_params_limbs = [${w.redc_params_limbs.map((l) => `"${l}"`).join(", ")}]`
88
+ );
89
+ lines.push(
90
+ `signature_limbs = [${w.signature_limbs.map((l) => `"${l}"`).join(", ")}]`
91
+ );
92
+ lines.push(
93
+ `pubkey_modulus_limbs = [${w.pubkey_modulus_limbs.map((l) => `"${l}"`).join(", ")}]`
94
+ );
95
+ lines.push(`_session_pub = [${w._session_pub.join(", ")}]`);
96
+ lines.push("");
97
+
98
+ // BoundedVec tables
99
+ lines.push("[data]");
100
+ lines.push(`storage = [${w.data.storage.join(", ")}]`);
101
+ lines.push(`len = ${w.data.len}`);
102
+ lines.push("");
103
+
104
+ writeBoundedVecToml(lines, "expected_iss", w.expected_iss);
105
+ writeBoundedVecToml(lines, "expected_sub", w.expected_sub);
106
+ writeBoundedVecToml(lines, "expected_aud", w.expected_aud);
107
+ writeBoundedVecToml(lines, "expected_azp", w.expected_azp);
108
+
109
+ return lines.join("\n");
110
+ }
111
+
112
+ function toBoundedVec(
113
+ value: string,
114
+ maxLen: number
115
+ ): { storage: number[]; len: number } {
116
+ const storage = new Array(maxLen).fill(0);
117
+ for (let i = 0; i < value.length; i++) {
118
+ storage[i] = value.charCodeAt(i);
119
+ }
120
+ return { storage, len: value.length };
121
+ }
122
+
123
+ function writeBoundedVecToml(
124
+ lines: string[],
125
+ name: string,
126
+ vec: { storage: number[]; len: number }
127
+ ): void {
128
+ lines.push(`[${name}]`);
129
+ lines.push(`storage = [${vec.storage.join(", ")}]`);
130
+ lines.push(`len = ${vec.len}`);
131
+ lines.push("");
132
+ }