@swimmingkiim/api-sdk 0.1.35 → 0.1.36

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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Function signature for VC verification.
3
+ * Injected by the caller — can be trust-sdk's VCHandler or any custom impl.
4
+ */
5
+ export type VerifyVCFunction = (vcJwt: string) => Promise<{
6
+ valid: boolean;
7
+ did: string;
8
+ }>;
9
+ export interface AttestationSignerOptions {
10
+ /** Hex-encoded secp256k1 private key of the voucher (any registered agent or bootstrap) */
11
+ privateKey: `0x${string}`;
12
+ /** Address of the deployed CredentialVerifier contract */
13
+ verifierContractAddress: string;
14
+ /** Chain ID (e.g., 8453 for Base Mainnet) */
15
+ chainId: number;
16
+ /** Injected VC verification function */
17
+ verifyVC: VerifyVCFunction;
18
+ /** Attestation validity duration in seconds (default: 3600 = 1 hour) */
19
+ ttlSeconds?: number;
20
+ }
21
+ /**
22
+ * Structured attestation proof ready for on-chain submission.
23
+ */
24
+ export interface AttestationProof {
25
+ /** keccak256 hash of the verified DID */
26
+ didHash: `0x${string}`;
27
+ /** Unix timestamp after which the attestation expires */
28
+ deadline: bigint;
29
+ /** EIP-712 ECDSA signature */
30
+ signature: string;
31
+ /** Encodes the proof as ABI-packed bytes for contract submission */
32
+ encode: () => `0x${string}`;
33
+ }
34
+ /**
35
+ * Creates EIP-712 signed attestation proofs for the on-chain CredentialVerifier.
36
+ *
37
+ * Web of Trust: Any registered agent (or bootstrap voucher) can sign attestations
38
+ * to vouch for new agents. No single trusted signer required.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { AttestationSigner } from '@swimmingkiim/api-sdk'
43
+ * import { VCHandler } from '@swimmingkiim/trust-sdk'
44
+ *
45
+ * const vcHandler = new VCHandler()
46
+ * // Any registered agent's key can be used as voucher
47
+ * const signer = new AttestationSigner({
48
+ * privateKey: process.env.VOUCHER_KEY as `0x${string}`,
49
+ * verifierContractAddress: '0x...',
50
+ * chainId: 8453,
51
+ * verifyVC: async (jwt) => {
52
+ * const valid = await vcHandler.verifyCredential(jwt)
53
+ * const payload = JSON.parse(atob(jwt.split('.')[1]))
54
+ * return { valid, did: payload.iss }
55
+ * },
56
+ * })
57
+ *
58
+ * const proof = await signer.createAttestation(vcJwt, walletAddress)
59
+ * // proof.encode() → bytes for AgentRegistry.register(meta, units, proof.encode())
60
+ * ```
61
+ */
62
+ export declare class AttestationSigner {
63
+ private readonly account;
64
+ private readonly verifierContractAddress;
65
+ private readonly chainId;
66
+ private readonly verifyVC;
67
+ private readonly ttlSeconds;
68
+ constructor(options: AttestationSignerOptions);
69
+ /**
70
+ * Verifies a VC JWT and produces a signed attestation proof.
71
+ *
72
+ * @param vcJwt - Verifiable Credential JWT string
73
+ * @param walletAddress - The Ethereum address of the agent being attested
74
+ * @returns AttestationProof with didHash, deadline, signature, and encode()
75
+ */
76
+ createAttestation(vcJwt: string, walletAddress: string): Promise<AttestationProof>;
77
+ }
78
+ //# sourceMappingURL=attestation-signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation-signer.d.ts","sourceRoot":"","sources":["../../src/discovery/attestation-signer.ts"],"names":[],"mappings":"AAgCA;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAE1F,MAAM,WAAW,wBAAwB;IACrC,2FAA2F;IAC3F,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;IACzB,0DAA0D;IAC1D,uBAAuB,EAAE,MAAM,CAAA;IAC/B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,yCAAyC;IACzC,OAAO,EAAE,KAAK,MAAM,EAAE,CAAA;IACtB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAA;IAChB,8BAA8B;IAC9B,SAAS,EAAE,MAAM,CAAA;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,CAAA;CAC9B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,iBAAiB;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,uBAAuB,CAAe;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;gBAEvB,OAAO,EAAE,wBAAwB;IAU7C;;;;;;OAMG;IACG,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAmD3F"}
@@ -0,0 +1,111 @@
1
+ import { keccak256, toHex, encodeAbiParameters, isAddress, } from 'viem';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { z } from 'zod';
4
+ // --- Validation ---
5
+ const EthAddressSchema = z.string().refine((val) => isAddress(val), { message: 'Invalid Ethereum address format' });
6
+ // --- EIP-712 Type Definitions (must match CredentialVerifier.sol) ---
7
+ const EIP712_DOMAIN = {
8
+ name: 'CredentialVerifier',
9
+ version: '1',
10
+ };
11
+ const ATTESTATION_TYPES = {
12
+ Attestation: [
13
+ { name: 'user', type: 'address' },
14
+ { name: 'didHash', type: 'bytes32' },
15
+ { name: 'deadline', type: 'uint256' },
16
+ ],
17
+ };
18
+ const DEFAULT_TTL_SECONDS = 3600; // 1 hour
19
+ /**
20
+ * Creates EIP-712 signed attestation proofs for the on-chain CredentialVerifier.
21
+ *
22
+ * Web of Trust: Any registered agent (or bootstrap voucher) can sign attestations
23
+ * to vouch for new agents. No single trusted signer required.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { AttestationSigner } from '@swimmingkiim/api-sdk'
28
+ * import { VCHandler } from '@swimmingkiim/trust-sdk'
29
+ *
30
+ * const vcHandler = new VCHandler()
31
+ * // Any registered agent's key can be used as voucher
32
+ * const signer = new AttestationSigner({
33
+ * privateKey: process.env.VOUCHER_KEY as `0x${string}`,
34
+ * verifierContractAddress: '0x...',
35
+ * chainId: 8453,
36
+ * verifyVC: async (jwt) => {
37
+ * const valid = await vcHandler.verifyCredential(jwt)
38
+ * const payload = JSON.parse(atob(jwt.split('.')[1]))
39
+ * return { valid, did: payload.iss }
40
+ * },
41
+ * })
42
+ *
43
+ * const proof = await signer.createAttestation(vcJwt, walletAddress)
44
+ * // proof.encode() → bytes for AgentRegistry.register(meta, units, proof.encode())
45
+ * ```
46
+ */
47
+ export class AttestationSigner {
48
+ account;
49
+ verifierContractAddress;
50
+ chainId;
51
+ verifyVC;
52
+ ttlSeconds;
53
+ constructor(options) {
54
+ EthAddressSchema.parse(options.verifierContractAddress);
55
+ this.account = privateKeyToAccount(options.privateKey);
56
+ this.verifierContractAddress = options.verifierContractAddress;
57
+ this.chainId = options.chainId;
58
+ this.verifyVC = options.verifyVC;
59
+ this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS;
60
+ }
61
+ /**
62
+ * Verifies a VC JWT and produces a signed attestation proof.
63
+ *
64
+ * @param vcJwt - Verifiable Credential JWT string
65
+ * @param walletAddress - The Ethereum address of the agent being attested
66
+ * @returns AttestationProof with didHash, deadline, signature, and encode()
67
+ */
68
+ async createAttestation(vcJwt, walletAddress) {
69
+ // 1. Validate wallet address
70
+ EthAddressSchema.parse(walletAddress);
71
+ // 2. Verify VC
72
+ const { valid, did } = await this.verifyVC(vcJwt);
73
+ if (!valid) {
74
+ throw new Error('VC verification failed');
75
+ }
76
+ // 3. Hash the DID for nullifier
77
+ const didHash = keccak256(toHex(did));
78
+ // 4. Calculate deadline
79
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + this.ttlSeconds);
80
+ if (!this.account.signTypedData) {
81
+ throw new Error('Account does not support signTypedData');
82
+ }
83
+ // 5. Sign EIP-712 typed data
84
+ const signature = await this.account.signTypedData({
85
+ domain: {
86
+ ...EIP712_DOMAIN,
87
+ chainId: this.chainId,
88
+ verifyingContract: this.verifierContractAddress,
89
+ },
90
+ types: ATTESTATION_TYPES,
91
+ primaryType: 'Attestation',
92
+ message: {
93
+ user: walletAddress,
94
+ didHash,
95
+ deadline,
96
+ },
97
+ });
98
+ // 6. Return structured proof
99
+ return {
100
+ didHash,
101
+ deadline,
102
+ signature,
103
+ encode: () => encodeAbiParameters([
104
+ { name: 'didHash', type: 'bytes32' },
105
+ { name: 'deadline', type: 'uint256' },
106
+ { name: 'signature', type: 'bytes' },
107
+ ], [didHash, deadline, signature]),
108
+ };
109
+ }
110
+ }
111
+ //# sourceMappingURL=attestation-signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation-signer.js","sourceRoot":"","sources":["../../src/discovery/attestation-signer.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './mcp-server.js';
2
2
  export * from './discovery/llms-reader.js';
