@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.
- package/dist/discovery/attestation-signer.d.ts +78 -0
- package/dist/discovery/attestation-signer.d.ts.map +1 -0
- package/dist/discovery/attestation-signer.js +111 -0
- package/dist/discovery/attestation-signer.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/discovery/attestation-signer.ts +171 -0
- package/src/index.ts +1 -0
- package/test/attestation-signer.test.ts +162 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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
|
@@ -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
|
@@ -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
|
+
})
|