@lemmaoracle/x402 0.1.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/broadcast/DeployPaymentVerifier.s.sol/10143/run-1775996174982.json +34 -0
- package/broadcast/DeployPaymentVerifier.s.sol/10143/run-1775996423983.json +51 -0
- package/broadcast/DeployPaymentVerifier.s.sol/10143/run-1775996907850.json +51 -0
- package/broadcast/DeployPaymentVerifier.s.sol/10143/run-latest.json +51 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-1775996174982.json +7 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-1775996423983.json +7 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-1775996907850.json +7 -0
- package/cache/DeployPaymentVerifier.s.sol/10143/run-latest.json +7 -0
- package/cache/solidity-files-cache.json +1 -0
- package/circuits/README.md +78 -0
- package/circuits/payment-comprehensive.test.mjs +272 -0
- package/circuits/payment.circom +89 -0
- package/circuits/payment.test.mjs +173 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/proof-generator.d.ts +26 -0
- package/dist/proof-generator.d.ts.map +1 -0
- package/dist/proof-generator.js +154 -0
- package/dist/proof-generator.js.map +1 -0
- package/dist/proof-generator.test.d.ts +7 -0
- package/dist/proof-generator.test.d.ts.map +1 -0
- package/dist/proof-generator.test.js +121 -0
- package/dist/proof-generator.test.js.map +1 -0
- package/dist/transaction-watcher.d.ts +42 -0
- package/dist/transaction-watcher.d.ts.map +1 -0
- package/dist/transaction-watcher.js +88 -0
- package/dist/transaction-watcher.js.map +1 -0
- package/dist/transaction-watcher.test.d.ts +7 -0
- package/dist/transaction-watcher.test.d.ts.map +1 -0
- package/dist/transaction-watcher.test.js +111 -0
- package/dist/transaction-watcher.test.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/foundry.lock +11 -0
- package/foundry.toml +16 -0
- package/lib/forge-std/.gitattributes +1 -0
- package/lib/forge-std/.github/CODEOWNERS +1 -0
- package/lib/forge-std/.github/dependabot.yml +6 -0
- package/lib/forge-std/.github/workflows/ci.yml +161 -0
- package/lib/forge-std/.github/workflows/sync.yml +36 -0
- package/lib/forge-std/CONTRIBUTING.md +193 -0
- package/lib/forge-std/LICENSE-APACHE +203 -0
- package/lib/forge-std/LICENSE-MIT +25 -0
- package/lib/forge-std/README.md +314 -0
- package/lib/forge-std/RELEASE_CHECKLIST.md +12 -0
- package/lib/forge-std/foundry.toml +18 -0
- package/lib/forge-std/package.json +16 -0
- package/lib/forge-std/scripts/vm.py +636 -0
- package/lib/forge-std/src/Base.sol +48 -0
- package/lib/forge-std/src/Config.sol +60 -0
- package/lib/forge-std/src/LibVariable.sol +477 -0
- package/lib/forge-std/src/Script.sol +28 -0
- package/lib/forge-std/src/StdAssertions.sol +1300 -0
- package/lib/forge-std/src/StdChains.sol +303 -0
- package/lib/forge-std/src/StdCheats.sol +825 -0
- package/lib/forge-std/src/StdConfig.sol +632 -0
- package/lib/forge-std/src/StdConstants.sol +30 -0
- package/lib/forge-std/src/StdError.sol +15 -0
- package/lib/forge-std/src/StdInvariant.sol +161 -0
- package/lib/forge-std/src/StdJson.sol +275 -0
- package/lib/forge-std/src/StdMath.sol +47 -0
- package/lib/forge-std/src/StdStorage.sol +476 -0
- package/lib/forge-std/src/StdStyle.sol +333 -0
- package/lib/forge-std/src/StdToml.sol +275 -0
- package/lib/forge-std/src/StdUtils.sol +200 -0
- package/lib/forge-std/src/Test.sol +32 -0
- package/lib/forge-std/src/Vm.sol +2533 -0
- package/lib/forge-std/src/console.sol +1551 -0
- package/lib/forge-std/src/console2.sol +4 -0
- package/lib/forge-std/src/interfaces/IERC1155.sol +105 -0
- package/lib/forge-std/src/interfaces/IERC165.sol +12 -0
- package/lib/forge-std/src/interfaces/IERC20.sol +43 -0
- package/lib/forge-std/src/interfaces/IERC4626.sol +190 -0
- package/lib/forge-std/src/interfaces/IERC6909.sol +72 -0
- package/lib/forge-std/src/interfaces/IERC721.sol +164 -0
- package/lib/forge-std/src/interfaces/IERC7540.sol +145 -0
- package/lib/forge-std/src/interfaces/IERC7575.sol +241 -0
- package/lib/forge-std/src/interfaces/IMulticall3.sol +68 -0
- package/lib/forge-std/src/safeconsole.sol +13248 -0
- package/lib/forge-std/test/CommonBase.t.sol +44 -0
- package/lib/forge-std/test/Config.t.sol +381 -0
- package/lib/forge-std/test/LibVariable.t.sol +452 -0
- package/lib/forge-std/test/StdAssertions.t.sol +141 -0
- package/lib/forge-std/test/StdChains.t.sol +227 -0
- package/lib/forge-std/test/StdCheats.t.sol +638 -0
- package/lib/forge-std/test/StdConstants.t.sol +38 -0
- package/lib/forge-std/test/StdError.t.sol +119 -0
- package/lib/forge-std/test/StdJson.t.sol +49 -0
- package/lib/forge-std/test/StdMath.t.sol +202 -0
- package/lib/forge-std/test/StdStorage.t.sol +532 -0
- package/lib/forge-std/test/StdStyle.t.sol +110 -0
- package/lib/forge-std/test/StdToml.t.sol +49 -0
- package/lib/forge-std/test/StdUtils.t.sol +342 -0
- package/lib/forge-std/test/Vm.t.sol +18 -0
- package/lib/forge-std/test/compilation/CompilationScript.sol +8 -0
- package/lib/forge-std/test/compilation/CompilationScriptBase.sol +8 -0
- package/lib/forge-std/test/compilation/CompilationTest.sol +8 -0
- package/lib/forge-std/test/compilation/CompilationTestBase.sol +8 -0
- package/lib/forge-std/test/fixtures/broadcast.log.json +187 -0
- package/lib/forge-std/test/fixtures/config.toml +81 -0
- package/lib/forge-std/test/fixtures/test.json +8 -0
- package/lib/forge-std/test/fixtures/test.toml +6 -0
- package/package.json +55 -0
- package/package.json.backup +55 -0
- package/scripts/DeployPaymentVerifier.s.sol +18 -0
- package/scripts/build.sh +61 -0
- package/scripts/deploy.sh +65 -0
- package/scripts/fix-verifier.mjs +64 -0
- package/scripts/register-circuit.ts +213 -0
- package/scripts/test-with-circuit.mjs +88 -0
- package/src/index.ts +115 -0
- package/src/proof-generator.test.ts +139 -0
- package/src/proof-generator.ts +220 -0
- package/src/transaction-watcher.test.ts +153 -0
- package/src/transaction-watcher.ts +154 -0
- package/src/types.ts +110 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +18 -0
- package/workers/x402-facilitator/src/index.ts +582 -0
- package/workers/x402-facilitator/src/x402-env.d.ts +25 -0
- package/workers/x402-facilitator/wrangler.toml +19 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Combined test script that runs both TypeScript tests and circuit tests
|
|
4
|
+
* Usage: node scripts/test-with-circuit.mjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, resolve } from 'path';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT_DIR = resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
// Check if build files exist
|
|
16
|
+
function checkBuildFiles() {
|
|
17
|
+
const requiredFiles = [
|
|
18
|
+
'build/payment_js/payment.wasm',
|
|
19
|
+
'build/payment_final.zkey',
|
|
20
|
+
'build/verification_key.json'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const missingFiles = requiredFiles.filter(file => !existsSync(resolve(ROOT_DIR, file)));
|
|
24
|
+
return missingFiles;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Helper function to run commands
|
|
28
|
+
function runCommand(command, args = [], cwd = ROOT_DIR) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
console.log(`\n>>> Running: ${command} ${args.join(' ')}`);
|
|
31
|
+
|
|
32
|
+
const child = spawn(command, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
shell: true
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.on('close', (code) => {
|
|
39
|
+
if (code === 0) {
|
|
40
|
+
resolve();
|
|
41
|
+
} else {
|
|
42
|
+
reject(new Error(`Command failed with exit code ${code}`));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
child.on('error', (error) => {
|
|
47
|
+
reject(error);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
console.log('=== Running X402 Package Tests ===\n');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// 1. Run TypeScript tests (direct vitest execution)
|
|
57
|
+
console.log('1. Running TypeScript tests with Vitest...');
|
|
58
|
+
await runCommand('npx', ['vitest', 'run']);
|
|
59
|
+
|
|
60
|
+
// 2. Check circuit build files
|
|
61
|
+
console.log('\n2. Checking circuit build files...');
|
|
62
|
+
const missingFiles = checkBuildFiles();
|
|
63
|
+
|
|
64
|
+
if (missingFiles.length > 0) {
|
|
65
|
+
console.log('⚠️ Circuit build files missing. Building circuit...');
|
|
66
|
+
await runCommand('npm', ['run', 'build:circuit']);
|
|
67
|
+
} else {
|
|
68
|
+
console.log('✅ Circuit build files are present.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Run circuit tests
|
|
72
|
+
console.log('\n3. Running circuit tests...');
|
|
73
|
+
await runCommand('node', ['circuits/payment.test.mjs']);
|
|
74
|
+
|
|
75
|
+
console.log('\n✅ All tests passed!');
|
|
76
|
+
process.exit(0);
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('\n❌ Test failed:', error.message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Execute main function
|
|
85
|
+
main().catch(error => {
|
|
86
|
+
console.error('Unhandled error:', error);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { create, type LemmaClient } from "@lemmaoracle/sdk";
|
|
2
|
+
import * as R from "ramda";
|
|
3
|
+
import type { Config, Disclosure, Proof, Transaction, ProofResult } from "./types.js";
|
|
4
|
+
import { createProofGenerator } from "./proof-generator.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a Lemma API client from config.
|
|
8
|
+
*/
|
|
9
|
+
const createLemmaClient = (config: Config): LemmaClient =>
|
|
10
|
+
create({
|
|
11
|
+
apiBase: config.lemmaApiBase,
|
|
12
|
+
apiKey: config.lemmaApiKey,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a disclosure object from a proof.
|
|
17
|
+
*/
|
|
18
|
+
const disclosure = (proof: Proof): Disclosure =>
|
|
19
|
+
({
|
|
20
|
+
condition: { circuitId: proof.circuitId },
|
|
21
|
+
proof: proof.proof,
|
|
22
|
+
inputs: proof.inputs,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build disclosure with empty proof for initial query (will be filled by facilitator).
|
|
27
|
+
*/
|
|
28
|
+
const emptyDisclosure = (circuitId: string): Disclosure =>
|
|
29
|
+
({
|
|
30
|
+
condition: { circuitId },
|
|
31
|
+
proof: "",
|
|
32
|
+
inputs: [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a configured facilitator client with proof generation capabilities.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* const facilitator = facilitator(config);
|
|
40
|
+
*
|
|
41
|
+
* // Generate disclosure with payment proof
|
|
42
|
+
* const disclosure = await facilitator.generateDisclosure(txHash);
|
|
43
|
+
*
|
|
44
|
+
* // Query Lemma API
|
|
45
|
+
* const results = await lemmaApi.query({ disclosure });
|
|
46
|
+
*/
|
|
47
|
+
const facilitator = (config: Config) => {
|
|
48
|
+
const lemmaClient = createLemmaClient(config);
|
|
49
|
+
const proofGenerator = createProofGenerator(config, lemmaClient);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
/**
|
|
53
|
+
* Generate a disclosure with payment proof for a transaction.
|
|
54
|
+
*/
|
|
55
|
+
generateDisclosure: async (txHash: `0x${string}`): Promise<Disclosure> => {
|
|
56
|
+
const { proof } = await proofGenerator.generateProof(txHash);
|
|
57
|
+
|
|
58
|
+
return disclosure(proof);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get an empty disclosure template with condition for the configured circuit.
|
|
63
|
+
*/
|
|
64
|
+
getEmptyDisclosure: (_?: undefined): Disclosure =>
|
|
65
|
+
emptyDisclosure(config.circuitId),
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the configuration.
|
|
69
|
+
*/
|
|
70
|
+
getConfig: (_?: undefined): Config => config,
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the Lemma client.
|
|
74
|
+
*/
|
|
75
|
+
getLemmaClient: (_?: undefined): LemmaClient => lemmaClient,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Default x402 facilitator configuration for Base Sepolia.
|
|
81
|
+
*/
|
|
82
|
+
const defaultConfig = (overrides: Partial<Config> = {}): Config =>
|
|
83
|
+
R.mergeRight(
|
|
84
|
+
{
|
|
85
|
+
payToAddress: "0x" as `0x${string}`,
|
|
86
|
+
network: "eip155:84532",
|
|
87
|
+
price: "$0.001",
|
|
88
|
+
ethereumRpcUrl: "https://sepolia.base.org",
|
|
89
|
+
chainId: 84532,
|
|
90
|
+
lemmaApiBase: "https://workers.lemma.workers.dev",
|
|
91
|
+
circuitId: "x402-payment-v1",
|
|
92
|
+
minAmount: 1000n, // $0.001 USDC in smallest units (6 decimals)
|
|
93
|
+
requiredConfirmations: 6,
|
|
94
|
+
maxProofAge: 3600, // 1 hour
|
|
95
|
+
ipfsGateway: "https://ipfs.io/ipfs/",
|
|
96
|
+
},
|
|
97
|
+
overrides,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export {
|
|
101
|
+
facilitator,
|
|
102
|
+
createProofGenerator,
|
|
103
|
+
disclosure,
|
|
104
|
+
emptyDisclosure,
|
|
105
|
+
defaultConfig,
|
|
106
|
+
// createLemmaClient is not exported
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type {
|
|
110
|
+
Config,
|
|
111
|
+
Disclosure,
|
|
112
|
+
Proof,
|
|
113
|
+
Transaction,
|
|
114
|
+
ProofResult,
|
|
115
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for x402 payment proof generator.
|
|
3
|
+
*
|
|
4
|
+
* Test files are exempt from FP rules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
import { createProofGenerator } from "./proof-generator.js";
|
|
9
|
+
import type { Config } from "./types.js";
|
|
10
|
+
|
|
11
|
+
// Mock viem
|
|
12
|
+
vi.mock("viem", async () => {
|
|
13
|
+
const actual = await vi.importActual("viem");
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
createPublicClient: vi.fn(() => ({
|
|
17
|
+
getTransaction: vi.fn().mockResolvedValue({
|
|
18
|
+
hash: "0xabc123",
|
|
19
|
+
from: "0xsender",
|
|
20
|
+
to: "0x2222222222222222222222222222222222222222",
|
|
21
|
+
value: 1000n,
|
|
22
|
+
}),
|
|
23
|
+
getTransactionReceipt: vi.fn().mockResolvedValue({
|
|
24
|
+
status: "success",
|
|
25
|
+
blockNumber: 1000n,
|
|
26
|
+
gasUsed: 21000n,
|
|
27
|
+
effectiveGasPrice: 1000000000n,
|
|
28
|
+
confirmations: 6,
|
|
29
|
+
}),
|
|
30
|
+
getBlock: vi.fn().mockResolvedValue({
|
|
31
|
+
number: 1006n,
|
|
32
|
+
timestamp: 1712000000n,
|
|
33
|
+
}),
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Mock Lemma SDK
|
|
39
|
+
vi.mock("@lemmaoracle/sdk", async () => {
|
|
40
|
+
const actual = await vi.importActual("@lemmaoracle/sdk");
|
|
41
|
+
return {
|
|
42
|
+
...actual,
|
|
43
|
+
prover: {
|
|
44
|
+
prove: vi.fn().mockResolvedValue({
|
|
45
|
+
proof: "mock-proof-base64",
|
|
46
|
+
publicInputs: ["input1", "input2"],
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
createClient: vi.fn().mockReturnValue({
|
|
50
|
+
apiBase: "https://workers.lemma.workers.dev",
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const mockConfig: Config = {
|
|
56
|
+
payToAddress: "0x2222222222222222222222222222222222222222" as `0x${string}`,
|
|
57
|
+
network: "eip155:84532",
|
|
58
|
+
price: "$0.001",
|
|
59
|
+
ethereumRpcUrl: "https://sepolia.base.org",
|
|
60
|
+
chainId: 84532,
|
|
61
|
+
lemmaApiBase: "https://workers.lemma.workers.dev",
|
|
62
|
+
circuitId: "x402-payment-v1",
|
|
63
|
+
minAmount: 1000n,
|
|
64
|
+
requiredConfirmations: 6,
|
|
65
|
+
maxProofAge: 3600,
|
|
66
|
+
ipfsGateway: "https://ipfs.io/ipfs/",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const mockLemmaClient = {
|
|
70
|
+
apiBase: "https://workers.lemma.workers.dev",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
describe("X402 Proof Generator", () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.clearAllMocks();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should create a proof generator", () => {
|
|
79
|
+
const generator = createProofGenerator(mockConfig, mockLemmaClient as any);
|
|
80
|
+
|
|
81
|
+
expect(generator).toBeDefined();
|
|
82
|
+
expect(typeof generator.generateProof).toBe("function");
|
|
83
|
+
expect(typeof generator.toFieldElements).toBe("function");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should convert payment details to field elements", async () => {
|
|
87
|
+
const generator = createProofGenerator(mockConfig, mockLemmaClient as any);
|
|
88
|
+
|
|
89
|
+
const mockPayment = {
|
|
90
|
+
txHash: "0xabc123" as `0x${string}`,
|
|
91
|
+
from: "0x1111111111111111111111111111111111111111" as `0x${string}`,
|
|
92
|
+
to: "0x2222222222222222222222222222222222222222" as `0x${string}`,
|
|
93
|
+
amount: 1000n,
|
|
94
|
+
timestamp: 1712000000,
|
|
95
|
+
blockNumber: 1000n,
|
|
96
|
+
confirmations: 6,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = await generator.toFieldElements(mockPayment, mockConfig);
|
|
100
|
+
|
|
101
|
+
expect(result).toBeDefined();
|
|
102
|
+
expect(result.witness).toBeDefined();
|
|
103
|
+
expect(result.publicInputs).toBeDefined();
|
|
104
|
+
expect(result.witness.amount).toBeDefined();
|
|
105
|
+
expect(result.witness.minAmount).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle empty txHash gracefully", async () => {
|
|
109
|
+
const generator = createProofGenerator(mockConfig, mockLemmaClient as any);
|
|
110
|
+
|
|
111
|
+
expect(generator.generateProof).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Field Element Conversion", () => {
|
|
116
|
+
it("should convert addresses to field elements", async () => {
|
|
117
|
+
const generator = createProofGenerator(mockConfig, mockLemmaClient as any);
|
|
118
|
+
|
|
119
|
+
const mockPayment = {
|
|
120
|
+
txHash: "0xabc123" as `0x${string}`,
|
|
121
|
+
from: "0x1111111111111111111111111111111111111111" as `0x${string}`,
|
|
122
|
+
to: "0x2222222222222222222222222222222222222222" as `0x${string}`,
|
|
123
|
+
amount: 1000n,
|
|
124
|
+
timestamp: 1712000000,
|
|
125
|
+
blockNumber: 1000n,
|
|
126
|
+
confirmations: 6,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const { witness } = await generator.toFieldElements(mockPayment, mockConfig);
|
|
130
|
+
|
|
131
|
+
// Verify field elements are present and not null
|
|
132
|
+
expect(witness.txHashPacked_0).toBeDefined();
|
|
133
|
+
expect(witness.txHashPacked_1).toBeDefined();
|
|
134
|
+
expect(witness.recipientLow).toBeDefined();
|
|
135
|
+
expect(witness.amount).toBeDefined();
|
|
136
|
+
expect(witness.timestamp).toBeDefined();
|
|
137
|
+
expect(witness.minAmount).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X402 Payment Proof Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates ZK proofs for x402 payment transactions so that
|
|
5
|
+
* Lemma's disclosure.condition API can verify payments without
|
|
6
|
+
* exposing transaction details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createPublicClient, http, type PublicClient } from "viem";
|
|
10
|
+
import { prover, type LemmaClient } from "@lemmaoracle/sdk";
|
|
11
|
+
import * as R from "ramda";
|
|
12
|
+
import type {
|
|
13
|
+
ProofResult,
|
|
14
|
+
Config,
|
|
15
|
+
Transaction,
|
|
16
|
+
Proof,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Default IPFS gateway if not specified in config */
|
|
24
|
+
const _DEFAULT_IPFS_GATEWAY = "https://ipfs.io/ipfs/";
|
|
25
|
+
|
|
26
|
+
/** Poseidon hash function for field element hashing */
|
|
27
|
+
const toFieldElement = (value: string): string =>
|
|
28
|
+
BigInt(value) < 0n
|
|
29
|
+
? String(BigInt(value) % 21888242871839275222246405745257275088548364400416034343698204186575808495617n)
|
|
30
|
+
: value;
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Proof Generator Class
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates x402 payment proofs using Lemma's prover infrastructure.
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* const generator = new X402ProofGenerator(config, lemmaClient);
|
|
41
|
+
* const result = await generator.generateProof(txHash);
|
|
42
|
+
*/
|
|
43
|
+
const createProofGenerator = (
|
|
44
|
+
config: Config,
|
|
45
|
+
lemmaClient: LemmaClient,
|
|
46
|
+
): Readonly<{
|
|
47
|
+
generateProof: (txHash: `0x${string}`) => Promise<ProofResult>;
|
|
48
|
+
toFieldElements: (
|
|
49
|
+
payment: Transaction,
|
|
50
|
+
config: Config,
|
|
51
|
+
) => Readonly<{ witness: Record<string, string>; publicInputs: string[] }>;
|
|
52
|
+
}> => {
|
|
53
|
+
/** Create public client for transaction inspection */
|
|
54
|
+
const publicClient: PublicClient = createPublicClient({
|
|
55
|
+
transport: http(config.ethereumRpcUrl),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* Generate a payment proof for a given transaction hash.
|
|
61
|
+
* This fetches the transaction details, computes the witness,
|
|
62
|
+
* and generates the ZK proof.
|
|
63
|
+
*/
|
|
64
|
+
generateProof: async (txHash: `0x${string}`): Promise<ProofResult> => {
|
|
65
|
+
// Step 1: Fetch transaction details from blockchain
|
|
66
|
+
const payment = await fetchPaymentTransaction(txHash, publicClient);
|
|
67
|
+
|
|
68
|
+
// Step 2: Generate proof
|
|
69
|
+
const proofResult = await generateZKProof(
|
|
70
|
+
lemmaClient,
|
|
71
|
+
payment,
|
|
72
|
+
config,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return R.always({
|
|
76
|
+
payment,
|
|
77
|
+
proof: proofResult,
|
|
78
|
+
})();
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Convert payment transaction details to circuit field elements.
|
|
83
|
+
*/
|
|
84
|
+
toFieldElements: (
|
|
85
|
+
payment: Transaction,
|
|
86
|
+
cfg: Config,
|
|
87
|
+
) => {
|
|
88
|
+
// Split 256-bit txHash into two 128-bit elements
|
|
89
|
+
const txHashBigInt = BigInt(payment.txHash);
|
|
90
|
+
const upper128 = toFieldElement(String(txHashBigInt >> 128n));
|
|
91
|
+
const lower128 = toFieldElement(String(txHashBigInt & ((1n << 128n) - 1n)));
|
|
92
|
+
|
|
93
|
+
// Split 160-bit recipient address
|
|
94
|
+
const recipientBigInt = BigInt(payment.to);
|
|
95
|
+
const recipientLow = toFieldElement(String(recipientBigInt & ((1n << 128n) - 1n)));
|
|
96
|
+
const recipientHigh = toFieldElement(String(recipientBigInt >> 128n));
|
|
97
|
+
|
|
98
|
+
// Amount and timestamp
|
|
99
|
+
const amount = toFieldElement(String(payment.amount));
|
|
100
|
+
const timestamp = toFieldElement(String(payment.timestamp));
|
|
101
|
+
const minAmount = toFieldElement(String(cfg.minAmount));
|
|
102
|
+
|
|
103
|
+
return R.always({
|
|
104
|
+
witness: {
|
|
105
|
+
txHashPacked_0: lower128,
|
|
106
|
+
txHashPacked_1: upper128,
|
|
107
|
+
recipientLow,
|
|
108
|
+
recipientHigh,
|
|
109
|
+
amount,
|
|
110
|
+
timestamp,
|
|
111
|
+
minAmount,
|
|
112
|
+
},
|
|
113
|
+
publicInputs: [
|
|
114
|
+
// commitment will be computed during proof generation
|
|
115
|
+
toFieldElement(String(cfg.minAmount)), // minAmountPublic
|
|
116
|
+
toFieldElement(String(cfg.maxProofAge)), // timestampMax
|
|
117
|
+
],
|
|
118
|
+
})();
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Private Helpers
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch payment transaction details from the blockchain.
|
|
129
|
+
*/
|
|
130
|
+
const fetchPaymentTransaction = async (
|
|
131
|
+
txHash: `0x${string}`,
|
|
132
|
+
client: PublicClient,
|
|
133
|
+
): Promise<Transaction> => {
|
|
134
|
+
const txPromise = client.getTransaction({ hash: txHash });
|
|
135
|
+
const receiptPromise = client.getTransactionReceipt({ hash: txHash });
|
|
136
|
+
const blockPromise = client.getBlock({ blockTag: "latest" });
|
|
137
|
+
|
|
138
|
+
const [tx, receipt, block] = await Promise.all([
|
|
139
|
+
txPromise,
|
|
140
|
+
receiptPromise,
|
|
141
|
+
blockPromise,
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
return receipt.status !== "success"
|
|
145
|
+
? Promise.reject(new Error(`Transaction ${txHash} failed`))
|
|
146
|
+
: client.getBlock({ blockNumber: receipt.blockNumber }).then(
|
|
147
|
+
(blockData) =>
|
|
148
|
+
R.always({
|
|
149
|
+
txHash,
|
|
150
|
+
from: tx.from,
|
|
151
|
+
to: tx.to ?? ("0x" as `0x${string}`),
|
|
152
|
+
amount: tx.value + receipt.effectiveGasPrice * receipt.gasUsed,
|
|
153
|
+
timestamp: Number(blockData.timestamp),
|
|
154
|
+
blockNumber: receipt.blockNumber,
|
|
155
|
+
confirmations: Number(block.number - receipt.blockNumber),
|
|
156
|
+
})(),
|
|
157
|
+
);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate ZK proof for the payment transaction.
|
|
162
|
+
*/
|
|
163
|
+
const generateZKProof = async (
|
|
164
|
+
lemmaClient: LemmaClient,
|
|
165
|
+
payment: Transaction,
|
|
166
|
+
config: Config,
|
|
167
|
+
): Promise<Proof> => {
|
|
168
|
+
// Build witness from payment transaction
|
|
169
|
+
const builder = createProofGenerator(config, lemmaClient);
|
|
170
|
+
const { witness, publicInputs } = builder.toFieldElements(payment, config);
|
|
171
|
+
|
|
172
|
+
// Generate proof using Lemma SDK
|
|
173
|
+
const proofResult = await prover.prove(lemmaClient, {
|
|
174
|
+
circuitId: config.circuitId,
|
|
175
|
+
witness,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Compute the commitment for public inputs
|
|
179
|
+
const commitment = computeCommitment(witness, publicInputs);
|
|
180
|
+
|
|
181
|
+
return R.always({
|
|
182
|
+
proof: proofResult.proof,
|
|
183
|
+
inputs: [commitment, ...publicInputs],
|
|
184
|
+
circuitId: config.circuitId,
|
|
185
|
+
generatedAt: Date.now(),
|
|
186
|
+
})();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compute the commitment hash for the circuit public inputs.
|
|
191
|
+
*/
|
|
192
|
+
const computeCommitment = (
|
|
193
|
+
witness: Record<string, string>,
|
|
194
|
+
_publicInputs: string[],
|
|
195
|
+
): string => {
|
|
196
|
+
// The commitment is computed from:
|
|
197
|
+
// poseidon(txHashPacked[0], txHashPacked[1], recipientLow, amount, timestamp, minAmount)
|
|
198
|
+
// This should match the public commitment used in the circuit
|
|
199
|
+
const values = [
|
|
200
|
+
witness.txHashPacked_0,
|
|
201
|
+
witness.txHashPacked_1,
|
|
202
|
+
witness.recipientLow,
|
|
203
|
+
witness.amount,
|
|
204
|
+
witness.timestamp,
|
|
205
|
+
witness.minAmount,
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
// In a real implementation, use poseidon from circomlib
|
|
209
|
+
// For now, we use a simple hash placeholder
|
|
210
|
+
return values.join(",");
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export type {
|
|
214
|
+
ProofResult,
|
|
215
|
+
Config,
|
|
216
|
+
Proof,
|
|
217
|
+
Transaction,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export { createProofGenerator };
|