bstest001 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/README.md +22 -0
- package/circuits/transaction2.wasm +0 -0
- package/circuits/transaction2.zkey +0 -0
- package/dist/balance.d.ts +7 -0
- package/dist/balance.js +46 -0
- package/dist/deposit.d.ts +8 -0
- package/dist/deposit.js +136 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/utils/EtherPool.abi.json +798 -0
- package/dist/utils/constants.d.ts +8 -0
- package/dist/utils/constants.js +8 -0
- package/dist/utils/encryption.d.ts +8 -0
- package/dist/utils/encryption.js +34 -0
- package/dist/utils/keypair.d.ts +9 -0
- package/dist/utils/keypair.js +19 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/prover.d.ts +9 -0
- package/dist/utils/prover.js +63 -0
- package/dist/utils/utils.d.ts +82 -0
- package/dist/utils/utils.js +202 -0
- package/dist/utils/utxo.d.ts +20 -0
- package/dist/utils/utxo.js +55 -0
- package/dist/withdraw.d.ts +7 -0
- package/dist/withdraw.js +123 -0
- package/package.json +30 -0
- package/src/balance.ts +55 -0
- package/src/deposit.ts +157 -0
- package/src/index.ts +5 -0
- package/src/utils/EtherPool.abi.json +798 -0
- package/src/utils/constants.ts +10 -0
- package/src/utils/encryption.ts +40 -0
- package/src/utils/keypair.ts +24 -0
- package/src/utils/logger.ts +42 -0
- package/src/utils/prover.ts +82 -0
- package/src/utils/utils.ts +295 -0
- package/src/utils/utxo.ts +74 -0
- package/src/withdraw.ts +153 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export let CONTRACT_ADDRESS = '0x51450B59ec2Bc26852415cB1047DF31772aD9cD5';
|
|
2
|
+
export let FEE_RECIPIENT_ADDRESS = '0x44eb9939cfdE7C394f1632C6890191d695f0a3ce';
|
|
3
|
+
export let INDEXER_URL = process.env.INDEXER_URL || 'https://basetest.privacycashapi.com';
|
|
4
|
+
export let PRIVATE_KEY = process.env.PRIVATE_KEY || '';
|
|
5
|
+
export let BASE_SEPOLIA_RPC = process.env.BASE_SEPOLIA_RPC || 'https://sepolia.base.org';
|
|
6
|
+
|
|
7
|
+
export const RENT_FEE = 0.00025
|
|
8
|
+
export const FEE_RATE = 35; // 0.35% (basis points out of 10000)
|
|
9
|
+
|
|
10
|
+
export const SIGN_PRIVACY_MESSAGE = 'Privacy Money account sign in';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { ethers } from 'ethers';
|
|
3
|
+
import { Keypair } from './keypair.js';
|
|
4
|
+
|
|
5
|
+
export function deriveKeys(signature: string) {
|
|
6
|
+
const encryptionKeyHex = ethers.utils.keccak256(signature);
|
|
7
|
+
const encryptionKey = Buffer.from(encryptionKeyHex.slice(2), 'hex');
|
|
8
|
+
const utxoPrivateKey = ethers.utils.keccak256(encryptionKey);
|
|
9
|
+
const keypair = new Keypair(utxoPrivateKey);
|
|
10
|
+
return { encryptionKey, utxoPrivateKey, keypair };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// export async function signMessage(signer: ethers.Signer) {
|
|
14
|
+
// const signature = await signer.signMessage(SIGN_IN_MESSAGe);
|
|
15
|
+
// return deriveKeys(signature);
|
|
16
|
+
// }
|
|
17
|
+
|
|
18
|
+
export function encrypt(data: string | Buffer, encryptionKey: Buffer): string {
|
|
19
|
+
const dataBuffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
20
|
+
const iv = crypto.randomBytes(12);
|
|
21
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey as any, iv as any);
|
|
22
|
+
const encrypted = Buffer.concat([cipher.update(dataBuffer as any), cipher.final()] as any[]);
|
|
23
|
+
const authTag = cipher.getAuthTag();
|
|
24
|
+
const result = Buffer.concat([iv, authTag, encrypted] as any[]);
|
|
25
|
+
return '0x' + result.toString('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function decrypt(encryptedData: string | Buffer, encryptionKey: Buffer): Buffer {
|
|
29
|
+
const buf = typeof encryptedData === 'string'
|
|
30
|
+
? Buffer.from(encryptedData.replace(/^0x/, ''), 'hex')
|
|
31
|
+
: (encryptedData as any);
|
|
32
|
+
|
|
33
|
+
const iv = buf.slice(0, 12);
|
|
34
|
+
const authTag = buf.slice(12, 28);
|
|
35
|
+
const data = buf.slice(28);
|
|
36
|
+
|
|
37
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey as any, iv as any);
|
|
38
|
+
decipher.setAuthTag(authTag as any);
|
|
39
|
+
return Buffer.concat([decipher.update(data as any), decipher.final()] as any[]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BigNumber, ethers } from 'ethers';
|
|
2
|
+
import { poseidonHash, toFixedHex } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export class Keypair {
|
|
5
|
+
public privkey: string;
|
|
6
|
+
public pubkey: BigNumber;
|
|
7
|
+
|
|
8
|
+
constructor(privkey: string = ethers.Wallet.createRandom().privateKey) {
|
|
9
|
+
this.privkey = privkey;
|
|
10
|
+
this.pubkey = poseidonHash([this.privkey]);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toString(): string {
|
|
14
|
+
return toFixedHex(this.pubkey);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
address(): string {
|
|
18
|
+
return this.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
sign(commitment: any, merklePath: any): BigNumber {
|
|
22
|
+
return poseidonHash([this.privkey, commitment, merklePath]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// log level
|
|
2
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
3
|
+
|
|
4
|
+
export type LoggerFn = (level: LogLevel, message: string) => void;
|
|
5
|
+
|
|
6
|
+
const defaultLogger: LoggerFn = (level, message) => {
|
|
7
|
+
const prefix = `[${level.toUpperCase()}]`;
|
|
8
|
+
console.log(prefix, message);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let userLogger: LoggerFn = defaultLogger;
|
|
12
|
+
|
|
13
|
+
export function setLogger(logger: LoggerFn) {
|
|
14
|
+
userLogger = logger;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function argToStr(args: unknown[]) {
|
|
18
|
+
return args.map(arg => {
|
|
19
|
+
if (typeof arg === "object" && arg !== null) {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(arg);
|
|
22
|
+
} catch {
|
|
23
|
+
return String(arg);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return String(arg);
|
|
27
|
+
}).join(" ");
|
|
28
|
+
}
|
|
29
|
+
export const logger = {
|
|
30
|
+
debug: (...args: unknown[]) => {
|
|
31
|
+
userLogger('debug', argToStr(args))
|
|
32
|
+
},
|
|
33
|
+
info: (...args: unknown[]) => {
|
|
34
|
+
userLogger('info', argToStr(args))
|
|
35
|
+
},
|
|
36
|
+
warn: (...args: unknown[]) => {
|
|
37
|
+
userLogger('warn', argToStr(args))
|
|
38
|
+
},
|
|
39
|
+
error: (...args: unknown[]) => {
|
|
40
|
+
userLogger('error', argToStr(args))
|
|
41
|
+
},
|
|
42
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExecFileOptions } from 'child_process';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import { utils } from 'ffjavascript';
|
|
4
|
+
import { toFixedHex } from './utils.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates a Zero-Knowledge proof for a transaction.
|
|
8
|
+
* Supports both Node.js (via snarkjs CLI) and Web Browser (via snarkjs library).
|
|
9
|
+
*/
|
|
10
|
+
export async function prove(input: any, keyBasePath: string) {
|
|
11
|
+
// 1. Check environment
|
|
12
|
+
const isNode = typeof process !== 'undefined' && process.versions && (!!process.versions.node || !!process.versions.bun);
|
|
13
|
+
|
|
14
|
+
if (isNode) {
|
|
15
|
+
return await proveNode(input, keyBasePath);
|
|
16
|
+
} else {
|
|
17
|
+
return await proveBrowser(input, keyBasePath);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function proveBrowser(input: any, keyBasePath: string) {
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
const snarkjs = await import('snarkjs');
|
|
24
|
+
|
|
25
|
+
// In the browser, we use snarkjs.groth16.fullProve
|
|
26
|
+
const { proof } = await snarkjs.groth16.fullProve(
|
|
27
|
+
utils.stringifyBigInts(input),
|
|
28
|
+
`${keyBasePath}.wasm`,
|
|
29
|
+
`${keyBasePath}.zkey`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const pA = [toFixedHex(proof.pi_a[0]), toFixedHex(proof.pi_a[1])];
|
|
33
|
+
const pB = [
|
|
34
|
+
[toFixedHex(proof.pi_b[0][1]), toFixedHex(proof.pi_b[0][0])],
|
|
35
|
+
[toFixedHex(proof.pi_b[1][1]), toFixedHex(proof.pi_b[1][0])],
|
|
36
|
+
];
|
|
37
|
+
const pC = [toFixedHex(proof.pi_c[0]), toFixedHex(proof.pi_c[1])];
|
|
38
|
+
|
|
39
|
+
return { pA, pB, pC };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function proveNode(input: any, keyBasePath: string) {
|
|
43
|
+
const { execFile } = await import('child_process');
|
|
44
|
+
const { promises: fs } = await import('fs');
|
|
45
|
+
const os = (await import('os')).default;
|
|
46
|
+
const path = (await import('path')).default;
|
|
47
|
+
const { promisify } = await import('util');
|
|
48
|
+
const execFileAsync = promisify(execFile);
|
|
49
|
+
|
|
50
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'privacycash-proof-'));
|
|
51
|
+
const inputPath = path.join(tmpDir, 'input.json');
|
|
52
|
+
const proofPath = path.join(tmpDir, 'proof.json');
|
|
53
|
+
const publicPath = path.join(tmpDir, 'public.json');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await fs.writeFile(inputPath, JSON.stringify(utils.stringifyBigInts(input)));
|
|
57
|
+
|
|
58
|
+
await execFileAsync(
|
|
59
|
+
path.resolve(process.cwd(), 'node_modules/.bin/snarkjs'),
|
|
60
|
+
['groth16', 'fullprove', inputPath, `${keyBasePath}.wasm`, `${keyBasePath}.zkey`, proofPath, publicPath],
|
|
61
|
+
{ maxBuffer: 1024 * 1024 * 10 } as ExecFileOptions,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const proofRaw = await fs.readFile(proofPath, 'utf8');
|
|
65
|
+
const proof = JSON.parse(proofRaw);
|
|
66
|
+
|
|
67
|
+
const pA = [toFixedHex(proof.pi_a[0]), toFixedHex(proof.pi_a[1])];
|
|
68
|
+
const pB = [
|
|
69
|
+
[toFixedHex(proof.pi_b[0][1]), toFixedHex(proof.pi_b[0][0])],
|
|
70
|
+
[toFixedHex(proof.pi_b[1][1]), toFixedHex(proof.pi_b[1][0])],
|
|
71
|
+
];
|
|
72
|
+
const pC = [toFixedHex(proof.pi_c[0]), toFixedHex(proof.pi_c[1])];
|
|
73
|
+
|
|
74
|
+
return { pA, pB, pC };
|
|
75
|
+
} finally {
|
|
76
|
+
try {
|
|
77
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { BigNumber, ethers } from 'ethers';
|
|
3
|
+
import { poseidon1, poseidon2, poseidon3, poseidon4 } from 'poseidon-lite';
|
|
4
|
+
import { INDEXER_URL } from './constants.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
import { prove } from './prover.js';
|
|
7
|
+
import { Utxo } from './utxo.js';
|
|
8
|
+
|
|
9
|
+
export const FIELD_SIZE = BigNumber.from(
|
|
10
|
+
'21888242871839275222246405745257275088548364400416034343698204186575808495617',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export function poseidonHash(items: any[]): BigNumber {
|
|
14
|
+
if (items.length === 1) return BigNumber.from(poseidon1([items[0]]));
|
|
15
|
+
if (items.length === 2) return BigNumber.from(poseidon2([items[0], items[1]]));
|
|
16
|
+
if (items.length === 3) return BigNumber.from(poseidon3([items[0], items[1], items[2]]));
|
|
17
|
+
if (items.length === 4) return BigNumber.from(poseidon4([items[0], items[1], items[2], items[3]]));
|
|
18
|
+
throw new Error(`Unsupported poseidon input length: ${items.length}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const poseidonHash2 = (a: any, b: any) => poseidonHash([a, b]);
|
|
22
|
+
|
|
23
|
+
/** Generate random number of specified byte length */
|
|
24
|
+
export const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes));
|
|
25
|
+
|
|
26
|
+
/** BigNumber to hex string of specified length */
|
|
27
|
+
export function toFixedHex(number: any, length = 32): string {
|
|
28
|
+
let result =
|
|
29
|
+
'0x' +
|
|
30
|
+
(number instanceof Buffer
|
|
31
|
+
? number.toString('hex')
|
|
32
|
+
: BigNumber.from(number).toHexString().replace('0x', '')
|
|
33
|
+
).padStart(length * 2, '0');
|
|
34
|
+
if (result.indexOf('-') > -1) {
|
|
35
|
+
result = '-' + result.replace('-', '');
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Convert value into buffer of specified byte length */
|
|
41
|
+
export const toBuffer = (value: any, length: number) =>
|
|
42
|
+
Buffer.from(
|
|
43
|
+
BigNumber.from(value)
|
|
44
|
+
.toHexString()
|
|
45
|
+
.slice(2)
|
|
46
|
+
.padStart(length * 2, '0'),
|
|
47
|
+
'hex',
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export function getExtDataHash({
|
|
51
|
+
recipient,
|
|
52
|
+
extAmount,
|
|
53
|
+
relayer,
|
|
54
|
+
feeRecipient,
|
|
55
|
+
fee,
|
|
56
|
+
encryptedOutput1,
|
|
57
|
+
encryptedOutput2,
|
|
58
|
+
}: {
|
|
59
|
+
recipient: any;
|
|
60
|
+
extAmount: any;
|
|
61
|
+
relayer?: any;
|
|
62
|
+
feeRecipient?: any;
|
|
63
|
+
fee: any;
|
|
64
|
+
encryptedOutput1: string;
|
|
65
|
+
encryptedOutput2: string;
|
|
66
|
+
}): BigNumber {
|
|
67
|
+
const abi = new ethers.utils.AbiCoder();
|
|
68
|
+
|
|
69
|
+
const encodedData = abi.encode(
|
|
70
|
+
[
|
|
71
|
+
'tuple(address recipient,int256 extAmount,address feeRecipient,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2)',
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
recipient: toFixedHex(recipient, 20),
|
|
76
|
+
extAmount: toFixedHex(extAmount),
|
|
77
|
+
feeRecipient,
|
|
78
|
+
fee: toFixedHex(fee),
|
|
79
|
+
encryptedOutput1: encryptedOutput1,
|
|
80
|
+
encryptedOutput2: encryptedOutput2,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
);
|
|
84
|
+
const hash = ethers.utils.keccak256(encodedData);
|
|
85
|
+
return BigNumber.from(hash).mod(FIELD_SIZE);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function getProof({
|
|
89
|
+
inputs,
|
|
90
|
+
outputs,
|
|
91
|
+
extAmount,
|
|
92
|
+
fee,
|
|
93
|
+
recipient,
|
|
94
|
+
feeRecipient,
|
|
95
|
+
encryptionKey,
|
|
96
|
+
keyBasePath,
|
|
97
|
+
}: {
|
|
98
|
+
inputs: Utxo[];
|
|
99
|
+
outputs: Utxo[];
|
|
100
|
+
extAmount: BigNumber;
|
|
101
|
+
fee: BigNumber;
|
|
102
|
+
recipient: string;
|
|
103
|
+
feeRecipient: string;
|
|
104
|
+
encryptionKey: Buffer;
|
|
105
|
+
keyBasePath: string;
|
|
106
|
+
}) {
|
|
107
|
+
let inputMerklePathIndices: number[] = [];
|
|
108
|
+
let inputMerklePathElements: any[] = [];
|
|
109
|
+
|
|
110
|
+
// fetch /merkle/root from indexer url
|
|
111
|
+
const res = await fetch(`${INDEXER_URL}/merkle/root`);
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
throw new Error(`Failed to fetch merkle root: ${res.status} ${res.statusText}`);
|
|
114
|
+
}
|
|
115
|
+
const { root, nextIndex: initialNextIndex } = await res.json();
|
|
116
|
+
let nextIndex = initialNextIndex;
|
|
117
|
+
|
|
118
|
+
for (const input of inputs) {
|
|
119
|
+
if (input.amount.gt(0)) {
|
|
120
|
+
let res = await fetch(`${INDEXER_URL}/commitment/`, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({ commitment: toFixedHex(input.getCommitment()) }),
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
throw new Error(`Failed to fetch commitment info`);
|
|
127
|
+
}
|
|
128
|
+
const { index, pathElements } = await res.json();
|
|
129
|
+
input.index = index;
|
|
130
|
+
if (input.index == null) {
|
|
131
|
+
throw new Error(`Input commitment ${toFixedHex(input.getCommitment())} was not found`);
|
|
132
|
+
}
|
|
133
|
+
inputMerklePathIndices.push(input.index);
|
|
134
|
+
inputMerklePathElements.push(pathElements);
|
|
135
|
+
} else {
|
|
136
|
+
inputMerklePathIndices.push(0);
|
|
137
|
+
inputMerklePathElements.push(new Array(26).fill(0));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// update nextIndex to outputs
|
|
142
|
+
for (let output of outputs) {
|
|
143
|
+
output.index = nextIndex++;
|
|
144
|
+
}
|
|
145
|
+
const extData = {
|
|
146
|
+
recipient,
|
|
147
|
+
extAmount: toFixedHex(extAmount),
|
|
148
|
+
feeRecipient,
|
|
149
|
+
fee: toFixedHex(fee),
|
|
150
|
+
encryptedOutput1: outputs[0].encrypt(encryptionKey),
|
|
151
|
+
encryptedOutput2: (outputs[1] || new Utxo()).encrypt(encryptionKey),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const extDataHash = getExtDataHash(extData);
|
|
155
|
+
let input = {
|
|
156
|
+
root,
|
|
157
|
+
inputNullifier: inputs.map((x) => x.getNullifier().toString()),
|
|
158
|
+
outputCommitment: outputs.map((x) => x.getCommitment().toString()),
|
|
159
|
+
publicAmount: BigNumber.from(extAmount).sub(fee).add(FIELD_SIZE).mod(FIELD_SIZE).toString(),
|
|
160
|
+
extDataHash: extDataHash.toString(),
|
|
161
|
+
mintAddress: inputs[0].mintAddress.toString(),
|
|
162
|
+
|
|
163
|
+
inAmount: inputs.map((x) => x.amount.toString()),
|
|
164
|
+
inPrivateKey: inputs.map((x) => x.keypair.privkey.toString()),
|
|
165
|
+
inBlinding: inputs.map((x) => x.blinding.toString()),
|
|
166
|
+
inPathIndices: inputMerklePathIndices,
|
|
167
|
+
inPathElements: inputMerklePathElements,
|
|
168
|
+
|
|
169
|
+
outAmount: outputs.map((x) => x.amount.toString()),
|
|
170
|
+
outBlinding: outputs.map((x) => x.blinding.toString()),
|
|
171
|
+
outPubkey: outputs.map((x) => x.keypair.pubkey.toString()),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const { pA, pB, pC } = await prove(input, `${keyBasePath}${inputs.length}`);
|
|
175
|
+
|
|
176
|
+
const args = {
|
|
177
|
+
pA,
|
|
178
|
+
pB,
|
|
179
|
+
pC,
|
|
180
|
+
root: toFixedHex(input.root),
|
|
181
|
+
inputNullifiers: inputs.map((x) => toFixedHex(x.getNullifier())),
|
|
182
|
+
outputCommitments: outputs.map((x) => toFixedHex(x.getCommitment())),
|
|
183
|
+
publicAmount: toFixedHex(input.publicAmount),
|
|
184
|
+
extDataHash: toFixedHex(extDataHash),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return { extData, args };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function prepareTransaction({
|
|
191
|
+
inputs = [],
|
|
192
|
+
outputs = [],
|
|
193
|
+
fee = BigNumber.from(0),
|
|
194
|
+
recipient = '0x44eb9939cfdE7C394f1632C6890191d695f0a3ce',
|
|
195
|
+
feeRecipient = '0x44eb9939cfdE7C394f1632C6890191d695f0a3ce',
|
|
196
|
+
encryptionKey,
|
|
197
|
+
keyBasePath,
|
|
198
|
+
}: {
|
|
199
|
+
inputs?: Utxo[];
|
|
200
|
+
outputs?: Utxo[];
|
|
201
|
+
fee?: BigNumber;
|
|
202
|
+
recipient?: string;
|
|
203
|
+
feeRecipient?: string;
|
|
204
|
+
encryptionKey: Buffer;
|
|
205
|
+
keyBasePath: string;
|
|
206
|
+
}) {
|
|
207
|
+
if (inputs.length > 2 || outputs.length > 2) {
|
|
208
|
+
throw new Error('Only 2 inputs and 2 outputs are supported');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
while (inputs.length < 2) inputs.push(new Utxo());
|
|
212
|
+
while (outputs.length < 2) outputs.push(new Utxo());
|
|
213
|
+
|
|
214
|
+
let extAmount = outputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0))
|
|
215
|
+
.sub(inputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
|
|
216
|
+
.add(fee);
|
|
217
|
+
|
|
218
|
+
const { extData, args } = await getProof({
|
|
219
|
+
inputs,
|
|
220
|
+
outputs,
|
|
221
|
+
extAmount,
|
|
222
|
+
fee,
|
|
223
|
+
recipient,
|
|
224
|
+
feeRecipient,
|
|
225
|
+
encryptionKey,
|
|
226
|
+
keyBasePath,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return { extData, args };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function findUnspentUtxos({
|
|
233
|
+
etherPool,
|
|
234
|
+
encryptionKey,
|
|
235
|
+
keypair,
|
|
236
|
+
}: {
|
|
237
|
+
etherPool: ethers.Contract;
|
|
238
|
+
encryptionKey: Buffer;
|
|
239
|
+
keypair: any;
|
|
240
|
+
}) {
|
|
241
|
+
let start = 0
|
|
242
|
+
const limit = 1000
|
|
243
|
+
let hasMore = true
|
|
244
|
+
const candidates: { utxo: Utxo; nullifier: string }[] = []
|
|
245
|
+
|
|
246
|
+
while (hasMore) {
|
|
247
|
+
let url = `${INDEXER_URL}/get_encrypted?start=${start}&end=${start + limit}`
|
|
248
|
+
logger.debug(`Fetching encrypted UTXOs from indexer: ${url}`)
|
|
249
|
+
const res = await fetch(url)
|
|
250
|
+
if (!res.ok) {
|
|
251
|
+
throw new Error(`Failed to fetch encrypted UTXOs: ${res.statusText}`)
|
|
252
|
+
}
|
|
253
|
+
const data = await res.json()
|
|
254
|
+
const { encrypted_outputs, hasMore: more, start: realStart } = data
|
|
255
|
+
|
|
256
|
+
if (encrypted_outputs && encrypted_outputs.length > 0) {
|
|
257
|
+
encrypted_outputs.forEach((encryptedOutput: string, i: number) => {
|
|
258
|
+
const index = (realStart ?? start) + i
|
|
259
|
+
try {
|
|
260
|
+
const utxo = Utxo.decrypt(encryptionKey, encryptedOutput, Number(index), keypair)
|
|
261
|
+
if (!utxo.amount.isZero()) {
|
|
262
|
+
utxo.index = index
|
|
263
|
+
const nullifier = toFixedHex(utxo.getNullifier())
|
|
264
|
+
candidates.push({ utxo, nullifier })
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Decryption failed — this output belongs to someone else
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
hasMore = more
|
|
273
|
+
start += limit
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// log candidates length
|
|
277
|
+
logger.debug(`Found ${candidates.length} candidate UTXOs, checking which are unspent...`)
|
|
278
|
+
|
|
279
|
+
const unspent: Utxo[] = []
|
|
280
|
+
const spentCheckChunkSize = 256
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < candidates.length; i += spentCheckChunkSize) {
|
|
283
|
+
const chunk = candidates.slice(i, i + spentCheckChunkSize)
|
|
284
|
+
const nullifiers = chunk.map((x) => x.nullifier)
|
|
285
|
+
const spentFlags: boolean[] = await etherPool.isSpentArray(nullifiers)
|
|
286
|
+
|
|
287
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
288
|
+
if (!spentFlags[j]) {
|
|
289
|
+
unspent.push(chunk[j].utxo)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return unspent
|
|
295
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { BigNumber } from 'ethers';
|
|
2
|
+
import { decrypt, encrypt } from './encryption.js';
|
|
3
|
+
import { Keypair } from './keypair.js';
|
|
4
|
+
import { poseidonHash, randomBN } from './utils.js';
|
|
5
|
+
|
|
6
|
+
const DUMMY_MINT = BigNumber.from(0);
|
|
7
|
+
|
|
8
|
+
export class Utxo {
|
|
9
|
+
public amount: BigNumber;
|
|
10
|
+
public blinding: BigNumber;
|
|
11
|
+
public keypair: Keypair;
|
|
12
|
+
public index: number | null;
|
|
13
|
+
public mintAddress: BigNumber;
|
|
14
|
+
|
|
15
|
+
constructor({
|
|
16
|
+
amount = 0,
|
|
17
|
+
keypair = new Keypair(),
|
|
18
|
+
blinding = randomBN(),
|
|
19
|
+
index = null,
|
|
20
|
+
mintAddress = DUMMY_MINT,
|
|
21
|
+
}: {
|
|
22
|
+
amount?: any;
|
|
23
|
+
keypair?: Keypair;
|
|
24
|
+
blinding?: any;
|
|
25
|
+
index?: number | null;
|
|
26
|
+
mintAddress?: any;
|
|
27
|
+
} = {}) {
|
|
28
|
+
this.amount = BigNumber.from(amount);
|
|
29
|
+
this.blinding = BigNumber.from(blinding);
|
|
30
|
+
this.keypair = keypair;
|
|
31
|
+
this.index = index;
|
|
32
|
+
this.mintAddress = BigNumber.from(mintAddress);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getCommitment(): BigNumber {
|
|
36
|
+
return poseidonHash([this.amount, this.keypair.pubkey, this.blinding, this.mintAddress]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getNullifier(): BigNumber {
|
|
40
|
+
if (this.amount.gt(0)) {
|
|
41
|
+
if (this.index == null) {
|
|
42
|
+
throw new Error('Can not compute nullifier without utxo index');
|
|
43
|
+
}
|
|
44
|
+
if (this.keypair.privkey == null) {
|
|
45
|
+
throw new Error('Can not compute nullifier without utxo private key');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const commitment = this.getCommitment();
|
|
49
|
+
const idx = this.index || 0;
|
|
50
|
+
const signature = this.keypair.sign(commitment, idx);
|
|
51
|
+
return poseidonHash([commitment, idx, signature]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
encrypt(encryptionKey: Buffer): string {
|
|
55
|
+
const utxoString = `${this.amount.toString()}|${this.blinding.toString()}|${this.index || 0}|${this.mintAddress.toString()}`;
|
|
56
|
+
return encrypt(utxoString, encryptionKey);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static decrypt(encryptionKey: Buffer, data: string, index: number, keypair: Keypair): Utxo {
|
|
60
|
+
const decrypted = decrypt(data, encryptionKey);
|
|
61
|
+
const parts = decrypted.toString().split('|');
|
|
62
|
+
if (parts.length !== 4) {
|
|
63
|
+
throw new Error('Invalid UTXO format after decryption');
|
|
64
|
+
}
|
|
65
|
+
const [amount, blinding, , mintAddress] = parts;
|
|
66
|
+
return new Utxo({
|
|
67
|
+
amount: BigNumber.from(amount),
|
|
68
|
+
blinding: BigNumber.from(blinding),
|
|
69
|
+
keypair,
|
|
70
|
+
index,
|
|
71
|
+
mintAddress: BigNumber.from(mintAddress),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|