Skip to content

Commit e9e4e2a

Browse files
BarisSozenclaude
andcommitted
feat(types): expose principal + attestation type surface (experimental)
Adds the agent-layer type surface to @hashlock-tech/sdk so SDK consumers get TypeScript autocomplete and shape-validation for principal attestation, agent instance metadata, KYC tier filters, and hideIdentity flags today. GraphQL wire-through to the Cayman backend lands in a later release once the backend accepts PrincipalAttestationInput and AgentInstanceInput. New - src/principal.ts: KycTier + PrincipalType + PrincipalAttestation + AgentInstance interfaces, KYC_TIER_RANK ordering, meetsKycTier helper. Mirror of the canonical types in @hashlock-tech/intent-schema, duplicated as plain TS interfaces per the SDK pattern (no zod runtime dep). Updated - src/types.ts - RFQ: attestationTier, attestationBlindId, minCounterpartyTier response fields (all optional + nullable) - Quote: attestationTier, attestationBlindId - Trade: initiatorAttestationTier, counterpartyAttestationTier - CreateRFQInput: attestation, agentInstance, minCounterpartyTier, hideIdentity inputs - SubmitQuoteInput: attestation, agentInstance, hideIdentity - FundHTLCInput: attestation, agentInstance All marked EXPERIMENTAL in doc comments — accepted by SDK, dropped by current GraphQL mutation strings, wire-through pending Cayman backend update. - src/index.ts: re-export new principal helpers + type aliases Tests - src/__tests__/hashlock.test.ts gains a "principal + attestation types" suite covering: * KycTier ordering and meetsKycTier comparisons * createRFQ with full agent payload (attestation + agentInstance + minCounterpartyTier + hideIdentity) * submitQuote with market-maker attestation * fundHTLC with funding-party attestation * backward compatibility: human calls without attestation remain unchanged * RFQ response parsing with attestationTier fields - All 25 tests pass (19 existing + 6 new); build is clean Not included - No changes to src/hashlock.ts GraphQL mutation strings. The new fields ride through the input interfaces but are silently omitted by the variables serialization until the backend schema accepts them. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e949535 commit e9e4e2a

4 files changed

Lines changed: 279 additions & 0 deletions

File tree

src/__tests__/hashlock.test.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, it, expect, vi, beforeEach } from 'vitest';
22
import { HashLock } from '../hashlock.js';
33
import { GraphQLError, AuthError, NetworkError } from '../errors.js';
4+
import { meetsKycTier, KYC_TIER_RANK } from '../principal.js';
5+
import type { PrincipalAttestation } from '../principal.js';
46

