@kushbitx/sdk 0.1.6 → 0.1.7
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 +12 -0
- package/kushbitx.d.ts +9 -3
- package/kushbitx.mjs +75 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,6 +80,16 @@ if (await canSignTypedData(report, request)) {
|
|
|
80
80
|
|
|
81
81
|
`typedDataDigest(typedData)` returns the same digest on its own. Neither function uses the network or a key.
|
|
82
82
|
|
|
83
|
+
**Signed verdicts (0.1.7).** An ALLOW from Signature Check carries `signedVerdict`: an EIP-712 `SignatureVerdict` signed by a KushBitx key, valid at most 60 seconds. When it is present, `canSignTypedData` also verifies it: same `typedDataDigest` and signer, chainId 8453, ALLOW, not expired, valid signature. For full trust, pass the active signer addresses from https://kushbitx.com/.well-known/kushbitx-preflight-signers.json:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const signers = (await (await fetch('https://kushbitx.com/.well-known/kushbitx-preflight-signers.json')).json())
|
|
87
|
+
.signers.filter(s => s.status === 'active').map(s => s.address);
|
|
88
|
+
if (await canSignTypedData(report, request, Date.now(), { signers, requireSigned: true })) { /* sign now */ }
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`verifySignedVerdict(signedVerdict, { primaryType, signers })` checks any KushBitx signed verdict on its own. Always pass `primaryType`: a `SignatureVerdict` (Signature Check) is never a `PreflightVerdict` (Preflight). `recoverAddress(digest, signature)` is the dependency-free secp256k1 recovery it uses.
|
|
92
|
+
|
|
83
93
|
Required checks per transaction type: `requiredChecks` in https://kushbitx.com/api/services.
|
|
84
94
|
|
|
85
95
|
Current prices: https://kushbitx.com/api/services
|
|
@@ -117,6 +127,8 @@ import { KushBitxClient } from 'https://kushbitx.com/sdk/kushbitx.mjs';
|
|
|
117
127
|
- Tools: https://kushbitx.com/#tools
|
|
118
128
|
- SpendGuard: https://kushbitx.com/spendguard
|
|
119
129
|
- Integration example: https://kushbitx.com/examples/agent.mjs
|
|
130
|
+
- Clodds skill (optional, external): [integrations/clodds/kushbitx-preflight](integrations/clodds/kushbitx-preflight). Copy the folder into .clodds/skills/ to run KushBitx Preflight before signing Base transactions. Not part of CloddsBot and not endorsed by its maintainers.
|
|
131
|
+
- Franklin PreSpend hook (optional, external): [integrations/franklin/kushbitx-token-risk](integrations/franklin/kushbitx-token-risk). Screens the token a Base0xSwap buys with KushBitx Token Risk before the swap runs. It sees token names, not the transaction; read its limits first. Not part of Franklin and not endorsed by BlockRun.
|
|
120
132
|
- Contact: Admin@kushbitx.com
|
|
121
133
|
|
|
122
134
|
## Community contributions
|
package/kushbitx.d.ts
CHANGED
|
@@ -133,11 +133,17 @@ export interface TypedData { domain: { name?: string; version?: string; chainId?
|
|
|
133
133
|
/** Input of Signature Check (0.003 USDC). */
|
|
134
134
|
export interface SignatureCheckInput { chainId: 8453; signer: Address; typedData: TypedData; expectedPayTo?: Address; expectedAmount?: string; expectedSpender?: Address }
|
|
135
135
|
/** Signature Check report (fields used by canSignTypedData). */
|
|
136
|
-
export interface SignatureCheckReport { service: 'signature-check'; verdict: 'ALLOW' | 'REVIEW' | 'BLOCK'; typedDataDigest: `0x${string}`; chainId: 8453; signer: Address; expiresAt: string; kind: string; reasonCodes: string[]; [k: string]: unknown }
|
|
136
|
+
export interface SignatureCheckReport { service: 'signature-check'; verdict: 'ALLOW' | 'REVIEW' | 'BLOCK'; typedDataDigest: `0x${string}`; chainId: 8453; signer: Address; expiresAt: string; kind: string; reasonCodes: string[]; signedVerdict?: SignedVerdict | null; signedVerdictNote?: string; [k: string]: unknown }
|
|
137
137
|
/** EIP-712 digest of typed data, computed locally (no network). Equals typedDataDigest in a Signature Check report. Throws on malformed typed data. */
|
|
138
138
|
export declare function typedDataDigest(typedData: TypedData): `0x${string}`;
|
|
139
|
-
/**
|
|
140
|
-
export
|
|
139
|
+
/** A KushBitx signed verdict (ALLOW only). Preflight: primaryType PreflightVerdict. Signature Check: primaryType SignatureVerdict. Never interchangeable. */
|
|
140
|
+
export interface SignedVerdict { scheme: "eip712"; primaryType?: "PreflightVerdict" | "SignatureVerdict"; keyId: string; signer: Address; rulesVersion: string; typedData: TypedData; digest: `0x${string}`; signature: `0x${string}`; verify?: string; [k: string]: unknown }
|
|
141
|
+
/** Address that produced a 65-byte r,s,v signature over a 32-byte digest, or null (high s and v other than 27/28 are rejected). No dependency, no network. */
|
|
142
|
+
export declare function recoverAddress(digest: string, signature: string): Address | null;
|
|
143
|
+
/** Verifies a KushBitx signedVerdict: domain KushBitx Preflight / 1 / 8453, primaryType, recomputed digest and signature. Pass signers (active addresses from /.well-known/kushbitx-preflight-signers.json) for full trust. Never throws. */
|
|
144
|
+
export declare function verifySignedVerdict(signedVerdict: SignedVerdict | unknown, options?: { primaryType?: "PreflightVerdict" | "SignatureVerdict"; signers?: string[] }): { valid: boolean; signer: Address | null; reason: string | null };
|
|
145
|
+
/** True only for ALLOW on this exact typed data, for this signer, before expiresAt. request.chainId and typedData.domain.chainId must both be 8453. When the report carries signedVerdict, it must verify too (SignatureVerdict, same digest and signer, not expired); options.signers restricts the key; options.requireSigned makes it mandatory. Anything else is false. */
|
|
146
|
+
export declare function canSignTypedData(report: SignatureCheckReport | Record<string, unknown>, request: { chainId: number | string | bigint; signer: Address; typedData: TypedData }, now?: number, options?: { signers?: string[]; requireSigned?: boolean }): Promise<boolean>;
|
|
141
147
|
|
|
142
148
|
export declare class KushBitxClient {
|
|
143
149
|
constructor(options?: ClientOptions);
|
package/kushbitx.mjs
CHANGED
|
@@ -246,12 +246,77 @@ export function typedDataDigest(typedData) {
|
|
|
246
246
|
return '0x' + Array.from(digest, b => b.toString(16).padStart(2, '0')).join('');
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
// secp256k1 public-key recovery (no dependency), used only to verify KushBitx signed verdicts.
|
|
250
|
+
const SECP_P = 2n ** 256n - 2n ** 32n - 977n;
|
|
251
|
+
const SECP_N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
252
|
+
const SECP_G = [0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n, 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n];
|
|
253
|
+
const modp = (a, m = SECP_P) => { const r = a % m; return r >= 0n ? r : r + m; };
|
|
254
|
+
function modInv(a, m) { let [x, y, u, v] = [0n, 1n, m, modp(a, m)]; while (v !== 0n) { const q = u / v; [x, y] = [y, x - q * y]; [u, v] = [v, u - q * v]; } if (u !== 1n) throw new Error('no inverse'); return modp(x, m); }
|
|
255
|
+
function modPow(b, e, m) { let r = 1n; b = modp(b, m); while (e > 0n) { if (e & 1n) r = r * b % m; b = b * b % m; e >>= 1n; } return r; }
|
|
256
|
+
// Jacobian coordinates [X, Y, Z]; null is the point at infinity.
|
|
257
|
+
function jDouble(P) { if (!P || P[1] === 0n) return null; const [X, Y, Z] = P; const S = modp(4n * X * Y * Y), M = modp(3n * X * X), X3 = modp(M * M - 2n * S); return [X3, modp(M * (S - X3) - 8n * Y ** 4n), modp(2n * Y * Z)]; }
|
|
258
|
+
function jAdd(P, Q) {
|
|
259
|
+
if (!P) return Q; if (!Q) return P;
|
|
260
|
+
const [X1, Y1, Z1] = P, [X2, Y2, Z2] = Q, Z1s = modp(Z1 * Z1), Z2s = modp(Z2 * Z2);
|
|
261
|
+
const U1 = modp(X1 * Z2s), U2 = modp(X2 * Z1s), S1 = modp(Y1 * Z2s * Z2), S2 = modp(Y2 * Z1s * Z1);
|
|
262
|
+
if (U1 === U2) return S1 === S2 ? jDouble(P) : null;
|
|
263
|
+
const H = modp(U2 - U1), R = modp(S2 - S1), H2 = modp(H * H), H3 = modp(H2 * H), X3 = modp(R * R - H3 - 2n * U1 * H2);
|
|
264
|
+
return [X3, modp(R * (U1 * H2 - X3) - S1 * H3), modp(H * Z1 * Z2)];
|
|
265
|
+
}
|
|
266
|
+
function jMul(P, k) { let R = null, A = P; while (k > 0n) { if (k & 1n) R = jAdd(R, A); A = jDouble(A); k >>= 1n; } return R; }
|
|
267
|
+
function toAffine(P) { if (!P) return null; const zi = modInv(P[2], SECP_P), zi2 = modp(zi * zi); return [modp(P[0] * zi2), modp(P[1] * zi2 * zi)]; }
|
|
268
|
+
const hex32 = n => n.toString(16).padStart(64, '0');
|
|
269
|
+
/** Address that produced a 65-byte r,s,v signature over a 32-byte digest, or null. Rejects high s and v other than 27/28. */
|
|
270
|
+
export function recoverAddress(digestHex, signatureHex) {
|
|
271
|
+
try {
|
|
272
|
+
const sig = String(signatureHex).replace(/^0x/i, ''), dig = String(digestHex).replace(/^0x/i, '');
|
|
273
|
+
if (!/^[0-9a-f]{130}$/i.test(sig) || !/^[0-9a-f]{64}$/i.test(dig)) return null;
|
|
274
|
+
const r = BigInt('0x' + sig.slice(0, 64)), s = BigInt('0x' + sig.slice(64, 128)), v = parseInt(sig.slice(128), 16);
|
|
275
|
+
if ((v !== 27 && v !== 28) || r <= 0n || r >= SECP_N || s <= 0n || s > SECP_N / 2n) return null;
|
|
276
|
+
const y2 = modp(r ** 3n + 7n); let y = modPow(y2, (SECP_P + 1n) / 4n, SECP_P);
|
|
277
|
+
if (modp(y * y) !== y2) return null;
|
|
278
|
+
if ((y & 1n) !== BigInt(v - 27)) y = SECP_P - y;
|
|
279
|
+
const e = BigInt('0x' + dig) % SECP_N, rInv = modInv(r, SECP_N);
|
|
280
|
+
const Q = toAffine(jAdd(jMul([SECP_G[0], SECP_G[1], 1n], modp(-e * rInv, SECP_N)), jMul([r, y, 1n], modp(s * rInv, SECP_N))));
|
|
281
|
+
if (!Q) return null;
|
|
282
|
+
const hash = keccak256(hexBytes(hex32(Q[0]) + hex32(Q[1])));
|
|
283
|
+
return '0x' + Array.from(hash.slice(12), b => b.toString(16).padStart(2, '0')).join('');
|
|
284
|
+
} catch { return null; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const VERDICT_PRIMARY_TYPES = ['PreflightVerdict', 'SignatureVerdict'];
|
|
288
|
+
/**
|
|
289
|
+
* Verifies a KushBitx signedVerdict (Preflight or Signature Check): the domain is KushBitx Preflight / 1 / 8453, the
|
|
290
|
+
* primaryType is the one expected, the EIP-712 digest recomputes, and the signature recovers to signedVerdict.signer.
|
|
291
|
+
* options.signers: addresses listed as active at /.well-known/kushbitx-preflight-signers.json; pass them for full trust.
|
|
292
|
+
* options.primaryType: 'PreflightVerdict' or 'SignatureVerdict' (always pass it; the two are never interchangeable).
|
|
293
|
+
* Returns { valid, signer, reason }. Never throws.
|
|
294
|
+
*/
|
|
295
|
+
export function verifySignedVerdict(signedVerdict, options = {}) {
|
|
296
|
+
const fail = reason => ({ valid: false, signer: null, reason });
|
|
297
|
+
try {
|
|
298
|
+
if (!signedVerdict || typeof signedVerdict !== 'object') return fail('no signedVerdict');
|
|
299
|
+
const td = signedVerdict.typedData, d = td?.domain;
|
|
300
|
+
if (!td || !d || d.name !== 'KushBitx Preflight' || String(d.version) !== '1' || chainIdOf({ chainId: d.chainId }) !== BASE_CHAIN_ID) return fail('wrong domain');
|
|
301
|
+
if (!VERDICT_PRIMARY_TYPES.includes(td.primaryType) || (options.primaryType && td.primaryType !== options.primaryType)) return fail('wrong primaryType');
|
|
302
|
+
const digest = typedDataDigest(td);
|
|
303
|
+
if (String(signedVerdict.digest || '').toLowerCase() !== digest) return fail('digest does not match the typed data');
|
|
304
|
+
const signer = recoverAddress(digest, signedVerdict.signature);
|
|
305
|
+
if (!signer || signer !== String(signedVerdict.signer || '').toLowerCase()) return fail('bad signature');
|
|
306
|
+
if (Array.isArray(options.signers) && !options.signers.map(a => String(a).toLowerCase()).includes(signer)) return fail('signer not in the trusted list');
|
|
307
|
+
return { valid: true, signer, reason: null };
|
|
308
|
+
} catch { return fail('malformed signedVerdict'); }
|
|
309
|
+
}
|
|
310
|
+
|
|
249
311
|
/**
|
|
250
312
|
* True only for an ALLOW Signature Check report on this exact typed data, for this signer, on Base, before expiresAt.
|
|
251
313
|
* request: { chainId: 8453 (stated), signer, typedData }. typedData.domain.chainId must also be 8453: a signature
|
|
252
314
|
* without a chain in its domain could be replayed elsewhere, so it is never accepted. Anything else: do not sign.
|
|
315
|
+
* When the report carries signedVerdict, it must also verify: a SignatureVerdict for the same typedDataDigest and
|
|
316
|
+
* signer, chainId 8453, ALLOW, not expired, with a valid KushBitx signature. options.signers (active addresses from
|
|
317
|
+
* /.well-known/kushbitx-preflight-signers.json) restricts the key; options.requireSigned makes signedVerdict mandatory.
|
|
253
318
|
*/
|
|
254
|
-
export async function canSignTypedData(report, request, now = Date.now()) {
|
|
319
|
+
export async function canSignTypedData(report, request, now = Date.now(), options = {}) {
|
|
255
320
|
if (!report || typeof report !== 'object' || report.verdict !== 'ALLOW' || typeof report.typedDataDigest !== 'string') return false;
|
|
256
321
|
if (!request || typeof request !== 'object' || request.chainId === undefined || request.chainId === null || chainIdOf(request) !== BASE_CHAIN_ID) return false;
|
|
257
322
|
if (report.chainId !== undefined && Number(report.chainId) !== BASE_CHAIN_ID) return false;
|
|
@@ -259,9 +324,13 @@ export async function canSignTypedData(report, request, now = Date.now()) {
|
|
|
259
324
|
if (domainChain === undefined || domainChain === null || chainIdOf({ chainId: domainChain }) !== BASE_CHAIN_ID) return false;
|
|
260
325
|
if (typeof request.signer !== 'string' || typeof report.signer !== 'string' || request.signer.toLowerCase() !== report.signer.toLowerCase()) return false;
|
|
261
326
|
if (!report.expiresAt || !(Date.parse(report.expiresAt) > now)) return false;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
let local;
|
|
328
|
+
try { local = typedDataDigest(request.typedData); } catch { return false; }
|
|
329
|
+
if (report.typedDataDigest.toLowerCase() !== local) return false;
|
|
330
|
+
const sv = report.signedVerdict;
|
|
331
|
+
if (sv === undefined || sv === null) return !options.requireSigned;
|
|
332
|
+
const m = sv.typedData?.message;
|
|
333
|
+
if (!m || String(m.typedDataDigest || '').toLowerCase() !== local || String(m.signer || '').toLowerCase() !== request.signer.toLowerCase()) return false;
|
|
334
|
+
if (Number(m.chainId) !== BASE_CHAIN_ID || Number(m.verdict) !== 1 || !(Number(m.expiresAt) * 1000 > now)) return false;
|
|
335
|
+
return verifySignedVerdict(sv, { primaryType: 'SignatureVerdict', signers: options.signers }).valid;
|
|
267
336
|
}
|