@vlayer/sdk 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- package/.changeset/config.json +11 -0
- package/CHANGELOG.md +7 -0
- package/README.md +7 -0
- package/eslint.config.ts +22 -0
- package/package.json +25 -0
- package/src/api/email/dnsResolver.test.ts +18 -0
- package/src/api/email/dnsResolver.ts +14 -0
- package/src/api/email/parseEmail.test.ts +105 -0
- package/src/api/email/parseEmail.ts +45 -0
- package/src/api/email/preverify.test.ts +37 -0
- package/src/api/email/preverify.ts +19 -0
- package/src/api/email/testdata/test_email.txt +21 -0
- package/src/api/email/testdata/test_email_multiple_dkims.txt +28 -0
- package/src/api/email/testdata/test_email_unknown_domain.txt +21 -0
- package/src/api/helpers.ts +169 -0
- package/src/api/lib/client.ts +18 -0
- package/src/api/lib/types/ethereum.ts +43 -0
- package/src/api/lib/types/index.ts +2 -0
- package/src/api/lib/types/utils.ts +3 -0
- package/src/api/lib/types/vlayer.ts +55 -0
- package/src/api/lib/types/webProof.ts +106 -0
- package/src/api/prover.ts +73 -0
- package/src/api/v_call.ts +44 -0
- package/src/index.ts +8 -0
- package/src/testHelpers/readFile.ts +3 -0
- package/tsconfig.json +30 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
4
|
+
"commit": false,
|
5
|
+
"fixed": [],
|
6
|
+
"linked": [],
|
7
|
+
"access": "restricted",
|
8
|
+
"baseBranch": "main",
|
9
|
+
"updateInternalDependencies": "patch",
|
10
|
+
"ignore": []
|
11
|
+
}
|
package/CHANGELOG.md
ADDED
package/README.md
ADDED
package/eslint.config.ts
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
import js from "@eslint/js";
|
2
|
+
import ts from "typescript-eslint";
|
3
|
+
import prettierRecommended from "eslint-plugin-prettier/recommended";
|
4
|
+
import globals from "globals";
|
5
|
+
|
6
|
+
export default [
|
7
|
+
js.configs.recommended,
|
8
|
+
...ts.configs.recommended,
|
9
|
+
prettierRecommended,
|
10
|
+
{
|
11
|
+
rules: {
|
12
|
+
"no-unused-vars": "warn",
|
13
|
+
"no-undef": "warn",
|
14
|
+
},
|
15
|
+
languageOptions: {
|
16
|
+
globals: {
|
17
|
+
...globals.browser,
|
18
|
+
Bun: false,
|
19
|
+
},
|
20
|
+
},
|
21
|
+
},
|
22
|
+
];
|
package/package.json
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"name": "@vlayer/sdk",
|
3
|
+
"type": "module",
|
4
|
+
"module": "src/index.ts",
|
5
|
+
"version": "0.0.1",
|
6
|
+
"scripts": {
|
7
|
+
"build": "npm run gen:types",
|
8
|
+
"test": "vitest --run",
|
9
|
+
"gen:types": "sh ../bash/build_ts_types.sh"
|
10
|
+
},
|
11
|
+
"devDependencies": {
|
12
|
+
"@changesets/cli": "^2.27.7",
|
13
|
+
"@types/bun": "latest",
|
14
|
+
"@types/mailparser": "^3.4.4",
|
15
|
+
"abitype": "^1.0.6",
|
16
|
+
"vitest": "^2.1.1"
|
17
|
+
},
|
18
|
+
"peerDependencies": {
|
19
|
+
"typescript": "^5.0.0"
|
20
|
+
},
|
21
|
+
"dependencies": {
|
22
|
+
"mailparser": "^3.7.1",
|
23
|
+
"viem": "^2.21.0"
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
2
|
+
import { resolveDkimDns } from "./dnsResolver.ts";
|
3
|
+
|
4
|
+
describe("resolveDkimDns Integration", () => {
|
5
|
+
test("resolves VLayer DNS", async () => {
|
6
|
+
const resolved = await resolveDkimDns(
|
7
|
+
"vlayer-xyz.20230601.gappssmtp.com",
|
8
|
+
"20230601",
|
9
|
+
);
|
10
|
+
const expected =
|
11
|
+
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3gWcOhCm99qzN+h7/2+LeP3CLsJkQQ4EP/2mrceXle5pKq8uZmBl1U4d2Vxn4w+pWFANDLmcHolLboESLFqEL5N6ae7u9b236dW4zn9AFkXAGenTzQEeif9VUFtLAZ0Qh2eV7OQgz/vPj5IaNqJ7h9hpM9gO031fe4v+J0DLCE8Rgo7hXbNgJavctc0983DaCDQaznHZ44LZ6TtZv9TBs+QFvsy4+UCTfsuOtHzoEqOOuXsVXZKLP6B882XbEnBpXEF8QzV4J26HiAJFUbO3mAqZL2UeKC0hhzoIZqZXNG0BfuzOF0VLpDa18GYMUiu+LhEJPJO9D8zhzvQIHNrpGwIDAQAB";
|
12
|
+
expect(resolved).toBe(expected);
|
13
|
+
});
|
14
|
+
|
15
|
+
test("throws error if dns not found", async () => {
|
16
|
+
await expect(resolveDkimDns("not-a-domain.com", "abcd")).rejects.toThrow();
|
17
|
+
});
|
18
|
+
});
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import dns from "node:dns";
|
2
|
+
|
3
|
+
export async function resolveDkimDns(domain: string, selector: string) {
|
4
|
+
return new Promise<string>((resolve, reject) => {
|
5
|
+
dns.resolveTxt(`${selector}._domainkey.${domain}`, (err, addresses) => {
|
6
|
+
if (err) {
|
7
|
+
reject(err);
|
8
|
+
return;
|
9
|
+
}
|
10
|
+
|
11
|
+
resolve(addresses.flat()[0]);
|
12
|
+
});
|
13
|
+
});
|
14
|
+
}
|
@@ -0,0 +1,105 @@
|
|
1
|
+
import assert from "node:assert";
|
2
|
+
import { describe, expect, test } from "vitest";
|
3
|
+
import { getDkimSigners, parseEmail } from "./parseEmail.ts";
|
4
|
+
import { StructuredHeader } from "mailparser";
|
5
|
+
|
6
|
+
const emailHeaders = `From: "John Doe" <john@d.oe>
|
7
|
+
To: "Jane Doe" <jane@d.oe>
|
8
|
+
Subject: Hello World
|
9
|
+
Date: Thu, 1 Jan 1970 00:00:00 +0000
|
10
|
+
`;
|
11
|
+
|
12
|
+
const dkimHeader =
|
13
|
+
"DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; h=from:to:subject; s=selector1; b=abcdef;";
|
14
|
+
|
15
|
+
const body = "Hello, World!";
|
16
|
+
|
17
|
+
const emailFixture = `${emailHeaders}${dkimHeader}\n\n${body}`;
|
18
|
+
|
19
|
+
describe("parseEmail", () => {
|
20
|
+
test("should get dkim header from email", async () => {
|
21
|
+
const email = await parseEmail(emailFixture);
|
22
|
+
const dkim = email.headers.get("dkim-signature")!;
|
23
|
+
assert(typeof dkim === "object" && "params" in dkim);
|
24
|
+
expect(dkim.params.d).toBe("example.com");
|
25
|
+
expect(dkim.params.s).toBe("selector1");
|
26
|
+
});
|
27
|
+
|
28
|
+
test("correctly parses untrimmed email", async () => {
|
29
|
+
const untrimmed = `\n ${emailFixture} \n`;
|
30
|
+
const email = await parseEmail(untrimmed);
|
31
|
+
expect(email.headers.get("dkim-signature")).toBeDefined();
|
32
|
+
});
|
33
|
+
|
34
|
+
test("works well with multiple dkim headers", async () => {
|
35
|
+
const dkimHeader2 =
|
36
|
+
"DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=second.signer; h=from:to:subject; s=selector2; b=abcdef;";
|
37
|
+
|
38
|
+
const email = await parseEmail(
|
39
|
+
`${emailHeaders}${dkimHeader}\n${dkimHeader2}\n\n${body}`,
|
40
|
+
);
|
41
|
+
const dkim = email.headers.get(
|
42
|
+
"dkim-signature",
|
43
|
+
)! as unknown as StructuredHeader[];
|
44
|
+
expect(dkim).toHaveLength(2);
|
45
|
+
expect(dkim[0].params.s).toBe("selector1");
|
46
|
+
expect(dkim[1].params.s).toBe("selector2");
|
47
|
+
});
|
48
|
+
});
|
49
|
+
|
50
|
+
describe("getDkimSigners", () => {
|
51
|
+
test("should get dkim signers from email", async () => {
|
52
|
+
const email = await parseEmail(emailFixture);
|
53
|
+
const dkim = getDkimSigners(email);
|
54
|
+
expect(dkim).toHaveLength(1);
|
55
|
+
expect(dkim[0].domain).toBe("example.com");
|
56
|
+
expect(dkim[0].selector).toBe("selector1");
|
57
|
+
});
|
58
|
+
|
59
|
+
test("should get multiple dkim signers from email", async () => {
|
60
|
+
const dkimHeader2 =
|
61
|
+
"DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=second.signer; h=from:to:subject; s=selector2; b=abcdef;";
|
62
|
+
const email = await parseEmail(
|
63
|
+
`${emailHeaders}${dkimHeader}\n${dkimHeader2}\n\n${body}`,
|
64
|
+
);
|
65
|
+
|
66
|
+
const dkim = getDkimSigners(email);
|
67
|
+
expect(dkim).toHaveLength(2);
|
68
|
+
expect(dkim[0].domain).toBe("example.com");
|
69
|
+
expect(dkim[0].selector).toBe("selector1");
|
70
|
+
expect(dkim[1].domain).toBe("second.signer");
|
71
|
+
expect(dkim[1].selector).toBe("selector2");
|
72
|
+
});
|
73
|
+
|
74
|
+
test("should throw if no dkim header found", async () => {
|
75
|
+
const email = await parseEmail(emailHeaders);
|
76
|
+
expect(() => getDkimSigners(email)).toThrowError("No DKIM header found");
|
77
|
+
});
|
78
|
+
|
79
|
+
test("should throw if dkim header is invalid", async () => {
|
80
|
+
const email = await parseEmail(
|
81
|
+
`${emailHeaders}DKIM-Signature: invalid\n\n${body}`,
|
82
|
+
);
|
83
|
+
expect(() => getDkimSigners(email)).toThrowError(
|
84
|
+
"DKIM header missing domain",
|
85
|
+
);
|
86
|
+
});
|
87
|
+
|
88
|
+
test("should throw if dkim header is missing domain", async () => {
|
89
|
+
const email = await parseEmail(
|
90
|
+
`${emailHeaders}DKIM-Signature: v=1; s=selector\n\n${body}`,
|
91
|
+
);
|
92
|
+
expect(() => getDkimSigners(email)).toThrowError(
|
93
|
+
"DKIM header missing domain",
|
94
|
+
);
|
95
|
+
});
|
96
|
+
|
97
|
+
test("should throw if dkim header is missing selector", async () => {
|
98
|
+
const email = await parseEmail(
|
99
|
+
`${emailHeaders}DKIM-Signature: v=1; d=example.com\n\n${body}`,
|
100
|
+
);
|
101
|
+
expect(() => getDkimSigners(email)).toThrowError(
|
102
|
+
"DKIM header missing selector",
|
103
|
+
);
|
104
|
+
});
|
105
|
+
});
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { simpleParser, HeaderValue, type ParsedMail } from "mailparser";
|
2
|
+
|
3
|
+
export class DkimParsingError extends Error {
|
4
|
+
constructor(message: string) {
|
5
|
+
super(message);
|
6
|
+
this.name = "DkimParsingError";
|
7
|
+
}
|
8
|
+
}
|
9
|
+
|
10
|
+
export async function parseEmail(mime: string) {
|
11
|
+
return simpleParser(mime.trim(), {
|
12
|
+
skipHtmlToText: true,
|
13
|
+
skipTextToHtml: true,
|
14
|
+
skipTextLinks: true,
|
15
|
+
skipImageLinks: true,
|
16
|
+
});
|
17
|
+
}
|
18
|
+
|
19
|
+
export function getDkimSigners(mail: ParsedMail) {
|
20
|
+
const dkimHeader = mail.headers.get("dkim-signature");
|
21
|
+
if (!dkimHeader) throw new DkimParsingError("No DKIM header found");
|
22
|
+
if (Array.isArray(dkimHeader)) {
|
23
|
+
return dkimHeader.map(parseHeader);
|
24
|
+
}
|
25
|
+
return [parseHeader(dkimHeader)];
|
26
|
+
}
|
27
|
+
|
28
|
+
function parseHeader(header: HeaderValue) {
|
29
|
+
if (typeof header === "string" || !("params" in header)) {
|
30
|
+
throw new DkimParsingError(`Invalid DKIM header ${header}`);
|
31
|
+
}
|
32
|
+
|
33
|
+
if (!header.params.d) {
|
34
|
+
throw new DkimParsingError("DKIM header missing domain");
|
35
|
+
}
|
36
|
+
|
37
|
+
if (!header.params.s) {
|
38
|
+
throw new DkimParsingError("DKIM header missing selector");
|
39
|
+
}
|
40
|
+
|
41
|
+
return {
|
42
|
+
domain: header.params.d,
|
43
|
+
selector: header.params.s,
|
44
|
+
};
|
45
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
2
|
+
import { preverifyEmail } from "./preverify.ts";
|
3
|
+
import { readFile } from "testHelpers/readFile";
|
4
|
+
|
5
|
+
describe("Preverify email: integration", () => {
|
6
|
+
test("adds dns record to email mime", async () => {
|
7
|
+
const rawEmail = readFile("./src/api/email/testdata/test_email.txt");
|
8
|
+
const preverifiedEmail = await preverifyEmail(rawEmail);
|
9
|
+
expect(preverifiedEmail).toMatchObject({
|
10
|
+
email: rawEmail,
|
11
|
+
dnsRecords: [expect.stringContaining("v=DKIM1; k=rsa; p=")],
|
12
|
+
});
|
13
|
+
});
|
14
|
+
|
15
|
+
test("throws error if DKIM not found", async () => {
|
16
|
+
const emailWithNoDkimHeader = 'From: "Alice"\n\nBody';
|
17
|
+
await expect(preverifyEmail(emailWithNoDkimHeader)).rejects.toThrow(
|
18
|
+
"No DKIM header found",
|
19
|
+
);
|
20
|
+
});
|
21
|
+
|
22
|
+
test("throws error if DNS could not be resolved", async () => {
|
23
|
+
const emailWithNoDkimHeader = readFile(
|
24
|
+
"./src/api/email/testdata/test_email_unknown_domain.txt",
|
25
|
+
);
|
26
|
+
await expect(preverifyEmail(emailWithNoDkimHeader)).rejects.toThrow();
|
27
|
+
});
|
28
|
+
|
29
|
+
test("throws error if multiple DNS records found", async () => {
|
30
|
+
const emailWithNoDkimHeader = readFile(
|
31
|
+
"./src/api/email/testdata/test_email_multiple_dkims.txt",
|
32
|
+
);
|
33
|
+
await expect(preverifyEmail(emailWithNoDkimHeader)).rejects.toThrow(
|
34
|
+
"Multiple DKIM headers found",
|
35
|
+
);
|
36
|
+
});
|
37
|
+
});
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { parseEmail, getDkimSigners } from "./parseEmail.ts";
|
2
|
+
import { resolveDkimDns } from "./dnsResolver.ts";
|
3
|
+
|
4
|
+
export async function preverifyEmail(mimeEmail: string) {
|
5
|
+
const parsedEmail = await parseEmail(mimeEmail);
|
6
|
+
const signers = getDkimSigners(parsedEmail);
|
7
|
+
if (signers.length === 0) {
|
8
|
+
throw new Error("No DKIM header found");
|
9
|
+
}
|
10
|
+
if (signers.length > 1) {
|
11
|
+
throw new Error("Multiple DKIM headers found");
|
12
|
+
}
|
13
|
+
const [{ domain, selector }] = signers;
|
14
|
+
const dnsRecord = await resolveDkimDns(domain, selector);
|
15
|
+
return {
|
16
|
+
email: mimeEmail,
|
17
|
+
dnsRecords: [dnsRecord],
|
18
|
+
};
|
19
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
2
|
+
c=simple/simple; d=google.com;
|
3
|
+
h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
|
4
|
+
s=20230601; t=1615825284; v=1;
|
5
|
+
b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
|
6
|
+
k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
|
7
|
+
s4wwFRRKz/1bksZGSjD8uuSU=
|
8
|
+
Received: from client1.football.example.com [192.0.2.1]
|
9
|
+
by submitserver.example.com with SUBMISSION;
|
10
|
+
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
|
11
|
+
From: Joe SixPack <joe@football.example.com>
|
12
|
+
To: Suzie Q <suzie@shopping.example.net>
|
13
|
+
Subject: Is dinner ready?
|
14
|
+
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
|
15
|
+
Message-ID: <20030712040037.46341.5F8J@football.example.com>
|
16
|
+
|
17
|
+
Hi.
|
18
|
+
|
19
|
+
We lost the game. Are you hungry yet?
|
20
|
+
|
21
|
+
Joe.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
2
|
+
c=simple/simple; d=google.com;
|
3
|
+
h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
|
4
|
+
s=20230601; t=1615825284; v=1;
|
5
|
+
b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
|
6
|
+
k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
|
7
|
+
s4wwFRRKz/1bksZGSjD8uuSU=
|
8
|
+
DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
9
|
+
c=simple/simple; d=google.com;
|
10
|
+
h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
|
11
|
+
s=20230601; t=1615825284; v=1;
|
12
|
+
b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
|
13
|
+
k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
|
14
|
+
s4wwFRRKz/1bksZGSjD8uuSU=
|
15
|
+
Received: from client1.football.example.com [192.0.2.1]
|
16
|
+
by submitserver.example.com with SUBMISSION;
|
17
|
+
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
|
18
|
+
From: Joe SixPack <joe@football.example.com>
|
19
|
+
To: Suzie Q <suzie@shopping.example.net>
|
20
|
+
Subject: Is dinner ready?
|
21
|
+
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
|
22
|
+
Message-ID: <20030712040037.46341.5F8J@football.example.com>
|
23
|
+
|
24
|
+
Hi.
|
25
|
+
|
26
|
+
We lost the game. Are you hungry yet?
|
27
|
+
|
28
|
+
Joe.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
DKIM-Signature: a=rsa-sha256; bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
|
2
|
+
c=simple/simple; d=foobar.xyz;
|
3
|
+
h=Received:From:To:Subject:Date:Message-ID; i=joe@football.example.com;
|
4
|
+
s=wertyu; t=1615825284; v=1;
|
5
|
+
b=Xh4Ujb2wv5x54gXtulCiy4C0e+plRm6pZ4owF+kICpYzs/8WkTVIDBrzhJP0DAYCpnL62T0G
|
6
|
+
k+0OH8pi/yqETVjKtKk+peMnNvKkut0GeWZMTze0bfq3/JUK3Ln3jTzzpXxrgVnvBxeY9EZIL4g
|
7
|
+
s4wwFRRKz/1bksZGSjD8uuSU=
|
8
|
+
Received: from client1.football.example.com [192.0.2.1]
|
9
|
+
by submitserver.example.com with SUBMISSION;
|
10
|
+
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
|
11
|
+
From: Joe SixPack <joe@football.example.com>
|
12
|
+
To: Suzie Q <suzie@shopping.example.net>
|
13
|
+
Subject: Is dinner ready?
|
14
|
+
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
|
15
|
+
Message-ID: <20030712040037.46341.5F8J@football.example.com>
|
16
|
+
|
17
|
+
Hi.
|
18
|
+
|
19
|
+
We lost the game. Are you hungry yet?
|
20
|
+
|
21
|
+
Joe.
|
@@ -0,0 +1,169 @@
|
|
1
|
+
import {
|
2
|
+
type Abi,
|
3
|
+
type Address,
|
4
|
+
type ContractFunctionArgs,
|
5
|
+
type ContractFunctionName,
|
6
|
+
createTestClient,
|
7
|
+
type Hex,
|
8
|
+
http,
|
9
|
+
HttpTransport,
|
10
|
+
publicActions,
|
11
|
+
PublicClient,
|
12
|
+
walletActions,
|
13
|
+
} from "viem";
|
14
|
+
|
15
|
+
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
|
16
|
+
import { foundry } from "viem/chains";
|
17
|
+
|
18
|
+
import type { ContractSpec, ContractArg } from "types/ethereum";
|
19
|
+
|
20
|
+
export const testChainId1 = 100001;
|
21
|
+
export const testChainId2 = 100002;
|
22
|
+
|
23
|
+
const rpcUrls: Map<number, HttpTransport> = new Map([
|
24
|
+
[testChainId1, http()],
|
25
|
+
[testChainId2, http("http://127.0.0.1:8546")],
|
26
|
+
]);
|
27
|
+
|
28
|
+
export const chainIds = [testChainId1, testChainId2];
|
29
|
+
|
30
|
+
export function client(
|
31
|
+
chainId: number = testChainId1,
|
32
|
+
): ReturnType<typeof walletActions> & PublicClient {
|
33
|
+
const transport = rpcUrls.get(chainId);
|
34
|
+
if (transport == undefined) {
|
35
|
+
throw Error(`No url for chainId ${chainId}`);
|
36
|
+
}
|
37
|
+
|
38
|
+
return createTestClient({
|
39
|
+
chain: foundry,
|
40
|
+
mode: "anvil",
|
41
|
+
transport: transport,
|
42
|
+
})
|
43
|
+
.extend(publicActions)
|
44
|
+
.extend(walletActions);
|
45
|
+
}
|
46
|
+
|
47
|
+
export async function deployContract(
|
48
|
+
contractSpec: ContractSpec,
|
49
|
+
args: ContractArg[] = [],
|
50
|
+
chainId: number = testChainId1,
|
51
|
+
): Promise<Address> {
|
52
|
+
const ethClient = client(chainId);
|
53
|
+
|
54
|
+
const [deployer] = await ethClient.getAddresses();
|
55
|
+
|
56
|
+
const txHash = await ethClient.deployContract({
|
57
|
+
abi: contractSpec.abi,
|
58
|
+
bytecode: contractSpec.bytecode.object,
|
59
|
+
account: deployer,
|
60
|
+
args,
|
61
|
+
chain: foundry,
|
62
|
+
});
|
63
|
+
|
64
|
+
const receipt = await ethClient.waitForTransactionReceipt({ hash: txHash });
|
65
|
+
|
66
|
+
if (receipt.status != "success") {
|
67
|
+
throw new Error(
|
68
|
+
`Contract deployment failed with status: ${receipt.status}`,
|
69
|
+
);
|
70
|
+
}
|
71
|
+
|
72
|
+
return receipt.contractAddress as Address;
|
73
|
+
}
|
74
|
+
|
75
|
+
type DeploySpec<T extends Abi> = {
|
76
|
+
abi: T;
|
77
|
+
bytecode: {
|
78
|
+
object: Hex;
|
79
|
+
};
|
80
|
+
};
|
81
|
+
|
82
|
+
type Tail<T> = T extends readonly [unknown, ...infer U] ? U : [];
|
83
|
+
|
84
|
+
export async function deployProverVerifier<P extends Abi, V extends Abi>(
|
85
|
+
proverSpec: DeploySpec<P>,
|
86
|
+
verifierSpec: DeploySpec<V>,
|
87
|
+
args: {
|
88
|
+
prover?: ContractArg[];
|
89
|
+
verifier?: Tail<ContractArg>[];
|
90
|
+
} = {},
|
91
|
+
chainId: number = testChainId1,
|
92
|
+
) {
|
93
|
+
console.log("Deploying prover");
|
94
|
+
const proverAddress = await deployContract(
|
95
|
+
proverSpec,
|
96
|
+
args.prover ?? [],
|
97
|
+
chainId,
|
98
|
+
);
|
99
|
+
console.log(`Prover has been deployed on ${proverAddress} address`);
|
100
|
+
|
101
|
+
console.log("Deploying verifier");
|
102
|
+
const verifierAddress = await deployContract(
|
103
|
+
verifierSpec,
|
104
|
+
[proverAddress, ...(args.verifier ?? [])],
|
105
|
+
chainId,
|
106
|
+
);
|
107
|
+
console.log(`Verifier has been deployed on ${verifierAddress} address`);
|
108
|
+
|
109
|
+
return [proverAddress, verifierAddress];
|
110
|
+
}
|
111
|
+
|
112
|
+
export async function call<
|
113
|
+
T extends Abi,
|
114
|
+
F extends ContractFunctionName<T, "pure" | "view">,
|
115
|
+
>(
|
116
|
+
abi: T,
|
117
|
+
address: Address,
|
118
|
+
functionName: F,
|
119
|
+
args?: ContractFunctionArgs<T, "pure" | "view", F>,
|
120
|
+
chainId: number = testChainId1,
|
121
|
+
) {
|
122
|
+
const ethClient = client(chainId);
|
123
|
+
|
124
|
+
return ethClient.readContract({
|
125
|
+
abi,
|
126
|
+
address,
|
127
|
+
functionName,
|
128
|
+
args,
|
129
|
+
});
|
130
|
+
}
|
131
|
+
|
132
|
+
export async function writeContract<
|
133
|
+
T extends Abi,
|
134
|
+
F extends ContractFunctionName<T, "payable" | "nonpayable">,
|
135
|
+
>(
|
136
|
+
address: Address,
|
137
|
+
abi: T,
|
138
|
+
functionName: F,
|
139
|
+
args: ContractFunctionArgs<T, "payable" | "nonpayable", F>,
|
140
|
+
sender?: Address,
|
141
|
+
chainId: number = testChainId1,
|
142
|
+
) {
|
143
|
+
const ethClient = client(chainId);
|
144
|
+
const selectedSender = sender || (await ethClient.getAddresses())[0];
|
145
|
+
|
146
|
+
const txHash = await ethClient.writeContract({
|
147
|
+
abi,
|
148
|
+
address,
|
149
|
+
functionName,
|
150
|
+
args,
|
151
|
+
account: selectedSender,
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
153
|
+
} as any);
|
154
|
+
// TODO: fix any to viem type
|
155
|
+
|
156
|
+
const txReceipt = await ethClient.waitForTransactionReceipt({ hash: txHash });
|
157
|
+
|
158
|
+
if (txReceipt.status != "success") {
|
159
|
+
throw new Error(`Transaction failed with status: ${txReceipt.status}`);
|
160
|
+
}
|
161
|
+
|
162
|
+
return txReceipt;
|
163
|
+
}
|
164
|
+
|
165
|
+
export const getTestAccount = () => privateKeyToAccount(generatePrivateKey());
|
166
|
+
|
167
|
+
export const getTestAddresses = (
|
168
|
+
chainId: number = testChainId1,
|
169
|
+
): Promise<Address[]> => client(chainId).getAddresses();
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { VlayerClient } from "types/vlayer";
|
2
|
+
import { WebProofProvider } from "types/webProof";
|
3
|
+
|
4
|
+
export const createVlayerClient = ({
|
5
|
+
url,
|
6
|
+
webProofProvider,
|
7
|
+
}: {
|
8
|
+
url: string;
|
9
|
+
webProofProvider: WebProofProvider;
|
10
|
+
}): VlayerClient => {
|
11
|
+
return {
|
12
|
+
prove: async () => {
|
13
|
+
console.log("prove");
|
14
|
+
console.log("url", url);
|
15
|
+
console.log("webProofProvider", webProofProvider);
|
16
|
+
},
|
17
|
+
};
|
18
|
+
};
|
@@ -0,0 +1,43 @@
|
|
1
|
+
import { Abi, Address, Hex } from "viem";
|
2
|
+
import { Branded } from "./utils";
|
3
|
+
export type Bytecode = {
|
4
|
+
object: Hex;
|
5
|
+
};
|
6
|
+
|
7
|
+
export type ContractSpec = {
|
8
|
+
abi: Abi;
|
9
|
+
bytecode: Bytecode;
|
10
|
+
};
|
11
|
+
|
12
|
+
export type ContractArg =
|
13
|
+
| number
|
14
|
+
| string
|
15
|
+
| boolean
|
16
|
+
| bigint
|
17
|
+
| Address
|
18
|
+
| number[]
|
19
|
+
| string[]
|
20
|
+
| boolean[]
|
21
|
+
| bigint[]
|
22
|
+
| Address[];
|
23
|
+
|
24
|
+
export type EthereumAddress = Branded<Hex, "EthereumAddress">;
|
25
|
+
export type EthereumTxHash = Branded<Hex, "EthereumTxHash">;
|
26
|
+
|
27
|
+
export function assertEthereumAddress(
|
28
|
+
hash: string,
|
29
|
+
): asserts hash is EthereumAddress {
|
30
|
+
const regex = /^(0x)?[0-9a-fA-F]{40}$/;
|
31
|
+
if (!regex.test(hash)) {
|
32
|
+
throw new Error(`Invalid ethereum account ${hash}`);
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
export function assertEthereumTxHash(
|
37
|
+
hash: string,
|
38
|
+
): asserts hash is EthereumTxHash {
|
39
|
+
const regex = /^(0x)?[0-9a-fA-F]{64}$/;
|
40
|
+
if (!regex.test(hash)) {
|
41
|
+
throw new Error(`Invalid ethereum tx hash ${hash}`);
|
42
|
+
}
|
43
|
+
}
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import { Address, Hex } from "viem";
|
2
|
+
|
3
|
+
type Calldata = string;
|
4
|
+
|
5
|
+
export type CallParams = {
|
6
|
+
to: Address;
|
7
|
+
data: Calldata;
|
8
|
+
};
|
9
|
+
|
10
|
+
export type CallContext = {
|
11
|
+
chain_id: number; // 31337
|
12
|
+
};
|
13
|
+
|
14
|
+
export interface Proof {
|
15
|
+
length: bigint;
|
16
|
+
seal: {
|
17
|
+
verifierSelector: Hex;
|
18
|
+
seal: [Hex, Hex, Hex, Hex, Hex, Hex, Hex, Hex];
|
19
|
+
mode: number;
|
20
|
+
};
|
21
|
+
dynamicParamsOffsets: [
|
22
|
+
bigint,
|
23
|
+
bigint,
|
24
|
+
bigint,
|
25
|
+
bigint,
|
26
|
+
bigint,
|
27
|
+
bigint,
|
28
|
+
bigint,
|
29
|
+
bigint,
|
30
|
+
bigint,
|
31
|
+
bigint,
|
32
|
+
];
|
33
|
+
commitment: {
|
34
|
+
proverContractAddress: Address;
|
35
|
+
functionSelector: Hex;
|
36
|
+
settleBlockHash: Hex;
|
37
|
+
settleBlockNumber: bigint;
|
38
|
+
};
|
39
|
+
}
|
40
|
+
|
41
|
+
export interface VCallResult {
|
42
|
+
evm_call_result: Hex;
|
43
|
+
proof: Proof;
|
44
|
+
}
|
45
|
+
|
46
|
+
export interface VCallResponse {
|
47
|
+
jsonrpc: string;
|
48
|
+
result: VCallResult;
|
49
|
+
id: number;
|
50
|
+
}
|
51
|
+
|
52
|
+
// Add more methods here
|
53
|
+
export type VlayerClient = {
|
54
|
+
prove: () => void;
|
55
|
+
};
|
@@ -0,0 +1,106 @@
|
|
1
|
+
//NOTE : this is copied from tlsn-js 5.4
|
2
|
+
// for some reason newest vertsions doesnt not export this type (clarification is in progress)
|
3
|
+
// probaly it should be reexported from tlsn-js
|
4
|
+
|
5
|
+
export interface WebProof {
|
6
|
+
session: Session;
|
7
|
+
substrings: Substrings;
|
8
|
+
notaryUrl: string;
|
9
|
+
}
|
10
|
+
|
11
|
+
export interface Session {
|
12
|
+
header: Header;
|
13
|
+
signature: Signature;
|
14
|
+
session_info: SessionInfo;
|
15
|
+
}
|
16
|
+
|
17
|
+
export interface SessionInfo {
|
18
|
+
server_name: ServerName;
|
19
|
+
handshake_decommitment: HandshakeDecommitment;
|
20
|
+
}
|
21
|
+
|
22
|
+
export interface HandshakeDecommitment {
|
23
|
+
nonce: number[];
|
24
|
+
data: Data;
|
25
|
+
}
|
26
|
+
|
27
|
+
export interface Data {
|
28
|
+
server_cert_details: ServerCERTDetails;
|
29
|
+
server_kx_details: ServerKxDetails;
|
30
|
+
client_random: number[];
|
31
|
+
server_random: number[];
|
32
|
+
}
|
33
|
+
|
34
|
+
export interface ServerCERTDetails {
|
35
|
+
cert_chain: Array<number[]>;
|
36
|
+
ocsp_response: number[];
|
37
|
+
scts: null;
|
38
|
+
}
|
39
|
+
|
40
|
+
export interface ServerKxDetails {
|
41
|
+
kx_params: number[];
|
42
|
+
kx_sig: KxSig;
|
43
|
+
}
|
44
|
+
|
45
|
+
export interface KxSig {
|
46
|
+
scheme: string;
|
47
|
+
sig: number[];
|
48
|
+
}
|
49
|
+
|
50
|
+
export interface Header {
|
51
|
+
encoder_seed: number[];
|
52
|
+
merkle_root: number[];
|
53
|
+
sent_len: number;
|
54
|
+
recv_len: number;
|
55
|
+
handshake_summary: HandshakeSummary;
|
56
|
+
}
|
57
|
+
|
58
|
+
export interface HandshakeSummary {
|
59
|
+
time: number;
|
60
|
+
server_public_key: ServerPublicKey;
|
61
|
+
handshake_commitment: number[];
|
62
|
+
}
|
63
|
+
|
64
|
+
export interface ServerPublicKey {
|
65
|
+
group: string;
|
66
|
+
key: number[];
|
67
|
+
}
|
68
|
+
|
69
|
+
export interface ServerName {
|
70
|
+
Dns: string;
|
71
|
+
}
|
72
|
+
|
73
|
+
export interface Signature {
|
74
|
+
P256: string;
|
75
|
+
}
|
76
|
+
|
77
|
+
export interface Substrings {
|
78
|
+
openings: { [key: string]: Opening[] };
|
79
|
+
inclusion_proof: InclusionProof;
|
80
|
+
}
|
81
|
+
|
82
|
+
export interface InclusionProof {
|
83
|
+
proof: unknown[];
|
84
|
+
total_leaves: number;
|
85
|
+
}
|
86
|
+
|
87
|
+
export interface Opening {
|
88
|
+
kind?: string;
|
89
|
+
ranges?: Range[];
|
90
|
+
direction?: string;
|
91
|
+
Blake3?: Blake3;
|
92
|
+
}
|
93
|
+
|
94
|
+
export interface Blake3 {
|
95
|
+
data: number[];
|
96
|
+
nonce: number[];
|
97
|
+
}
|
98
|
+
|
99
|
+
export interface Range {
|
100
|
+
start: number;
|
101
|
+
end: number;
|
102
|
+
}
|
103
|
+
|
104
|
+
export type WebProofProvider = {
|
105
|
+
requestProof: (url: string) => Promise<WebProof>;
|
106
|
+
};
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import {
|
2
|
+
type Abi,
|
3
|
+
AbiFunction,
|
4
|
+
AbiStateMutability,
|
5
|
+
type Address,
|
6
|
+
ContractFunctionArgs,
|
7
|
+
ContractFunctionName,
|
8
|
+
decodeFunctionResult,
|
9
|
+
encodeFunctionData,
|
10
|
+
} from "viem";
|
11
|
+
|
12
|
+
import { type CallContext, type CallParams, Proof } from "types/vlayer";
|
13
|
+
import { v_call } from "./v_call";
|
14
|
+
import { testChainId1 } from "./helpers";
|
15
|
+
import { ContractSpec } from "types/ethereum";
|
16
|
+
|
17
|
+
export async function getContractSpec(file: string): Promise<ContractSpec> {
|
18
|
+
return Bun.file(file).json();
|
19
|
+
}
|
20
|
+
|
21
|
+
export async function prove<
|
22
|
+
T extends readonly [AbiFunction, ...Abi[number][]],
|
23
|
+
F extends ContractFunctionName<T>,
|
24
|
+
>(
|
25
|
+
prover: Address,
|
26
|
+
abi: T,
|
27
|
+
functionName: F,
|
28
|
+
args: ContractFunctionArgs<T, AbiStateMutability, F>,
|
29
|
+
chainId = testChainId1,
|
30
|
+
) {
|
31
|
+
const calldata = encodeFunctionData({
|
32
|
+
abi: abi as Abi,
|
33
|
+
functionName: functionName as string,
|
34
|
+
args: args as readonly unknown[],
|
35
|
+
});
|
36
|
+
|
37
|
+
const call: CallParams = { to: prover, data: calldata };
|
38
|
+
const context: CallContext = {
|
39
|
+
chain_id: chainId,
|
40
|
+
};
|
41
|
+
|
42
|
+
const {
|
43
|
+
result: { proof, evm_call_result },
|
44
|
+
} = await v_call(call, context);
|
45
|
+
|
46
|
+
const returnValue = decodeFunctionResult({
|
47
|
+
abi: abi as Abi,
|
48
|
+
data: evm_call_result,
|
49
|
+
functionName: functionName as string,
|
50
|
+
});
|
51
|
+
|
52
|
+
addDynamicParamsOffsets(abi, functionName, proof);
|
53
|
+
|
54
|
+
return { proof, returnValue: returnValue as `0x${string}`[] };
|
55
|
+
}
|
56
|
+
|
57
|
+
function addDynamicParamsOffsets(
|
58
|
+
abi: Abi,
|
59
|
+
functionName: string | undefined,
|
60
|
+
proof: Proof,
|
61
|
+
) {
|
62
|
+
const proverFunction = abi.find(
|
63
|
+
(f) => f.type === "function" && f.name === functionName,
|
64
|
+
) as AbiFunction;
|
65
|
+
|
66
|
+
if (proverFunction?.outputs && proverFunction.outputs.length > 0) {
|
67
|
+
const secondVerifyMethodParamType = proverFunction.outputs[0].type;
|
68
|
+
|
69
|
+
if (secondVerifyMethodParamType === "string") {
|
70
|
+
proof.dynamicParamsOffsets[0] = BigInt(32);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { CallContext, CallParams, VCallResponse } from "types/vlayer";
|
2
|
+
|
3
|
+
function v_callBody(call: CallParams, context: CallContext) {
|
4
|
+
return {
|
5
|
+
method: "v_call",
|
6
|
+
params: [call, context],
|
7
|
+
id: 1,
|
8
|
+
jsonrpc: "2.0",
|
9
|
+
};
|
10
|
+
}
|
11
|
+
|
12
|
+
export async function v_call(
|
13
|
+
call: CallParams,
|
14
|
+
context: CallContext,
|
15
|
+
): Promise<VCallResponse> {
|
16
|
+
const response = await fetch("http://127.0.0.1:3000", {
|
17
|
+
method: "POST",
|
18
|
+
body: JSON.stringify(v_callBody(call, context)),
|
19
|
+
headers: { "Content-Type": "application/json" },
|
20
|
+
});
|
21
|
+
|
22
|
+
if (!response.ok) {
|
23
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
24
|
+
}
|
25
|
+
|
26
|
+
const response_json = await response.json();
|
27
|
+
|
28
|
+
//TODO we should launch some schema validation here
|
29
|
+
assertObject(response_json);
|
30
|
+
|
31
|
+
if ("error" in response_json) {
|
32
|
+
throw new Error(
|
33
|
+
`Error response: ${(response_json.error as { message: string }).message || "unknown error"}`,
|
34
|
+
);
|
35
|
+
}
|
36
|
+
|
37
|
+
return response_json as Promise<VCallResponse>;
|
38
|
+
}
|
39
|
+
|
40
|
+
function assertObject(x: unknown): asserts x is object {
|
41
|
+
if (typeof x !== "object") {
|
42
|
+
throw new Error("Expected object");
|
43
|
+
}
|
44
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
export { v_call } from "./api/v_call";
|
2
|
+
export type { CallParams, CallContext } from "types/vlayer";
|
3
|
+
export type { ContractSpec } from "types/ethereum";
|
4
|
+
|
5
|
+
export { getContractSpec, prove } from "./api/prover";
|
6
|
+
export * as testHelpers from "./api/helpers";
|
7
|
+
export { client as createTestClient } from "./api/helpers";
|
8
|
+
export { preverifyEmail } from "./api/email/preverify.ts";
|
package/tsconfig.json
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"lib": [
|
4
|
+
"ESNext"
|
5
|
+
],
|
6
|
+
"module": "esnext",
|
7
|
+
"target": "esnext",
|
8
|
+
"moduleResolution": "bundler",
|
9
|
+
"moduleDetection": "force",
|
10
|
+
"allowImportingTsExtensions": true,
|
11
|
+
"strict": true,
|
12
|
+
"downlevelIteration": true,
|
13
|
+
"skipLibCheck": true,
|
14
|
+
"jsx": "preserve",
|
15
|
+
"allowSyntheticDefaultImports": true,
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
17
|
+
"allowJs": true,
|
18
|
+
"noEmit": true,
|
19
|
+
"composite": true,
|
20
|
+
"types": [
|
21
|
+
"bun" // add Bun global
|
22
|
+
],
|
23
|
+
"baseUrl": "./",
|
24
|
+
"paths": {
|
25
|
+
"types/*": ["./src/api/lib/types/*"],
|
26
|
+
"testHelpers/*": ["./src/testHelpers/*"],
|
27
|
+
},
|
28
|
+
|
29
|
+
}
|
30
|
+
}
|