3
3
  export * from './discovery/registry-reader.js';
4
+ export * from './discovery/attestation-signer.js';
4
5
  export * from './types.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './mcp-server.js';
2
2
  export * from './discovery/llms-reader.js';
3
3
  export * from './discovery/registry-reader.js';
4
+ export * from './discovery/attestation-signer.js';
4
5
  export * from './types.js';
5
6
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,4BAA4B,CAAA;AAC1C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,YAAY,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swimmingkiim/api-sdk",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/swimmingkiim/a2a-project.git"
@@ -0,0 +1,171 @@
1
+ import {
2
+ keccak256,
3
+ toHex,
4
+ encodeAbiParameters,
5
+ isAddress,
6
+ } from 'viem'
7
+ import { privateKeyToAccount } from 'viem/accounts'
8
+ import type { Account } from 'viem'
9
+ import { z } from 'zod'
10
+
11
+ // --- Validation ---
12
+ const EthAddressSchema = z.string().refine(
13
+ (val) => isAddress(val),
14
+ { message: 'Invalid Ethereum address format' }
15
+ )
16
+
17
+ // --- EIP-712 Type Definitions (must match CredentialVerifier.sol) ---
18
+ const EIP712_DOMAIN = {
19
+ name: 'CredentialVerifier',
20
+ version: '1',
21
+ } as const
22
+
23
+ const ATTESTATION_TYPES = {
24
+ Attestation: [
25
+ { name: 'user', type: 'address' },
26
+ { name: 'didHash', type: 'bytes32' },
27
+ { name: 'deadline', type: 'uint256' },
28
+ ],
29
+ } as const
30
+
31
+ // --- Types ---
32
+
33
+ /**
34
+ * Function signature for VC verification.
35
+ * Injected by the caller — can be trust-sdk's VCHandler or any custom impl.
36
+ */
37
+ export type VerifyVCFunction = (vcJwt: string) => Promise<{ valid: boolean; did: string }>
38
+
39
+ export interface AttestationSignerOptions {
40
+ /** Hex-encoded secp256k1 private key of the voucher (any registered agent or bootstrap) */
41
+ privateKey: `0x${string}`
42
+ /** Address of the deployed CredentialVerifier contract */
43
+ verifierContractAddress: string
44
+ /** Chain ID (e.g., 8453 for Base Mainnet) */
45
+ chainId: number
46
+ /** Injected VC verification function */
47
+ verifyVC: VerifyVCFunction
48
+ /** Attestation validity duration in seconds (default: 3600 = 1 hour) */
49
+ ttlSeconds?: number
50
+ }
51
+
52
+ /**
53
+ * Structured attestation proof ready for on-chain submission.
54
+ */
55
+ export interface AttestationProof {
56
+ /** keccak256 hash of the verified DID */
57
+ didHash: `0x${string}`
58
+ /** Unix timestamp after which the attestation expires */
59
+ deadline: bigint
60
+ /** EIP-712 ECDSA signature */
61
+ signature: string
62
+ /** Encodes the proof as ABI-packed bytes for contract submission */
63
+ encode: () => `0x${string}`
64
+ }
65
+
66
+ const DEFAULT_TTL_SECONDS = 3600 // 1 hour
67
+
68
+ /**
69
+ * Creates EIP-712 signed attestation proofs for the on-chain CredentialVerifier.
70
+ *
71
+ * Web of Trust: Any registered agent (or bootstrap voucher) can sign attestations
72
+ * to vouch for new agents. No single trusted signer required.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * import { AttestationSigner } from '@swimmingkiim/api-sdk'
77
+ * import { VCHandler } from '@swimmingkiim/trust-sdk'
78
+ *
79
+ * const vcHandler = new VCHandler()
80
+ * // Any registered agent's key can be used as voucher
81
+ * const signer = new AttestationSigner({
82
+ * privateKey: process.env.VOUCHER_KEY as `0x${string}`,
83
+ * verifierContractAddress: '0x...',
84
+ * chainId: 8453,
85
+ * verifyVC: async (jwt) => {
86
+ * const valid = await vcHandler.verifyCredential(jwt)
87
+ * const payload = JSON.parse(atob(jwt.split('.')[1]))
88
+ * return { valid, did: payload.iss }
89
+ * },
90
+ * })
91
+ *
92
+ * const proof = await signer.createAttestation(vcJwt, walletAddress)
93
+ * // proof.encode() → bytes for AgentRegistry.register(meta, units, proof.encode())
94
+ * ```
95
+ */
96
+ export class AttestationSigner {
97
+ private readonly account: Account
98
+ private readonly verifierContractAddress: `0x${string}`
99
+ private readonly chainId: number
100
+ private readonly verifyVC: VerifyVCFunction
101
+ private readonly ttlSeconds: number
102
+
103
+ constructor(options: AttestationSignerOptions) {
104
+ EthAddressSchema.parse(options.verifierContractAddress)
105
+
106
+ this.account = privateKeyToAccount(options.privateKey)
107
+ this.verifierContractAddress = options.verifierContractAddress as `0x${string}`
108
+ this.chainId = options.chainId
109
+ this.verifyVC = options.verifyVC
110
+ this.ttlSeconds = options.ttlSeconds ?? DEFAULT_TTL_SECONDS
111
+ }
112
+
113
+ /**
114
+ * Verifies a VC JWT and produces a signed attestation proof.
115
+ *
116
+ * @param vcJwt - Verifiable Credential JWT string
117
+ * @param walletAddress - The Ethereum address of the agent being attested
118
+ * @returns AttestationProof with didHash, deadline, signature, and encode()
119
+ */
120
+ async createAttestation(vcJwt: string, walletAddress: string): Promise<AttestationProof> {
121
+ // 1. Validate wallet address
122
+ EthAddressSchema.parse(walletAddress)
123
+
124
+ // 2. Verify VC
125
+ const { valid, did } = await this.verifyVC(vcJwt)
126
+ if (!valid) {
127
+ throw new Error('VC verification failed')
128
+ }
129
+
130
+ // 3. Hash the DID for nullifier
131
+ const didHash = keccak256(toHex(did)) as `0x${string}`
132
+
133
+ // 4. Calculate deadline
134
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + this.ttlSeconds)
135
+
136
+ if (!this.account.signTypedData) {
137
+ throw new Error('Account does not support signTypedData')
138
+ }
139
+
140
+ // 5. Sign EIP-712 typed data
141
+ const signature = await this.account.signTypedData({
142
+ domain: {
143
+ ...EIP712_DOMAIN,
144
+ chainId: this.chainId,
145
+ verifyingContract: this.verifierContractAddress,
146
+ },
147
+ types: ATTESTATION_TYPES,
148
+ primaryType: 'Attestation',
149
+ message: {
150
+ user: walletAddress as `0x${string}`,
151
+ didHash,
152
+ deadline,
153
+ },
154
+ })
155
+
156
+ // 6. Return structured proof
157
+ return {
158
+ didHash,
159
+ deadline,
160
+ signature,
161
+ encode: () => encodeAbiParameters(
162
+ [
163
+ { name: 'didHash', type: 'bytes32' },
164
+ { name: 'deadline', type: 'uint256' },
165
+ { name: 'signature', type: 'bytes' },
166
+ ],
167
+ [didHash, deadline, signature as `0x${string}`]
168
+ ),
169
+ }
170
+ }
171
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './mcp-server.js'
2
2
  export * from './discovery/llms-reader.js'
