@interest-protocol/vortex-sdk 0.0.1-alpha.0 → 1.1.0
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/.eslingignore +1 -0
- package/dist/__tests__/entities/keypair.spec.d.ts +2 -0
- package/dist/__tests__/entities/keypair.spec.d.ts.map +1 -0
- package/dist/__tests__/test-utils.d.ts +25 -0
- package/dist/__tests__/test-utils.d.ts.map +1 -0
- package/dist/__tests__/types.d.ts +3 -0
- package/dist/__tests__/types.d.ts.map +1 -0
- package/dist/__tests__/vortex.test.d.ts +2 -0
- package/dist/__tests__/vortex.test.d.ts.map +1 -0
- package/dist/constants.d.ts +44 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/crypto/ff/f1field.d.ts +76 -0
- package/dist/crypto/ff/f1field.d.ts.map +1 -0
- package/dist/crypto/ff/index.d.ts +6 -0
- package/dist/crypto/ff/index.d.ts.map +1 -0
- package/dist/crypto/ff/random.d.ts +2 -0
- package/dist/crypto/ff/random.d.ts.map +1 -0
- package/dist/crypto/ff/scalar.d.ts +45 -0
- package/dist/crypto/ff/scalar.d.ts.map +1 -0
- package/dist/crypto/ff/utils.d.ts +6 -0
- package/dist/crypto/ff/utils.d.ts.map +1 -0
- package/dist/crypto/index.d.ts +6 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/poseidon/index.d.ts +2 -0
- package/dist/crypto/poseidon/index.d.ts.map +1 -0
- package/dist/crypto/poseidon/poseidon-constants-opt.d.ts +7 -0
- package/dist/crypto/poseidon/poseidon-constants-opt.d.ts.map +1 -0
- package/dist/crypto/poseidon/poseidon-opt.d.ts +16 -0
- package/dist/crypto/poseidon/poseidon-opt.d.ts.map +1 -0
- package/dist/deposit.d.ts +4 -0
- package/dist/deposit.d.ts.map +1 -0
- package/dist/entities/index.d.ts +4 -0
- package/dist/entities/index.d.ts.map +1 -0
- package/dist/entities/keypair.d.ts +29 -0
- package/dist/entities/keypair.d.ts.map +1 -0
- package/dist/entities/merkle-tree.d.ts +81 -0
- package/dist/entities/merkle-tree.d.ts.map +1 -0
- package/dist/entities/utxo.d.ts +24 -0
- package/dist/entities/utxo.d.ts.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38280 -4459
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38244 -4453
- package/dist/index.mjs.map +1 -1
- package/dist/jest-setup.d.ts +2 -0
- package/dist/jest-setup.d.ts.map +1 -0
- package/dist/keys/index.d.ts +3 -0
- package/dist/keys/index.d.ts.map +1 -0
- package/dist/pkg/nodejs/vortex.d.ts +11 -0
- package/dist/pkg/nodejs/vortex.d.ts.map +1 -0
- package/dist/pkg/web/vortex.d.ts +44 -0
- package/dist/pkg/web/vortex.d.ts.map +1 -0
- package/dist/utils/decrypt.d.ts +12 -0
- package/dist/utils/decrypt.d.ts.map +1 -0
- package/dist/utils/env.d.ts +2 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/events.d.ts +7 -0
- package/dist/utils/events.d.ts.map +1 -0
- package/dist/utils/ext-data.d.ts +3 -0
- package/dist/utils/ext-data.d.ts.map +1 -0
- package/dist/utils/index.d.ts +50 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/prove.d.ts +3 -0
- package/dist/utils/prove.d.ts.map +1 -0
- package/dist/vortex.d.ts +51 -21
- package/dist/vortex.d.ts.map +1 -1
- package/dist/vortex.types.d.ts +74 -50
- package/dist/vortex.types.d.ts.map +1 -1
- package/dist/vortex_bg.wasm +0 -0
- package/dist/withdraw.d.ts +4 -0
- package/dist/withdraw.d.ts.map +1 -0
- package/jest.config.js +31 -0
- package/package.json +22 -7
- package/src/__tests__/entities/keypair.spec.ts +191 -0
- package/src/__tests__/test-utils.ts +76 -0
- package/src/__tests__/types.ts +3 -0
- package/src/__tests__/vortex.test.ts +25 -0
- package/src/constants.ts +104 -0
- package/src/crypto/ff/f1field.ts +464 -0
- package/src/crypto/ff/index.ts +6 -0
- package/src/crypto/ff/random.ts +32 -0
- package/src/crypto/ff/readme.md +8 -0
- package/src/crypto/ff/scalar.ts +264 -0
- package/src/crypto/ff/utils.ts +121 -0
- package/src/crypto/index.ts +8 -0
- package/src/crypto/poseidon/index.ts +1 -0
- package/src/crypto/poseidon/poseidon-constants-opt.ts +24806 -0
- package/src/crypto/poseidon/poseidon-opt.ts +184 -0
- package/src/deposit.ts +168 -0
- package/src/entities/index.ts +3 -0
- package/src/entities/keypair.ts +262 -0
- package/src/entities/merkle-tree.ts +256 -0
- package/src/entities/utxo.ts +52 -0
- package/src/index.ts +6 -2
- package/src/jest-setup.ts +2 -0
- package/src/keys/index.ts +5 -0
- package/src/pkg/nodejs/vortex.d.ts +36 -0
- package/src/pkg/nodejs/vortex.js +332 -0
- package/src/pkg/nodejs/vortex_bg.wasm +0 -0
- package/src/pkg/nodejs/vortex_bg.wasm.d.ts +12 -0
- package/src/pkg/web/vortex.d.ts +72 -0
- package/src/pkg/web/vortex.js +442 -0
- package/src/pkg/web/vortex_bg.wasm +0 -0
- package/src/pkg/web/vortex_bg.wasm.d.ts +12 -0
- package/src/utils/decrypt.ts +46 -0
- package/src/utils/env.ts +18 -0
- package/src/utils/events.ts +16 -0
- package/src/utils/ext-data.ts +43 -0
- package/src/utils/index.ts +152 -0
- package/src/utils/prove.ts +18 -0
- package/src/vortex.ts +235 -111
- package/src/vortex.types.ts +74 -54
- package/src/withdraw.ts +159 -0
- package/tsconfig.json +4 -2
- package/dist/admin.d.ts +0 -17
- package/dist/admin.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -11
- package/dist/utils.d.ts.map +0 -1
- package/src/admin.ts +0 -124
- package/src/utils.ts +0 -66
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { F1Field, Scalar, utils } from '../ff';
|
|
2
|
+
import { CONSTANTS } from './poseidon-constants-opt';
|
|
3
|
+
|
|
4
|
+
export const OPT = utils.unStringifyBigInts(CONSTANTS) as {
|
|
5
|
+
C: bigint[][];
|
|
6
|
+
S: bigint[][];
|
|
7
|
+
M: bigint[][][];
|
|
8
|
+
P: bigint[][][];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const N_ROUNDS_F = 8;
|
|
12
|
+
const N_ROUNDS_P = [
|
|
13
|
+
56, 57, 56, 60, 60, 63, 64, 63, 60, 66, 60, 65, 70, 60, 64, 68,
|
|
14
|
+
];
|
|
15
|
+
const SPONGE_INPUTS = 16;
|
|
16
|
+
const SPONGE_CHUNK_SIZE = 31;
|
|
17
|
+
|
|
18
|
+
const F = new F1Field(
|
|
19
|
+
Scalar.fromString(
|
|
20
|
+
'21888242871839275222246405745257275088548364400416034343698204186575808495617'
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
const pow5 = (a: bigint): bigint => F.mul(a, F.square(F.square(a)));
|
|
24
|
+
|
|
25
|
+
// circomlibjs Poseidon bn128
|
|
26
|
+
export class Poseidon {
|
|
27
|
+
static F = F;
|
|
28
|
+
|
|
29
|
+
static hash(inputs: bigint[]): bigint {
|
|
30
|
+
if (!(inputs.length > 0 && inputs.length <= N_ROUNDS_P.length)) {
|
|
31
|
+
throw new Error('Invalid inputs');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (inputs.some((i) => i < 0 || i >= F.p)) {
|
|
35
|
+
throw new Error(`One or more inputs are not in the field: ${F.p}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const t = inputs.length + 1;
|
|
39
|
+
const nRoundsF = N_ROUNDS_F;
|
|
40
|
+
const nRoundsP = N_ROUNDS_P[t - 2];
|
|
41
|
+
const C = OPT.C[t - 2]!;
|
|
42
|
+
const S = OPT.S[t - 2]!;
|
|
43
|
+
const M = OPT.M[t - 2]!;
|
|
44
|
+
const P = OPT.P[t - 2]!;
|
|
45
|
+
|
|
46
|
+
let state: bigint[] = [F.zero, ...inputs.map((a) => F.e(a) as bigint)];
|
|
47
|
+
|
|
48
|
+
state = state.map((a, i) => F.add(a, C[i]!));
|
|
49
|
+
|
|
50
|
+
for (let r = 0; r < nRoundsF / 2 - 1; r++) {
|
|
51
|
+
state = state.map((a) => pow5(a));
|
|
52
|
+
state = state.map((a, i) => F.add(a, C[(r + 1) * t + i]!));
|
|
53
|
+
state = state.map((_, i) =>
|
|
54
|
+
state.reduce((acc, a, j) => F.add(acc, F.mul(M[j]![i]!, a)), F.zero)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
state = state.map((a) => pow5(a));
|
|
58
|
+
state = state.map((a, i) => F.add(a, C[(nRoundsF / 2 - 1 + 1) * t + i]!));
|
|
59
|
+
state = state.map((_, i) =>
|
|
60
|
+
state.reduce((acc, a, j) => F.add(acc, F.mul(P[j]![i]!, a)), F.zero)
|
|
61
|
+
);
|
|
62
|
+
for (let r = 0; r < nRoundsP!; r++) {
|
|
63
|
+
state[0] = pow5(state[0]!);
|
|
64
|
+
state[0] = F.add(state[0]!, C[(nRoundsF / 2 + 1) * t + r]!);
|
|
65
|
+
|
|
66
|
+
const s0 = state.reduce((acc, a, j) => {
|
|
67
|
+
return F.add(acc, F.mul(S[(t * 2 - 1) * r + j]!, a!));
|
|
68
|
+
}, F.zero);
|
|
69
|
+
for (let k = 1; k < t; k++) {
|
|
70
|
+
state[k] = F.add(
|
|
71
|
+
state[k]!,
|
|
72
|
+
F.mul(state[0]!, S[(t * 2 - 1) * r + t + k - 1]!)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
state[0] = s0;
|
|
76
|
+
}
|
|
77
|
+
for (let r = 0; r < nRoundsF / 2 - 1; r++) {
|
|
78
|
+
state = state.map((a) => pow5(a));
|
|
79
|
+
state = state.map((a, i) =>
|
|
80
|
+
F.add(a, C[(nRoundsF / 2 + 1) * t + nRoundsP! + r * t + i]!)
|
|
81
|
+
);
|
|
82
|
+
state = state.map((_, i) =>
|
|
83
|
+
state.reduce((acc, a, j) => F.add(acc, F.mul(M[j]![i]!, a)), F.zero)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
state = state.map((a) => pow5(a));
|
|
87
|
+
state = state.map((_, i) =>
|
|
88
|
+
state.reduce((acc, a, j) => F.add(acc, F.mul(M[j]![i]!, a)), F.zero)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return F.normalize(state[0]!);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// HashBytes returns a sponge hash of a msg byte slice split into blocks of 31 bytes
|
|
95
|
+
static hashBytes(msg: Uint8Array): bigint {
|
|
96
|
+
return Poseidon.hashBytesX(msg, SPONGE_INPUTS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// hashBytesX returns a sponge hash of a msg byte slice split into blocks of 31 bytes
|
|
100
|
+
static hashBytesX(msg: Uint8Array, frameSize: number): bigint {
|
|
101
|
+
const inputs = new Array(frameSize).fill(BigInt(0));
|
|
102
|
+
let dirty = false;
|
|
103
|
+
let hash!: bigint;
|
|
104
|
+
|
|
105
|
+
let k = 0;
|
|
106
|
+
for (let i = 0; i < parseInt(`${msg.length / SPONGE_CHUNK_SIZE}`); i += 1) {
|
|
107
|
+
dirty = true;
|
|
108
|
+
inputs[k] = utils.beBuff2int(
|
|
109
|
+
msg.slice(SPONGE_CHUNK_SIZE * i, SPONGE_CHUNK_SIZE * (i + 1))
|
|
110
|
+
);
|
|
111
|
+
if (k === frameSize - 1) {
|
|
112
|
+
hash = Poseidon.hash(inputs);
|
|
113
|
+
dirty = false;
|
|
114
|
+
inputs[0] = hash;
|
|
115
|
+
inputs.fill(BigInt(0), 1, SPONGE_CHUNK_SIZE);
|
|
116
|
+
for (let j = 1; j < frameSize; j += 1) {
|
|
117
|
+
inputs[j] = BigInt(0);
|
|
118
|
+
}
|
|
119
|
+
k = 1;
|
|
120
|
+
} else {
|
|
121
|
+
k += 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (msg.length % SPONGE_CHUNK_SIZE != 0) {
|
|
126
|
+
const buff = new Uint8Array(SPONGE_CHUNK_SIZE);
|
|
127
|
+
const slice = msg.slice(
|
|
128
|
+
parseInt(`${msg.length / SPONGE_CHUNK_SIZE}`) * SPONGE_CHUNK_SIZE
|
|
129
|
+
);
|
|
130
|
+
slice.forEach((v, idx) => {
|
|
131
|
+
buff[idx] = v;
|
|
132
|
+
});
|
|
133
|
+
inputs[k] = utils.beBuff2int(buff);
|
|
134
|
+
dirty = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (dirty) {
|
|
138
|
+
// we haven't hashed something in the main sponge loop and need to do hash here
|
|
139
|
+
hash = Poseidon.hash(inputs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return hash;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// SpongeHashX returns a sponge hash of inputs using Poseidon with configurable frame size
|
|
146
|
+
static spongeHashX(inputs: bigint[], frameSize: number): bigint {
|
|
147
|
+
if (frameSize < 2 || frameSize > 16) {
|
|
148
|
+
throw new Error('incorrect frame size');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// not used frame default to zero
|
|
152
|
+
let frame = new Array(frameSize).fill(BigInt(0));
|
|
153
|
+
|
|
154
|
+
let dirty = false;
|
|
155
|
+
let hash!: bigint;
|
|
156
|
+
|
|
157
|
+
let k = 0;
|
|
158
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
159
|
+
dirty = true;
|
|
160
|
+
frame[k] = inputs[i];
|
|
161
|
+
if (k === frameSize - 1) {
|
|
162
|
+
hash = this.hash(frame);
|
|
163
|
+
dirty = false;
|
|
164
|
+
frame = new Array(frameSize).fill(BigInt(0));
|
|
165
|
+
frame[0] = hash;
|
|
166
|
+
k = 1;
|
|
167
|
+
} else {
|
|
168
|
+
k++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (dirty) {
|
|
173
|
+
// we haven't hashed something in the main sponge loop and need to do hash here
|
|
174
|
+
hash = this.hash(frame);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!hash) {
|
|
178
|
+
throw new Error('hash is undefined');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return hash;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
export const poseidon = Poseidon;
|
package/src/deposit.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
2
|
+
import { prove, verify } from './utils';
|
|
3
|
+
import invariant from 'tiny-invariant';
|
|
4
|
+
import { VortexKeypair } from './entities/keypair';
|
|
5
|
+
import { Utxo } from './entities/utxo';
|
|
6
|
+
import {
|
|
7
|
+
TREASURY_ADDRESS,
|
|
8
|
+
DEPOSIT_FEE_IN_BASIS_POINTS,
|
|
9
|
+
BASIS_POINTS,
|
|
10
|
+
BN254_FIELD_MODULUS,
|
|
11
|
+
} from './constants';
|
|
12
|
+
import { computeExtDataHash } from './utils/ext-data';
|
|
13
|
+
import { fromHex, normalizeSuiAddress } from '@mysten/sui/utils';
|
|
14
|
+
import { bytesToBigInt, reverseBytes, toProveInput } from './utils';
|
|
15
|
+
import { Proof, Action, DepositArgs } from './vortex.types';
|
|
16
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
17
|
+
|
|
18
|
+
export const deposit = async ({
|
|
19
|
+
tx = new Transaction(),
|
|
20
|
+
amount,
|
|
21
|
+
unspentUtxos = [],
|
|
22
|
+
vortex,
|
|
23
|
+
vortexKeypair,
|
|
24
|
+
merkleTree,
|
|
25
|
+
}: DepositArgs) => {
|
|
26
|
+
invariant(unspentUtxos.length <= 2, 'Unspent UTXOs must be at most 2');
|
|
27
|
+
invariant(
|
|
28
|
+
BN254_FIELD_MODULUS > amount,
|
|
29
|
+
'Amount must be less than field modulus'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const depositFee = (amount * DEPOSIT_FEE_IN_BASIS_POINTS) / BASIS_POINTS;
|
|
33
|
+
|
|
34
|
+
invariant(depositFee > 0n, 'Deposit fee must be greater than 0');
|
|
35
|
+
|
|
36
|
+
// Deposits we do not need a recipient, so we use a random one.
|
|
37
|
+
const randomRecipient = normalizeSuiAddress(
|
|
38
|
+
Ed25519Keypair.generate().toSuiAddress()
|
|
39
|
+
);
|
|
40
|
+
const randomVortexKeypair = VortexKeypair.generate();
|
|
41
|
+
|
|
42
|
+
// Determine input UTXOs
|
|
43
|
+
const inputUtxo0 =
|
|
44
|
+
unspentUtxos.length > 0 && unspentUtxos[0].amount > 0n
|
|
45
|
+
? unspentUtxos[0]
|
|
46
|
+
: new Utxo({
|
|
47
|
+
amount: 0n,
|
|
48
|
+
keypair: vortexKeypair,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const inputUtxo1 =
|
|
52
|
+
unspentUtxos.length > 1 && unspentUtxos[1].amount > 0n
|
|
53
|
+
? unspentUtxos[1]
|
|
54
|
+
: new Utxo({
|
|
55
|
+
amount: 0n,
|
|
56
|
+
keypair: vortexKeypair,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const amountMinusFrontendFee = amount - depositFee;
|
|
60
|
+
const nextIndex = await vortex.nextIndex();
|
|
61
|
+
|
|
62
|
+
// Calculate output UTXO0 amount: if using unspent UTXOs, include their amounts
|
|
63
|
+
const outputUtxo0 = new Utxo({
|
|
64
|
+
amount: amountMinusFrontendFee + inputUtxo0.amount + inputUtxo1.amount,
|
|
65
|
+
index: nextIndex,
|
|
66
|
+
keypair: vortexKeypair,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Dummy UTXO1 for obfuscation
|
|
70
|
+
const outputUtxo1 = new Utxo({
|
|
71
|
+
amount: 0n,
|
|
72
|
+
index: nextIndex + 1n,
|
|
73
|
+
keypair: vortexKeypair,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const [nullifier0, nullifier1, commitment0, commitment1] = [
|
|
77
|
+
inputUtxo0.nullifier(),
|
|
78
|
+
inputUtxo1.nullifier(),
|
|
79
|
+
outputUtxo0.commitment(),
|
|
80
|
+
outputUtxo1.commitment(),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const encryptedUtxo0 = VortexKeypair.encryptUtxoFor(
|
|
84
|
+
outputUtxo0.payload(),
|
|
85
|
+
vortexKeypair.encryptionKey
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// UTXO1 is a dummy UTXO for obfuscation, so we use a random Vortex keypair.
|
|
89
|
+
const encryptedUtxo1 = VortexKeypair.encryptUtxoFor(
|
|
90
|
+
outputUtxo1.payload(),
|
|
91
|
+
randomVortexKeypair.encryptionKey
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const extDataHash = computeExtDataHash({
|
|
95
|
+
recipient: randomRecipient,
|
|
96
|
+
value: amountMinusFrontendFee,
|
|
97
|
+
valueSign: true,
|
|
98
|
+
// No relayer for deposits
|
|
99
|
+
relayer: '0x0',
|
|
100
|
+
relayerFee: 0n,
|
|
101
|
+
encryptedOutput0: fromHex(encryptedUtxo0),
|
|
102
|
+
encryptedOutput1: fromHex(encryptedUtxo1),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const extDataHashBigInt = bytesToBigInt(reverseBytes(extDataHash));
|
|
106
|
+
|
|
107
|
+
// Prepare circuit input
|
|
108
|
+
const input = toProveInput({
|
|
109
|
+
merkleTree,
|
|
110
|
+
publicAmount: amountMinusFrontendFee,
|
|
111
|
+
extDataHash: extDataHashBigInt,
|
|
112
|
+
nullifier0,
|
|
113
|
+
nullifier1,
|
|
114
|
+
commitment0,
|
|
115
|
+
commitment1,
|
|
116
|
+
vortexKeypair,
|
|
117
|
+
inputUtxo0,
|
|
118
|
+
inputUtxo1,
|
|
119
|
+
outputUtxo0,
|
|
120
|
+
outputUtxo1,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const proofJson: string = prove(JSON.stringify(input));
|
|
124
|
+
|
|
125
|
+
const proof: Proof = JSON.parse(proofJson);
|
|
126
|
+
|
|
127
|
+
invariant(verify(proofJson), 'Proof verification failed');
|
|
128
|
+
|
|
129
|
+
const { extData, tx: tx2 } = vortex.newExtData({
|
|
130
|
+
tx,
|
|
131
|
+
recipient: randomRecipient,
|
|
132
|
+
value: amountMinusFrontendFee,
|
|
133
|
+
action: Action.Deposit,
|
|
134
|
+
relayer: normalizeSuiAddress('0x0'),
|
|
135
|
+
relayerFee: 0n,
|
|
136
|
+
encryptedOutput0: fromHex(encryptedUtxo0),
|
|
137
|
+
encryptedOutput1: fromHex(encryptedUtxo1),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const { proof: moveProof, tx: tx3 } = vortex.newProof({
|
|
141
|
+
tx: tx2,
|
|
142
|
+
proofPoints: fromHex('0x' + proof.proofSerializedHex),
|
|
143
|
+
root: merkleTree.root(),
|
|
144
|
+
publicValue: amountMinusFrontendFee,
|
|
145
|
+
action: Action.Deposit,
|
|
146
|
+
extDataHash: extDataHashBigInt,
|
|
147
|
+
inputNullifier0: nullifier0,
|
|
148
|
+
inputNullifier1: nullifier1,
|
|
149
|
+
outputCommitment0: commitment0,
|
|
150
|
+
outputCommitment1: commitment1,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const [suiCoinDeposit, suiCoinFee] = tx3.splitCoins(tx3.gas, [
|
|
154
|
+
tx3.pure.u64(amountMinusFrontendFee),
|
|
155
|
+
tx3.pure.u64(depositFee),
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
tx3.transferObjects([suiCoinFee], tx3.pure.address(TREASURY_ADDRESS));
|
|
159
|
+
|
|
160
|
+
const { tx: tx4 } = vortex.transact({
|
|
161
|
+
tx: tx3,
|
|
162
|
+
proof: moveProof,
|
|
163
|
+
extData: extData,
|
|
164
|
+
deposit: suiCoinDeposit,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return tx4;
|
|
168
|
+
};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { poseidon1, poseidon3 } from '../crypto';
|
|
2
|
+
import { BN254_FIELD_MODULUS, VORTEX_SIGNATURE_DOMAIN } from '../constants';
|
|
3
|
+
|
|
4
|
+
import { fromBase64, fromHex, toHex } from '@mysten/sui/utils';
|
|
5
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
6
|
+
import invariant from 'tiny-invariant';
|
|
7
|
+
import { randomBytes } from '@noble/ciphers/utils.js';
|
|
8
|
+
import { x25519 } from '@noble/curves/ed25519.js';
|
|
9
|
+
import { xsalsa20poly1305 } from '@noble/ciphers/salsa.js';
|
|
10
|
+
import { blake2b } from '@noble/hashes/blake2.js';
|
|
11
|
+
|
|
12
|
+
export interface UtxoPayload {
|
|
13
|
+
amount: bigint;
|
|
14
|
+
blinding: bigint;
|
|
15
|
+
index: bigint;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface EncryptedMessage {
|
|
19
|
+
version: string;
|
|
20
|
+
nonce: string; // base64
|
|
21
|
+
ephemPublicKey: string; // base64
|
|
22
|
+
ciphertext: string; // base64
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sui wallet signature function type
|
|
26
|
+
type SignMessageFn = (message: Uint8Array) => Promise<{
|
|
27
|
+
signature: string;
|
|
28
|
+
bytes: string;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
function packEncryptedMessage(encryptedMessage: EncryptedMessage): string {
|
|
32
|
+
const nonceBuf = Buffer.from(encryptedMessage.nonce, 'base64');
|
|
33
|
+
const ephemPublicKeyBuf = Buffer.from(
|
|
34
|
+
encryptedMessage.ephemPublicKey,
|
|
35
|
+
'base64'
|
|
36
|
+
);
|
|
37
|
+
const ciphertextBuf = Buffer.from(encryptedMessage.ciphertext, 'base64');
|
|
38
|
+
|
|
39
|
+
const messageBuff = Buffer.concat([
|
|
40
|
+
Buffer.alloc(24 - nonceBuf.length),
|
|
41
|
+
nonceBuf,
|
|
42
|
+
Buffer.alloc(32 - ephemPublicKeyBuf.length),
|
|
43
|
+
ephemPublicKeyBuf,
|
|
44
|
+
ciphertextBuf,
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
return '0x' + messageBuff.toString('hex');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function unpackEncryptedMessage(encryptedMessage: string): EncryptedMessage {
|
|
51
|
+
if (encryptedMessage.slice(0, 2) === '0x') {
|
|
52
|
+
encryptedMessage = encryptedMessage.slice(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const messageBuff = Buffer.from(encryptedMessage, 'hex');
|
|
56
|
+
|
|
57
|
+
const nonceBuf = messageBuff.subarray(0, 24);
|
|
58
|
+
const ephemPublicKeyBuf = messageBuff.subarray(24, 56);
|
|
59
|
+
const ciphertextBuf = messageBuff.subarray(56);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
version: 'x25519-xsalsa20-poly1305',
|
|
63
|
+
nonce: nonceBuf.toString('base64'),
|
|
64
|
+
ephemPublicKey: ephemPublicKeyBuf.toString('base64'),
|
|
65
|
+
ciphertext: ciphertextBuf.toString('base64'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class VortexKeypair {
|
|
70
|
+
privateKey: bigint;
|
|
71
|
+
publicKey: string;
|
|
72
|
+
encryptionKey: string; // base64 encoded X25519 public key
|
|
73
|
+
private x25519PrivateKey: Uint8Array | null;
|
|
74
|
+
|
|
75
|
+
constructor(privateKey: bigint) {
|
|
76
|
+
this.privateKey = privateKey % BN254_FIELD_MODULUS;
|
|
77
|
+
this.publicKey = poseidon1(this.privateKey).toString();
|
|
78
|
+
|
|
79
|
+
const privKeyBytes = this.#getPrivateKeyBytes();
|
|
80
|
+
this.x25519PrivateKey = blake2b(privKeyBytes, { dkLen: 32 });
|
|
81
|
+
|
|
82
|
+
invariant(
|
|
83
|
+
this.x25519PrivateKey.length === 32,
|
|
84
|
+
'X25519 private key must be 32 bytes'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const x25519PublicKey = x25519.getPublicKey(this.x25519PrivateKey);
|
|
88
|
+
this.encryptionKey = Buffer.from(x25519PublicKey).toString('base64');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static fromSuiPrivateKey(privateKey: string): VortexKeypair {
|
|
92
|
+
return new VortexKeypair(
|
|
93
|
+
BigInt('0x' + toHex(fromBase64(privateKey.replace('suiprivkey', ''))))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static generate(): VortexKeypair {
|
|
98
|
+
const keypair = Ed25519Keypair.generate();
|
|
99
|
+
return new VortexKeypair(
|
|
100
|
+
BigInt(
|
|
101
|
+
'0x' +
|
|
102
|
+
toHex(fromBase64(keypair.getSecretKey().replace('suiprivkey', '')))
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static fromString(str: string): VortexKeypair {
|
|
108
|
+
if (str.startsWith('0x')) {
|
|
109
|
+
str = str.slice(2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
invariant(str.length === 128, 'Invalid key length');
|
|
113
|
+
|
|
114
|
+
const keypair = Object.create(VortexKeypair.prototype);
|
|
115
|
+
keypair.privateKey = null;
|
|
116
|
+
keypair.x25519PrivateKey = null;
|
|
117
|
+
keypair.publicKey = BigInt('0x' + str.slice(0, 64)).toString();
|
|
118
|
+
keypair.encryptionKey = Buffer.from(str.slice(64, 128), 'hex').toString(
|
|
119
|
+
'base64'
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return keypair;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static async fromSuiWallet(
|
|
126
|
+
suiAddress: string,
|
|
127
|
+
signMessage: SignMessageFn
|
|
128
|
+
): Promise<VortexKeypair> {
|
|
129
|
+
// Domain-separated message to prevent signature reuse
|
|
130
|
+
const message = new TextEncoder().encode(
|
|
131
|
+
`Generate Vortex Keypair\n\nThis signature will be used to derive your Vortex privacy keypair.\n\nAddress: ${suiAddress}\nDomain: ${VORTEX_SIGNATURE_DOMAIN}`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const { signature } = await signMessage(message);
|
|
135
|
+
|
|
136
|
+
const signatureBytes = fromBase64(signature);
|
|
137
|
+
const seed = blake2b(signatureBytes, { dkLen: 32 });
|
|
138
|
+
|
|
139
|
+
const privateKey = BigInt('0x' + toHex(seed));
|
|
140
|
+
|
|
141
|
+
return new VortexKeypair(privateKey);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static encryptFor(bytes: Buffer, recipientEncryptionKey: string): string {
|
|
145
|
+
const ephemeralPrivateKey = randomBytes(32);
|
|
146
|
+
const ephemeralPublicKey = x25519.getPublicKey(ephemeralPrivateKey);
|
|
147
|
+
|
|
148
|
+
const recipientPublicKey = Buffer.from(recipientEncryptionKey, 'base64');
|
|
149
|
+
|
|
150
|
+
invariant(
|
|
151
|
+
recipientPublicKey.length === 32,
|
|
152
|
+
'Recipient public key must be 32 bytes'
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const sharedSecret = x25519.getSharedSecret(
|
|
156
|
+
ephemeralPrivateKey,
|
|
157
|
+
recipientPublicKey
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const nonce = randomBytes(24);
|
|
161
|
+
const cipher = xsalsa20poly1305(sharedSecret, nonce);
|
|
162
|
+
const ciphertext = cipher.encrypt(bytes);
|
|
163
|
+
|
|
164
|
+
const encryptedMessage: EncryptedMessage = {
|
|
165
|
+
version: 'x25519-xsalsa20-poly1305',
|
|
166
|
+
nonce: Buffer.from(nonce).toString('base64'),
|
|
167
|
+
ephemPublicKey: Buffer.from(ephemeralPublicKey).toString('base64'),
|
|
168
|
+
ciphertext: Buffer.from(ciphertext).toString('base64'),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return packEncryptedMessage(encryptedMessage);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
static encryptUtxoFor(
|
|
175
|
+
utxo: UtxoPayload,
|
|
176
|
+
recipientEncryptionKey: string
|
|
177
|
+
): string {
|
|
178
|
+
const utxoString = `${utxo.amount.toString()}|${utxo.blinding.toString()}|${utxo.index.toString()}`;
|
|
179
|
+
const bytes = Buffer.from(utxoString, 'utf8');
|
|
180
|
+
return VortexKeypair.encryptFor(bytes, recipientEncryptionKey);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
decryptUtxo(encryptedData: string): UtxoPayload {
|
|
184
|
+
const decrypted = this.#decrypt(encryptedData);
|
|
185
|
+
const decryptedStr = decrypted.toString('utf8');
|
|
186
|
+
const parts = decryptedStr.split('|');
|
|
187
|
+
|
|
188
|
+
invariant(parts.length === 3, 'Invalid UTXO format after decryption');
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
amount: BigInt(parts[0]),
|
|
192
|
+
blinding: BigInt(parts[1]),
|
|
193
|
+
index: BigInt(parts[2]),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
sign(commitment: bigint, merklePath: bigint): bigint {
|
|
198
|
+
invariant(this.privateKey !== null, 'Cannot sign without private key');
|
|
199
|
+
return poseidon3(this.privateKey, commitment, merklePath);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
toString(): string {
|
|
203
|
+
const pubkeyHex = BigInt(this.publicKey).toString(16).padStart(64, '0');
|
|
204
|
+
const encKeyHex = Buffer.from(this.encryptionKey, 'base64').toString('hex');
|
|
205
|
+
return '0x' + pubkeyHex + encKeyHex;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
address(): string {
|
|
209
|
+
return this.toString();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#decrypt(data: string): Buffer {
|
|
213
|
+
invariant(this.privateKey !== null, 'Cannot decrypt without private key');
|
|
214
|
+
invariant(
|
|
215
|
+
this.x25519PrivateKey !== null,
|
|
216
|
+
'Cannot decrypt without X25519 private key'
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const encryptedMessage = unpackEncryptedMessage(data);
|
|
220
|
+
|
|
221
|
+
const ephemeralPublicKey = Buffer.from(
|
|
222
|
+
encryptedMessage.ephemPublicKey,
|
|
223
|
+
'base64'
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
invariant(
|
|
227
|
+
ephemeralPublicKey.length === 32,
|
|
228
|
+
'Ephemeral public key must be 32 bytes'
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Derive shared secret
|
|
232
|
+
const sharedSecret = x25519.getSharedSecret(
|
|
233
|
+
this.x25519PrivateKey,
|
|
234
|
+
ephemeralPublicKey
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Decrypt using XSalsa20-Poly1305
|
|
238
|
+
const nonce = Buffer.from(encryptedMessage.nonce, 'base64');
|
|
239
|
+
const ciphertext = Buffer.from(encryptedMessage.ciphertext, 'base64');
|
|
240
|
+
|
|
241
|
+
const cipher = xsalsa20poly1305(sharedSecret, nonce);
|
|
242
|
+
const decrypted = cipher.decrypt(ciphertext);
|
|
243
|
+
|
|
244
|
+
return Buffer.from(decrypted);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#getPrivateKeyBytes(): Uint8Array {
|
|
248
|
+
const hex = this.privateKey.toString(16).padStart(64, '0');
|
|
249
|
+
const bytes = fromHex(hex);
|
|
250
|
+
|
|
251
|
+
// Ensure it's exactly 32 bytes
|
|
252
|
+
if (bytes.length < 32) {
|
|
253
|
+
const padded = new Uint8Array(32);
|
|
254
|
+
padded.set(bytes, 32 - bytes.length);
|
|
255
|
+
return padded;
|
|
256
|
+
} else if (bytes.length > 32) {
|
|
257
|
+
return bytes.slice(bytes.length - 32);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return bytes;
|
|
261
|
+
}
|
|
262
|
+
}
|