57
function mockFetch(data: unknown, status = 200) {
68
return vi.fn().mockResolvedValue({
@@ -228,4 +230,168 @@ describe('HashLock SDK', () => {
228230
expect(callArgs[1].headers['Authorization']).toBe('Bearer new-token');
229231
});
230232
});
233+
234+
// ─── Principal / Attestation (EXPERIMENTAL) ─────────────
235+
236+
describe('principal + attestation types', () => {
237+
const validAttestation: PrincipalAttestation = {
238+
principalId: 'pr_acme_001',
239+
principalType: 'INSTITUTION',
240+
tier: 'INSTITUTIONAL',
241+
blindId: 'ag_5g7k92bq',
242+
issuedAt: Math.floor(Date.now() / 1000),
243+
expiresAt: Math.floor(Date.now() / 1000) + 3600,
244+
proof: '0xdeadbeef',
245+
};
246+
247+
it('KycTier helpers rank tiers correctly', () => {
248+
expect(KYC_TIER_RANK.NONE).toBe(0);
249+
expect(KYC_TIER_RANK.INSTITUTIONAL).toBe(4);
250+
expect(meetsKycTier('ENHANCED', 'STANDARD')).toBe(true);
251+
expect(meetsKycTier('BASIC', 'ENHANCED')).toBe(false);
252+
});
253+
254+
it('createRFQ accepts attestation + agentInstance + tier filters', async () => {
255+
const rfq = {
256+
id: 'rfq-agent',
257+
baseToken: 'ETH',
258+
quoteToken: 'USDT',
259+
side: 'SELL',
260+
amount: '5.0',
261+
status: 'ACTIVE',
262+
isBlind: true,
263+
createdAt: '2026-04-11',
264+
userId: 'u1',
265+
expiresAt: null,
266+
quotesCount: 0,
267+
};
268+
const fetch = mockFetch({ data: { createRFQ: rfq } });
269+
const hl = createClient(fetch);
270+
271+
// Type-level acceptance: the SDK compiles and runs with the
272+
// new fields. GraphQL wire-through is a later release.
273+
const result = await hl.createRFQ({
274+
baseToken: 'ETH',
275+
quoteToken: 'USDT',
276+
side: 'SELL',
277+
amount: '5.0',
278+
isBlind: true,
279+
attestation: validAttestation,
280+
agentInstance: {
281+
instanceId: 'ag_5g7k92bq',
282+
strategy: 'mm-eth-usdt',
283+
version: '1.0.0',
284+
},
285+
minCounterpartyTier: 'STANDARD',
286+
hideIdentity: true,
287+
});
288+
289+
expect(result.id).toBe('rfq-agent');
290+
});
291+
292+
it('submitQuote accepts attestation + agentInstance + hideIdentity', async () => {
293+
const quote = {
294+
id: 'q-agent',
295+
rfqId: 'rfq-1',
296+
marketMakerId: 'mm-1',
297+
price: '3450',
298+
amount: '1.0',
299+
status: 'PENDING',
300+
createdAt: '2026-04-11',
301+
expiresAt: null,
302+
};
303+
const fetch = mockFetch({ data: { submitQuote: quote } });
304+
const hl = createClient(fetch);
305+
306+
const result = await hl.submitQuote({
307+
rfqId: 'rfq-1',
308+
price: '3450',
309+
amount: '1.0',
310+
attestation: validAttestation,
311+
agentInstance: { instanceId: 'ag_5g7k92bq' },
312+
hideIdentity: true,
313+
});
314+
315+
expect(result.id).toBe('q-agent');
316+
});
317+
318+
it('fundHTLC accepts attestation + agentInstance', async () => {
319+
const fetch = mockFetch({
320+
data: { fundHTLC: { tradeId: 't-1', txHash: '0xabc', status: 'PENDING' } },
321+
});
322+
const hl = createClient(fetch);
323+
324+
const result = await hl.fundHTLC({
325+
tradeId: 't-1',
326+
txHash: '0xabc',
327+
role: 'INITIATOR',
328+
chainType: 'evm',
329+
attestation: validAttestation,
330+
agentInstance: { instanceId: 'ag_5g7k92bq' },
331+
});
332+
333+
expect(result.status).toBe('PENDING');
334+
});
335+
336+
it('existing calls without attestation still work (backward compat)', async () => {
337+
const rfq = {
338+
id: 'rfq-human',
339+
baseToken: 'ETH',
340+
quoteToken: 'USDT',
341+
side: 'BUY',
342+
amount: '1.0',
343+
status: 'ACTIVE',
344+
isBlind: false,
345+
createdAt: '2026-04-11',
346+
userId: 'u1',
347+
expiresAt: null,
348+
quotesCount: 0,
349+
};
350+
const fetch = mockFetch({ data: { createRFQ: rfq } });
351+
const hl = createClient(fetch);
352+
353+
const result = await hl.createRFQ({
354+
baseToken: 'ETH',
355+
quoteToken: 'USDT',
356+
side: 'BUY',
357+
amount: '1.0',
358+
});
359+
360+
expect(result.id).toBe('rfq-human');
361+
});
362+
363+
it('parses attestation tier fields from RFQ responses', async () => {
364+
const rfq = {
365+
id: 'rfq-1',
366+
userId: 'u1',
367+
baseToken: 'ETH',
368+
quoteToken: 'USDT',
369+
side: 'SELL',
370+
amount: '5',
371+
status: 'ACTIVE',
372+
isBlind: true,
373+
createdAt: '2026-04-11',
374+
expiresAt: null,
375+
quotesCount: 0,
376+
attestationTier: 'INSTITUTIONAL',
377+
attestationBlindId: 'ag_5g7k92bq',
378+
minCounterpartyTier: 'STANDARD',
379+
};
380+
const fetch = mockFetch({ data: { createRFQ: rfq } });
381+
const hl = createClient(fetch);
382+
383+
const result = await hl.createRFQ({
384+
baseToken: 'ETH',
385+
quoteToken: 'USDT',
386+
side: 'SELL',
387+
amount: '5',
388+
});
389+
390+
// Type is `KycTier | null | undefined`; the JSON response is
391+
// assignable (TS response types are a loose projection).
392+
expect(result.attestationTier).toBe('INSTITUTIONAL');
393+
expect(result.attestationBlindId).toBe('ag_5g7k92bq');
394+
expect(result.minCounterpartyTier).toBe('STANDARD');
395+
});
396+
});
231397
});