3
3
  export * from './discovery/registry-reader.js'
4
+ export * from './discovery/attestation-signer.js'
4
5
  export * from './types.js'
@@ -0,0 +1,162 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { privateKeyToAccount, generatePrivateKey } from 'viem/accounts'
3
+ import { hashTypedData, recoverAddress, decodeAbiParameters } from 'viem'
4
+
5
+ // We'll import AttestationSigner after creating it
6
+ import { AttestationSigner } from '../src/discovery/attestation-signer.js'
7
+ import type { AttestationProof } from '../src/discovery/attestation-signer.js'
8
+
9
+ const TEST_CHAIN_ID = 8453 // Base Mainnet
10
+ const TEST_VERIFIER_ADDRESS = '0x1234567890123456789012345678901234567890'
11
+ const TEST_WALLET = '0xaabbccddee11223344556677889900aabbccddee'
12
+ const TEST_VC_JWT = 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkaWQ6a2V5Onp0ZXN0In0.c2lnbmF0dXJl'
13
+ const TEST_DID = 'did:key:zTestDID123456789'
14
+
15
+ describe('AttestationSigner', () => {
16
+ let signerKey: `0x${string}`
17
+ let attestationSigner: AttestationSigner
18
+
19
+ // Mock VC verifier that always succeeds and returns DID
20
+ const mockVerifyVC = vi.fn<(vcJwt: string) => Promise<{ valid: boolean; did: string }>>()
21
+
22
+ beforeEach(() => {
23
+ vi.clearAllMocks()
24
+ signerKey = generatePrivateKey()
25
+
26
+ attestationSigner = new AttestationSigner({
27
+ privateKey: signerKey,
28
+ verifierContractAddress: TEST_VERIFIER_ADDRESS,
29
+ chainId: TEST_CHAIN_ID,
30
+ verifyVC: mockVerifyVC,
31
+ })
32
+ })
33
+
34
+ describe('createAttestation', () => {
35
+ it('should produce a valid EIP-712 signed proof', async () => {
36
+ mockVerifyVC.mockResolvedValueOnce({ valid: true, did: TEST_DID })
37
+
38
+ const proof = await attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
39
+
40
+ // Proof should contain didHash, deadline, and signature
41
+ expect(proof.didHash).toBeDefined()
42
+ expect(proof.deadline).toBeGreaterThan(BigInt(Math.floor(Date.now() / 1000)))
43
+ expect(proof.signature).toBeDefined()
44
+ expect(proof.signature).toMatch(/^0x[0-9a-f]+$/i)
45
+ })
46
+
47
+ it('should produce a proof whose signer recovers to the correct address', async () => {
48
+ mockVerifyVC.mockResolvedValueOnce({ valid: true, did: TEST_DID })
49
+
50
+ const proof = await attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
51
+ const signerAccount = privateKeyToAccount(signerKey)
52
+
53
+ // Reconstruct EIP-712 digest and recover
54
+ const digest = hashTypedData({
55
+ domain: {
56
+ name: 'CredentialVerifier',
57
+ version: '1',
58
+ chainId: TEST_CHAIN_ID,
59
+ verifyingContract: TEST_VERIFIER_ADDRESS as `0x${string}`,
60
+ },
61
+ types: {
62
+ Attestation: [
63
+ { name: 'user', type: 'address' },
64
+ { name: 'didHash', type: 'bytes32' },
65
+ { name: 'deadline', type: 'uint256' },
66
+ ],
67
+ },
68
+ primaryType: 'Attestation',
69
+ message: {
70
+ user: TEST_WALLET as `0x${string}`,
71
+ didHash: proof.didHash,
72
+ deadline: proof.deadline,
73
+ },
74
+ })
75
+
76
+ const recovered = await recoverAddress({
77
+ hash: digest,
78
+ signature: proof.signature as `0x${string}`,
79
+ })
80
+
81
+ expect(recovered.toLowerCase()).toBe(signerAccount.address.toLowerCase())
82
+ })
83
+
84
+ it('should produce a deterministic didHash from the DID', async () => {
85
+ mockVerifyVC.mockResolvedValueOnce({ valid: true, did: TEST_DID })
86
+ const proof1 = await attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
87
+
88
+ mockVerifyVC.mockResolvedValueOnce({ valid: true, did: TEST_DID })
89
+ const proof2 = await attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
90
+
91
+ // Same DID → same didHash
92
+ expect(proof1.didHash).toBe(proof2.didHash)
93
+ })
94
+
95
+ it('should encode proof as ABI-packed bytes', async () => {
96
+ mockVerifyVC.mockResolvedValueOnce({ valid: true, did: TEST_DID })
97
+
98
+ const proof = await attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
99
+ const encoded = proof.encode()
100
+
101
+ // Should be decodable
102
+ const [didHash, deadline, signature] = decodeAbiParameters(
103
+ [
104
+ { name: 'didHash', type: 'bytes32' },
105
+ { name: 'deadline', type: 'uint256' },
106
+ { name: 'signature', type: 'bytes' },
107
+ ],
108
+ encoded
109
+ )
110
+
111
+ expect(didHash).toBe(proof.didHash)
112
+ expect(deadline).toBe(proof.deadline)
113
+ expect(signature).toBe(proof.signature)
114
+ })
115
+ })
116
+
117
+ describe('VC Verification Failure', () => {
118
+ it('should throw if VC verification fails', async () => {
119
+ mockVerifyVC.mockResolvedValueOnce({ valid: false, did: '' })
120
+
121
+ await expect(
122
+ attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
123
+ ).rejects.toThrow('VC verification failed')
124
+ })
125
+
126
+ it('should throw if VC verifier throws', async () => {
127
+ mockVerifyVC.mockRejectedValueOnce(new Error('Network error'))
128
+
129
+ await expect(
130
+ attestationSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
131
+ ).rejects.toThrow('Network error')
132
+ })
133
+ })
134
+
135
+ describe('Input Validation', () => {
136
+ it('should reject an invalid wallet address', async () => {
137
+ await expect(
138
+ attestationSigner.createAttestation(TEST_VC_JWT, 'not-an-address')
139
+ ).rejects.toThrow()
140
+ })
141
+ })
142
+
143
+ describe('Custom TTL', () => {
144
+ it('should respect custom ttlSeconds', async () => {
145
+ const customSigner = new AttestationSigner({
146
+ privateKey: signerKey,
147
+ verifierContractAddress: TEST_VERIFIER_ADDRESS,
148
+ chainId: TEST_CHAIN_ID,
149
+ verifyVC: mockVerifyVC,
150
+ ttlSeconds: 60, // 1 minute
151
+ })
152
+
153
+ mockVerifyVC.mockResolvedValueOnce({ valid: true, did: TEST_DID })
154
+ const proof = await customSigner.createAttestation(TEST_VC_JWT, TEST_WALLET)
155
+
156
+ const now = BigInt(Math.floor(Date.now() / 1000))
157
+ // Deadline should be within 60-65 seconds from now (some tolerance)
158
+ expect(proof.deadline - now).toBeLessThanOrEqual(65n)
159
+ expect(proof.deadline - now).toBeGreaterThanOrEqual(55n)
160
+ })
161
+ })
162
+ })