@vlayer/sdk 0.0.1
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/.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
|
+
}
|