src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
export { HashLock, MAINNET_ENDPOINT } from './hashlock.js';
22
export { HashLockError, GraphQLError, NetworkError, AuthError } from './errors.js';
33
export type * from './types.js';
4+
export { KYC_TIER_RANK, meetsKycTier } from './principal.js';
5+
export type {
6+
KycTier,
7+
PrincipalType,
8+
PrincipalAttestation,
9+
AgentInstance,
10+
} from './principal.js';

src/principal.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// ─── Principal + Attestation Types ───────────────────────────
2+
//
3+
// Mirror of the canonical types defined in @hashlock-tech/intent-schema.
4+
// Duplicated here as plain TS interfaces (the SDK pattern) so the
5+
// SDK has no runtime dependency on the intent-schema package.
6+
//
7+
// Keep these shapes in sync with:
8+
// @hashlock-tech/intent-schema → src/types/principal.ts
9+
//
10+
// EXPERIMENTAL: these fields are defined at the SDK type surface so
11+
// agents can construct the right shape today. GraphQL wire-through
12+
// to the Cayman backend happens in a later release once the backend
13+
// schema is updated to accept PrincipalAttestationInput and
14+
// AgentInstanceInput. Until then, passing these fields to SDK
15+
// methods is a no-op at the network layer.
16+
17+
export type KycTier =
18+
| 'NONE'
19+
| 'BASIC'
20+
| 'STANDARD'
21+
| 'ENHANCED'
22+
| 'INSTITUTIONAL';
23+
24+
export type PrincipalType = 'HUMAN' | 'INSTITUTION' | 'AGENT';
25+
26+
export interface PrincipalAttestation {
27+
/** Opaque identifier (hash) of the KYC'd principal entity */
28+
principalId: string;
29+
/** Kind of principal backing the intent/order */
30+
principalType: PrincipalType;
31+
/** Attested compliance tier of the principal */
32+
tier: KycTier;
33+
/** Rotating pseudonym visible to counterparty (omit for post-match attribution only) */
34+
blindId?: string;
35+
/** Attestation issuance time (unix seconds) */
36+
issuedAt: number;
37+
/** Attestation expiration (unix seconds) */
38+
expiresAt: number;
39+
/** Opaque proof (signature or ZK proof) verified by the HashLock gateway */
40+
proof: string;
41+
}
42+
43+
export interface AgentInstance {
44+
/** Stable identifier for the agent instance */
45+
instanceId: string;
46+
/** Human-readable strategy label (e.g. "mm-eth-usdc") */
47+
strategy?: string;
48+
/** Agent software version */
49+
version?: string;
50+
/** Instance spawn time (unix seconds) */
51+
spawnedAt?: number;
52+
}
53+
54+
export const KYC_TIER_RANK: Record<KycTier, number> = {
55+
NONE: 0,
56+
BASIC: 1,
57+
STANDARD: 2,
58+
ENHANCED: 3,
59+
INSTITUTIONAL: 4,
60+
};
61+
62+
export function meetsKycTier(actual: KycTier, required: KycTier): boolean {
63+
return KYC_TIER_RANK[actual] >= KYC_TIER_RANK[required];
64+
}

src/types.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import type {
2+
PrincipalAttestation,
3+
AgentInstance,
4+
KycTier,
5+
} from './principal.js';
6+
17
// ─── Enums ───────────────────────────────────────────────────
28

39
export type Side = 'BUY' | 'SELL';
@@ -53,6 +59,12 @@ export interface RFQ {
5359
createdAt: string;
5460
quotesCount: number | null;
5561
quotes: Quote[] | null;
62+
/** EXPERIMENTAL: tier the creator attested to (visible, non-leaky) */
63+
attestationTier?: KycTier | null;
64+
/** EXPERIMENTAL: rotating pseudonym of the creator (blind identity) */
65+
attestationBlindId?: string | null;
66+
/** EXPERIMENTAL: minimum KYC tier the creator wants in a counterparty */
67+
minCounterpartyTier?: KycTier | null;
5668
}
5769

5870
export interface Quote {
@@ -67,6 +79,10 @@ export interface Quote {
6779
deliveryDelayHours: number | null;
6880
collateralBtcSats: string | null;
6981
isCollateralBacked: boolean;
82+
/** EXPERIMENTAL: tier the market maker attested to */
83+
attestationTier?: KycTier | null;
84+
/** EXPERIMENTAL: blind identity of the market maker */
85+
attestationBlindId?: string | null;
7086
}
7187

7288
export interface Trade {
@@ -81,6 +97,10 @@ export interface Trade {
8197
price: string;
8298
status: TradeStatus;
8399
createdAt: string;
100+
/** EXPERIMENTAL: initiator's attested compliance tier */
101+
initiatorAttestationTier?: KycTier | null;
102+
/** EXPERIMENTAL: counterparty's attested compliance tier */
103+
counterpartyAttestationTier?: KycTier | null;
84104
}
85105

86106
export interface HTLC {
@@ -119,6 +139,18 @@ export interface CreateRFQInput {
119139
expiresIn?: number;
120140
/** Hide counterparty identity in blind auction mode */
121141
isBlind?: boolean;
142+
/** EXPERIMENTAL — Principal attestation for agent / institution
143+
* flows. The shape is accepted by the SDK today but is not yet
144+
* sent to the Cayman backend. Wire-through will land in a later
145+
* release once the backend accepts PrincipalAttestationInput. */
146+
attestation?: PrincipalAttestation;
147+
/** EXPERIMENTAL — Agent instance metadata. See `attestation`. */
148+
agentInstance?: AgentInstance;
149+
/** EXPERIMENTAL — Minimum KYC tier the counterparty must attest to. */
150+
minCounterpartyTier?: KycTier;
151+
/** EXPERIMENTAL — Hide blind identity in the solver proof. Requires
152+
* `isBlind: true` or a blind auction context. */
153+
hideIdentity?: boolean;
122154
}
123155

124156
export interface SubmitQuoteInput {
@@ -130,6 +162,12 @@ export interface SubmitQuoteInput {
130162
amount: string;
131163
/** Expiration time in seconds */
132164
expiresIn?: number;
165+
/** EXPERIMENTAL — Market maker's principal attestation. */
166+
attestation?: PrincipalAttestation;
167+
/** EXPERIMENTAL — Agent instance metadata. */
168+
agentInstance?: AgentInstance;
169+
/** EXPERIMENTAL — Hide blind identity from the RFQ creator. */
170+
hideIdentity?: boolean;
133171
}
134172

135173
export interface FundHTLCInput {
@@ -155,6 +193,10 @@ export interface FundHTLCInput {
155193
refundTxHex?: string;
156194
/** Preimage for initiator (kept encrypted server-side) */
157195
preimage?: string;
196+
/** EXPERIMENTAL — Principal attestation of the funding party. */
197+
attestation?: PrincipalAttestation;
198+
/** EXPERIMENTAL — Agent instance metadata. */
199+
agentInstance?: AgentInstance;
158200
}
159201

160202
export interface ClaimHTLCInput {

0 commit comments

Comments
 (0)