@veil-cash/sdk 0.4.0 → 0.6.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/README.md +130 -464
- package/SDK.md +365 -0
- package/dist/cli/index.cjs +2260 -955
- package/dist/index.cjs +561 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +486 -1
- package/dist/index.d.ts +486 -1
- package/dist/index.js +544 -27
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/skills/veil/SKILL.md +526 -0
- package/skills/veil/reference.md +231 -0
- package/src/abi.ts +172 -0
- package/src/addresses.ts +20 -2
- package/src/balance.ts +4 -4
- package/src/cli/commands/balance.ts +126 -38
- package/src/cli/commands/deposit.ts +136 -63
- package/src/cli/commands/init.ts +95 -72
- package/src/cli/commands/keypair.ts +34 -16
- package/src/cli/commands/private-balance.ts +37 -35
- package/src/cli/commands/queue-balance.ts +48 -36
- package/src/cli/commands/register.ts +67 -53
- package/src/cli/commands/status.ts +196 -70
- package/src/cli/commands/subaccount.ts +354 -0
- package/src/cli/commands/transfer.ts +62 -53
- package/src/cli/commands/withdraw.ts +32 -30
- package/src/cli/config.ts +85 -5
- package/src/cli/errors.ts +8 -0
- package/src/cli/index.ts +27 -5
- package/src/cli/output.ts +87 -0
- package/src/cli/wallet.ts +75 -16
- package/src/index.ts +41 -1
- package/src/prover.ts +3 -0
- package/src/relay.ts +36 -24
- package/src/subaccount.ts +476 -0
- package/src/types.ts +134 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPublicClient,
|
|
3
|
+
encodeFunctionData,
|
|
4
|
+
encodePacked,
|
|
5
|
+
formatEther,
|
|
6
|
+
formatUnits,
|
|
7
|
+
http,
|
|
8
|
+
isAddress,
|
|
9
|
+
keccak256,
|
|
10
|
+
parseEther,
|
|
11
|
+
parseUnits,
|
|
12
|
+
} from 'viem';
|
|
13
|
+
import { privateKeyToAccount, privateKeyToAddress } from 'viem/accounts';
|
|
14
|
+
import { base } from 'viem/chains';
|
|
15
|
+
import { FORWARDER_ABI, FORWARDER_FACTORY_ABI, ERC20_ABI } from './abi.js';
|
|
16
|
+
import { FORWARDER_CONTRACT_VERSION, getAddresses, getForwarderFactoryAddress } from './addresses.js';
|
|
17
|
+
import { getQueueBalance } from './balance.js';
|
|
18
|
+
import { Keypair } from './keypair.js';
|
|
19
|
+
import { postRelayJson } from './relay.js';
|
|
20
|
+
import type {
|
|
21
|
+
SubaccountAsset,
|
|
22
|
+
SubaccountDeployRequest,
|
|
23
|
+
SubaccountQueueStatus,
|
|
24
|
+
SubaccountRecoveryResult,
|
|
25
|
+
SubaccountRelayResult,
|
|
26
|
+
SubaccountSlot,
|
|
27
|
+
SubaccountStatusResult,
|
|
28
|
+
SubaccountSweepRequest,
|
|
29
|
+
SubaccountWithdrawTypedData,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
|
|
32
|
+
const SUBACCOUNT_CHILD_DOMAIN = 'veil-sua-child';
|
|
33
|
+
const SUBACCOUNT_SALT_DOMAIN = 'veil-sua-salt';
|
|
34
|
+
const ETH_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
|
|
35
|
+
const DEFAULT_WITHDRAW_DEADLINE_SECONDS = 3600n;
|
|
36
|
+
const DEFAULT_MAX_NONCE_SCAN = 100n;
|
|
37
|
+
export const MAX_SUBACCOUNT_SLOTS = 3;
|
|
38
|
+
|
|
39
|
+
function createBaseClient(rpcUrl?: string) {
|
|
40
|
+
return createPublicClient({
|
|
41
|
+
chain: base,
|
|
42
|
+
transport: http(rpcUrl),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertPrivateKey(value: string, label: string): asserts value is `0x${string}` {
|
|
47
|
+
if (!/^0x[a-fA-F0-9]{64}$/.test(value)) {
|
|
48
|
+
throw new Error(`${label} must be a 0x-prefixed 32-byte hex string`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeSlot(slot: number): number {
|
|
53
|
+
if (!Number.isInteger(slot) || slot < 0) {
|
|
54
|
+
throw new Error('slot must be a non-negative integer');
|
|
55
|
+
}
|
|
56
|
+
if (slot >= MAX_SUBACCOUNT_SLOTS) {
|
|
57
|
+
throw new Error(`slot must be less than ${MAX_SUBACCOUNT_SLOTS} (supported slots: 0-${MAX_SUBACCOUNT_SLOTS - 1})`);
|
|
58
|
+
}
|
|
59
|
+
return slot;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeAsset(asset: string): SubaccountAsset {
|
|
63
|
+
if (asset !== 'eth' && asset !== 'usdc') {
|
|
64
|
+
throw new Error('asset must be "eth" or "usdc"');
|
|
65
|
+
}
|
|
66
|
+
return asset;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeNonce(value: bigint | number): bigint {
|
|
70
|
+
const nonce = typeof value === 'bigint' ? value : BigInt(value);
|
|
71
|
+
if (nonce < 0n) {
|
|
72
|
+
throw new Error('nonce must be non-negative');
|
|
73
|
+
}
|
|
74
|
+
return nonce;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeDeadline(deadline?: bigint | number): bigint {
|
|
78
|
+
const nextDeadline =
|
|
79
|
+
deadline === undefined
|
|
80
|
+
? BigInt(Math.floor(Date.now() / 1000)) + DEFAULT_WITHDRAW_DEADLINE_SECONDS
|
|
81
|
+
: typeof deadline === 'bigint'
|
|
82
|
+
? deadline
|
|
83
|
+
: BigInt(deadline);
|
|
84
|
+
|
|
85
|
+
if (nextDeadline <= 0n) {
|
|
86
|
+
throw new Error('deadline must be greater than 0');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return nextDeadline;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function deriveSubaccountChildPrivateKey(
|
|
93
|
+
rootPrivateKey: string,
|
|
94
|
+
slot: number,
|
|
95
|
+
): `0x${string}` {
|
|
96
|
+
assertPrivateKey(rootPrivateKey, 'rootPrivateKey');
|
|
97
|
+
const normalizedSlot = normalizeSlot(slot);
|
|
98
|
+
return keccak256(
|
|
99
|
+
encodePacked(
|
|
100
|
+
['bytes32', 'string', 'uint256'],
|
|
101
|
+
[rootPrivateKey, SUBACCOUNT_CHILD_DOMAIN, BigInt(normalizedSlot)],
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function deriveSubaccountSalt(
|
|
107
|
+
rootPrivateKey: string,
|
|
108
|
+
slot: number,
|
|
109
|
+
): `0x${string}` {
|
|
110
|
+
assertPrivateKey(rootPrivateKey, 'rootPrivateKey');
|
|
111
|
+
const normalizedSlot = normalizeSlot(slot);
|
|
112
|
+
return keccak256(
|
|
113
|
+
encodePacked(
|
|
114
|
+
['bytes32', 'string', 'uint256'],
|
|
115
|
+
[rootPrivateKey, SUBACCOUNT_SALT_DOMAIN, BigInt(normalizedSlot)],
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function deriveSubaccountChildOwner(childPrivateKey: string): `0x${string}` {
|
|
121
|
+
assertPrivateKey(childPrivateKey, 'childPrivateKey');
|
|
122
|
+
return privateKeyToAddress(childPrivateKey);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function deriveSubaccountChildDepositKey(childPrivateKey: string): string {
|
|
126
|
+
assertPrivateKey(childPrivateKey, 'childPrivateKey');
|
|
127
|
+
return new Keypair(childPrivateKey).depositKey();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function predictSubaccountForwarder(options: {
|
|
131
|
+
salt: `0x${string}`;
|
|
132
|
+
childDepositKey: string;
|
|
133
|
+
childOwner: `0x${string}`;
|
|
134
|
+
rpcUrl?: string;
|
|
135
|
+
}): Promise<`0x${string}`> {
|
|
136
|
+
const publicClient = createBaseClient(options.rpcUrl);
|
|
137
|
+
return publicClient.readContract({
|
|
138
|
+
abi: FORWARDER_FACTORY_ABI,
|
|
139
|
+
address: getForwarderFactoryAddress(),
|
|
140
|
+
functionName: 'computeAddress',
|
|
141
|
+
args: [options.salt, options.childDepositKey as `0x${string}`, options.childOwner],
|
|
142
|
+
}) as Promise<`0x${string}`>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function deriveSubaccountSlot(options: {
|
|
146
|
+
rootPrivateKey: `0x${string}`;
|
|
147
|
+
slot: number;
|
|
148
|
+
rpcUrl?: string;
|
|
149
|
+
}): Promise<SubaccountSlot> {
|
|
150
|
+
const normalizedSlot = normalizeSlot(options.slot);
|
|
151
|
+
const childPrivateKey = deriveSubaccountChildPrivateKey(options.rootPrivateKey, normalizedSlot);
|
|
152
|
+
const salt = deriveSubaccountSalt(options.rootPrivateKey, normalizedSlot);
|
|
153
|
+
const childOwner = deriveSubaccountChildOwner(childPrivateKey);
|
|
154
|
+
const childDepositKey = deriveSubaccountChildDepositKey(childPrivateKey);
|
|
155
|
+
const forwarderAddress = await predictSubaccountForwarder({
|
|
156
|
+
salt,
|
|
157
|
+
childDepositKey,
|
|
158
|
+
childOwner,
|
|
159
|
+
rpcUrl: options.rpcUrl,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
slot: normalizedSlot,
|
|
164
|
+
childOwner,
|
|
165
|
+
childDepositKey,
|
|
166
|
+
salt,
|
|
167
|
+
forwarderAddress,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function isSubaccountForwarderDeployed(options: {
|
|
172
|
+
forwarderAddress: `0x${string}`;
|
|
173
|
+
rpcUrl?: string;
|
|
174
|
+
}): Promise<boolean> {
|
|
175
|
+
if (!isAddress(options.forwarderAddress)) {
|
|
176
|
+
throw new Error('forwarderAddress must be a valid Ethereum address');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const publicClient = createBaseClient(options.rpcUrl);
|
|
180
|
+
const code = await publicClient.getCode({ address: options.forwarderAddress });
|
|
181
|
+
return !!code && code !== '0x';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function deploySubaccountForwarder(
|
|
185
|
+
options: SubaccountDeployRequest,
|
|
186
|
+
): Promise<SubaccountRelayResult> {
|
|
187
|
+
const slot = await deriveSubaccountSlot({
|
|
188
|
+
rootPrivateKey: options.rootPrivateKey,
|
|
189
|
+
slot: options.slot,
|
|
190
|
+
rpcUrl: options.rpcUrl,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return postRelayJson<SubaccountRelayResult>(
|
|
194
|
+
'/stealth/deploy',
|
|
195
|
+
{
|
|
196
|
+
salt: slot.salt,
|
|
197
|
+
childDepositKey: slot.childDepositKey,
|
|
198
|
+
childOwner: slot.childOwner,
|
|
199
|
+
expectedForwarder: slot.forwarderAddress,
|
|
200
|
+
},
|
|
201
|
+
options.relayUrl,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function sweepSubaccountForwarder(
|
|
206
|
+
options: SubaccountSweepRequest,
|
|
207
|
+
): Promise<SubaccountRelayResult> {
|
|
208
|
+
const asset = normalizeAsset(options.asset);
|
|
209
|
+
if (!isAddress(options.forwarderAddress)) {
|
|
210
|
+
throw new Error('forwarderAddress must be a valid Ethereum address');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return postRelayJson<SubaccountRelayResult>(
|
|
214
|
+
'/stealth/sweep',
|
|
215
|
+
{
|
|
216
|
+
forwarder: options.forwarderAddress,
|
|
217
|
+
asset,
|
|
218
|
+
},
|
|
219
|
+
options.relayUrl,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toQueueStatus(
|
|
224
|
+
asset: SubaccountAsset,
|
|
225
|
+
result: Awaited<ReturnType<typeof getQueueBalance>>,
|
|
226
|
+
): SubaccountQueueStatus {
|
|
227
|
+
return {
|
|
228
|
+
asset,
|
|
229
|
+
queueBalance: result.queueBalance,
|
|
230
|
+
queueBalanceWei: result.queueBalanceWei,
|
|
231
|
+
pendingCount: result.pendingCount,
|
|
232
|
+
pendingDeposits: result.pendingDeposits,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function getSubaccountStatus(options: {
|
|
237
|
+
rootPrivateKey: `0x${string}`;
|
|
238
|
+
slot: number;
|
|
239
|
+
rpcUrl?: string;
|
|
240
|
+
}): Promise<SubaccountStatusResult> {
|
|
241
|
+
const slot = await deriveSubaccountSlot(options);
|
|
242
|
+
const publicClient = createBaseClient(options.rpcUrl);
|
|
243
|
+
const addresses = getAddresses();
|
|
244
|
+
|
|
245
|
+
const [deployed, ethWei, usdcWei, ethQueue, usdcQueue] = await Promise.all([
|
|
246
|
+
isSubaccountForwarderDeployed({
|
|
247
|
+
forwarderAddress: slot.forwarderAddress,
|
|
248
|
+
rpcUrl: options.rpcUrl,
|
|
249
|
+
}),
|
|
250
|
+
publicClient.getBalance({ address: slot.forwarderAddress }),
|
|
251
|
+
publicClient.readContract({
|
|
252
|
+
address: addresses.usdcToken,
|
|
253
|
+
abi: ERC20_ABI,
|
|
254
|
+
functionName: 'balanceOf',
|
|
255
|
+
args: [slot.forwarderAddress],
|
|
256
|
+
}) as Promise<bigint>,
|
|
257
|
+
getQueueBalance({
|
|
258
|
+
address: slot.forwarderAddress,
|
|
259
|
+
pool: 'eth',
|
|
260
|
+
rpcUrl: options.rpcUrl,
|
|
261
|
+
}),
|
|
262
|
+
getQueueBalance({
|
|
263
|
+
address: slot.forwarderAddress,
|
|
264
|
+
pool: 'usdc',
|
|
265
|
+
rpcUrl: options.rpcUrl,
|
|
266
|
+
}),
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
slot,
|
|
271
|
+
deployed,
|
|
272
|
+
balances: {
|
|
273
|
+
eth: {
|
|
274
|
+
balance: formatEther(ethWei),
|
|
275
|
+
balanceWei: ethWei.toString(),
|
|
276
|
+
},
|
|
277
|
+
usdc: {
|
|
278
|
+
balance: formatUnits(usdcWei, 6),
|
|
279
|
+
balanceWei: usdcWei.toString(),
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
queues: {
|
|
283
|
+
eth: toQueueStatus('eth', ethQueue),
|
|
284
|
+
usdc: toQueueStatus('usdc', usdcQueue),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const WITHDRAW_TYPES: SubaccountWithdrawTypedData['types'] = {
|
|
290
|
+
Withdraw: [
|
|
291
|
+
{ name: 'token', type: 'address' },
|
|
292
|
+
{ name: 'to', type: 'address' },
|
|
293
|
+
{ name: 'amount', type: 'uint256' },
|
|
294
|
+
{ name: 'nonce', type: 'uint256' },
|
|
295
|
+
{ name: 'deadline', type: 'uint256' },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export function buildSubaccountWithdrawTypedData(options: {
|
|
300
|
+
forwarderAddress: `0x${string}`;
|
|
301
|
+
token: `0x${string}`;
|
|
302
|
+
to: `0x${string}`;
|
|
303
|
+
amount: bigint;
|
|
304
|
+
nonce: bigint;
|
|
305
|
+
deadline: bigint;
|
|
306
|
+
}): SubaccountWithdrawTypedData {
|
|
307
|
+
if (!isAddress(options.forwarderAddress)) {
|
|
308
|
+
throw new Error('forwarderAddress must be a valid Ethereum address');
|
|
309
|
+
}
|
|
310
|
+
if (!isAddress(options.token)) {
|
|
311
|
+
throw new Error('token must be a valid Ethereum address');
|
|
312
|
+
}
|
|
313
|
+
if (!isAddress(options.to)) {
|
|
314
|
+
throw new Error('to must be a valid Ethereum address');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
domain: {
|
|
319
|
+
name: 'VeilForwarder',
|
|
320
|
+
version: FORWARDER_CONTRACT_VERSION,
|
|
321
|
+
chainId: getAddresses().chainId,
|
|
322
|
+
verifyingContract: options.forwarderAddress,
|
|
323
|
+
},
|
|
324
|
+
types: WITHDRAW_TYPES,
|
|
325
|
+
primaryType: 'Withdraw',
|
|
326
|
+
message: {
|
|
327
|
+
token: options.token,
|
|
328
|
+
to: options.to,
|
|
329
|
+
amount: options.amount,
|
|
330
|
+
nonce: options.nonce,
|
|
331
|
+
deadline: options.deadline,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function signSubaccountWithdraw(options: {
|
|
337
|
+
childPrivateKey: `0x${string}`;
|
|
338
|
+
typedData: SubaccountWithdrawTypedData;
|
|
339
|
+
}): Promise<`0x${string}`> {
|
|
340
|
+
assertPrivateKey(options.childPrivateKey, 'childPrivateKey');
|
|
341
|
+
const account = privateKeyToAccount(options.childPrivateKey);
|
|
342
|
+
return account.signTypedData({
|
|
343
|
+
domain: options.typedData.domain,
|
|
344
|
+
types: options.typedData.types,
|
|
345
|
+
primaryType: options.typedData.primaryType,
|
|
346
|
+
message: options.typedData.message,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function isSubaccountWithdrawNonceUsed(options: {
|
|
351
|
+
forwarderAddress: `0x${string}`;
|
|
352
|
+
nonce: bigint | number;
|
|
353
|
+
rpcUrl?: string;
|
|
354
|
+
}): Promise<boolean> {
|
|
355
|
+
if (!isAddress(options.forwarderAddress)) {
|
|
356
|
+
throw new Error('forwarderAddress must be a valid Ethereum address');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const publicClient = createBaseClient(options.rpcUrl);
|
|
360
|
+
try {
|
|
361
|
+
return await publicClient.readContract({
|
|
362
|
+
abi: FORWARDER_ABI,
|
|
363
|
+
address: options.forwarderAddress,
|
|
364
|
+
functionName: 'usedNonces',
|
|
365
|
+
args: [normalizeNonce(options.nonce)],
|
|
366
|
+
}) as boolean;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (String(error).includes('returned no data')) {
|
|
369
|
+
throw new Error('Subaccount forwarder is not deployed');
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function findNextSubaccountWithdrawNonce(options: {
|
|
376
|
+
forwarderAddress: `0x${string}`;
|
|
377
|
+
startNonce?: bigint | number;
|
|
378
|
+
maxScan?: bigint | number;
|
|
379
|
+
rpcUrl?: string;
|
|
380
|
+
}): Promise<bigint> {
|
|
381
|
+
const startNonce = normalizeNonce(options.startNonce ?? 0n);
|
|
382
|
+
const maxScan = normalizeNonce(options.maxScan ?? DEFAULT_MAX_NONCE_SCAN);
|
|
383
|
+
const limit = startNonce + maxScan;
|
|
384
|
+
let nonce = startNonce;
|
|
385
|
+
|
|
386
|
+
while (
|
|
387
|
+
await isSubaccountWithdrawNonceUsed({
|
|
388
|
+
forwarderAddress: options.forwarderAddress,
|
|
389
|
+
nonce,
|
|
390
|
+
rpcUrl: options.rpcUrl,
|
|
391
|
+
})
|
|
392
|
+
) {
|
|
393
|
+
nonce += 1n;
|
|
394
|
+
if (nonce > limit) {
|
|
395
|
+
throw new Error('Unable to find an unused withdraw nonce within the scan limit');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return nonce;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function buildSubaccountRecoveryTx(options: {
|
|
403
|
+
rootPrivateKey: `0x${string}`;
|
|
404
|
+
slot: number;
|
|
405
|
+
asset: SubaccountAsset;
|
|
406
|
+
to: `0x${string}`;
|
|
407
|
+
amount: string;
|
|
408
|
+
nonce?: bigint | number;
|
|
409
|
+
deadline?: bigint | number;
|
|
410
|
+
rpcUrl?: string;
|
|
411
|
+
}): Promise<SubaccountRecoveryResult> {
|
|
412
|
+
if (!isAddress(options.to)) {
|
|
413
|
+
throw new Error('to must be a valid Ethereum address');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const asset = normalizeAsset(options.asset);
|
|
417
|
+
const slot = await deriveSubaccountSlot({
|
|
418
|
+
rootPrivateKey: options.rootPrivateKey,
|
|
419
|
+
slot: options.slot,
|
|
420
|
+
rpcUrl: options.rpcUrl,
|
|
421
|
+
});
|
|
422
|
+
const deployed = await isSubaccountForwarderDeployed({
|
|
423
|
+
forwarderAddress: slot.forwarderAddress,
|
|
424
|
+
rpcUrl: options.rpcUrl,
|
|
425
|
+
});
|
|
426
|
+
if (!deployed) {
|
|
427
|
+
throw new Error('Subaccount forwarder is not deployed');
|
|
428
|
+
}
|
|
429
|
+
const childPrivateKey = deriveSubaccountChildPrivateKey(options.rootPrivateKey, options.slot);
|
|
430
|
+
const tokenAddress =
|
|
431
|
+
asset === 'eth' ? ETH_ADDRESS : getAddresses().usdcToken;
|
|
432
|
+
const amountWei =
|
|
433
|
+
asset === 'eth'
|
|
434
|
+
? parseEther(options.amount)
|
|
435
|
+
: parseUnits(options.amount, 6);
|
|
436
|
+
const nonce =
|
|
437
|
+
options.nonce === undefined
|
|
438
|
+
? await findNextSubaccountWithdrawNonce({
|
|
439
|
+
forwarderAddress: slot.forwarderAddress,
|
|
440
|
+
rpcUrl: options.rpcUrl,
|
|
441
|
+
})
|
|
442
|
+
: normalizeNonce(options.nonce);
|
|
443
|
+
const deadline = normalizeDeadline(options.deadline);
|
|
444
|
+
const typedData = buildSubaccountWithdrawTypedData({
|
|
445
|
+
forwarderAddress: slot.forwarderAddress,
|
|
446
|
+
token: tokenAddress,
|
|
447
|
+
to: options.to,
|
|
448
|
+
amount: amountWei,
|
|
449
|
+
nonce,
|
|
450
|
+
deadline,
|
|
451
|
+
});
|
|
452
|
+
const signature = await signSubaccountWithdraw({
|
|
453
|
+
childPrivateKey,
|
|
454
|
+
typedData,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
transaction: {
|
|
459
|
+
to: slot.forwarderAddress,
|
|
460
|
+
data: encodeFunctionData({
|
|
461
|
+
abi: FORWARDER_ABI,
|
|
462
|
+
functionName: 'withdraw',
|
|
463
|
+
args: [tokenAddress, options.to, amountWei, nonce, deadline, signature],
|
|
464
|
+
}),
|
|
465
|
+
},
|
|
466
|
+
forwarderAddress: slot.forwarderAddress,
|
|
467
|
+
asset,
|
|
468
|
+
amount: options.amount,
|
|
469
|
+
amountWei: amountWei.toString(),
|
|
470
|
+
nonce: nonce.toString(),
|
|
471
|
+
deadline: deadline.toString(),
|
|
472
|
+
recipient: options.to,
|
|
473
|
+
tokenAddress,
|
|
474
|
+
signature,
|
|
475
|
+
};
|
|
476
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface NetworkAddresses {
|
|
|
27
27
|
usdcPool: `0x${string}`;
|
|
28
28
|
usdcQueue: `0x${string}`;
|
|
29
29
|
usdcToken: `0x${string}`;
|
|
30
|
+
forwarderFactory: `0x${string}`;
|
|
30
31
|
chainId: number;
|
|
31
32
|
relayUrl: string;
|
|
32
33
|
}
|
|
@@ -308,3 +309,136 @@ export interface UtxoSelectionResult {
|
|
|
308
309
|
/** Change amount to return to sender (wei) */
|
|
309
310
|
changeAmount: bigint;
|
|
310
311
|
}
|
|
312
|
+
|
|
313
|
+
// =============================================================================
|
|
314
|
+
// Subaccount Types
|
|
315
|
+
// =============================================================================
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Supported subaccount assets
|
|
319
|
+
*/
|
|
320
|
+
export type SubaccountAsset = 'eth' | 'usdc';
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Deterministically derived subaccount slot metadata
|
|
324
|
+
*/
|
|
325
|
+
export interface SubaccountSlot {
|
|
326
|
+
slot: number;
|
|
327
|
+
childOwner: `0x${string}`;
|
|
328
|
+
childDepositKey: string;
|
|
329
|
+
salt: `0x${string}`;
|
|
330
|
+
forwarderAddress: `0x${string}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Relay-backed forwarder deploy request
|
|
335
|
+
*/
|
|
336
|
+
export interface SubaccountDeployRequest {
|
|
337
|
+
rootPrivateKey: `0x${string}`;
|
|
338
|
+
slot: number;
|
|
339
|
+
relayUrl?: string;
|
|
340
|
+
rpcUrl?: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Relay-backed forwarder sweep request
|
|
345
|
+
*/
|
|
346
|
+
export interface SubaccountSweepRequest {
|
|
347
|
+
forwarderAddress: `0x${string}`;
|
|
348
|
+
asset: SubaccountAsset;
|
|
349
|
+
relayUrl?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Relay response for subaccount deploy/sweep operations
|
|
354
|
+
*/
|
|
355
|
+
export interface SubaccountRelayResult {
|
|
356
|
+
success: boolean;
|
|
357
|
+
transactionHash: string;
|
|
358
|
+
blockNumber: string;
|
|
359
|
+
gasUsed: string;
|
|
360
|
+
status: string;
|
|
361
|
+
network: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Human-readable and wei-denominated balance
|
|
366
|
+
*/
|
|
367
|
+
export interface SubaccountAssetBalance {
|
|
368
|
+
balance: string;
|
|
369
|
+
balanceWei: string;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Forwarder wallet balances for both supported assets
|
|
374
|
+
*/
|
|
375
|
+
export interface SubaccountBalances {
|
|
376
|
+
eth: SubaccountAssetBalance;
|
|
377
|
+
usdc: SubaccountAssetBalance;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Queue status for a specific asset
|
|
382
|
+
*/
|
|
383
|
+
export interface SubaccountQueueStatus {
|
|
384
|
+
asset: SubaccountAsset;
|
|
385
|
+
queueBalance: string;
|
|
386
|
+
queueBalanceWei: string;
|
|
387
|
+
pendingCount: number;
|
|
388
|
+
pendingDeposits: PendingDeposit[];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Combined subaccount status result
|
|
393
|
+
*/
|
|
394
|
+
export interface SubaccountStatusResult {
|
|
395
|
+
slot: SubaccountSlot;
|
|
396
|
+
deployed: boolean;
|
|
397
|
+
balances: SubaccountBalances;
|
|
398
|
+
queues: {
|
|
399
|
+
eth: SubaccountQueueStatus;
|
|
400
|
+
usdc: SubaccountQueueStatus;
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Typed data for forwarder withdraw signing
|
|
406
|
+
*/
|
|
407
|
+
export interface SubaccountWithdrawTypedData {
|
|
408
|
+
domain: {
|
|
409
|
+
name: 'VeilForwarder';
|
|
410
|
+
version: string;
|
|
411
|
+
chainId: number;
|
|
412
|
+
verifyingContract: `0x${string}`;
|
|
413
|
+
};
|
|
414
|
+
types: {
|
|
415
|
+
Withdraw: Array<{
|
|
416
|
+
name: 'token' | 'to' | 'amount' | 'nonce' | 'deadline';
|
|
417
|
+
type: 'address' | 'uint256';
|
|
418
|
+
}>;
|
|
419
|
+
};
|
|
420
|
+
primaryType: 'Withdraw';
|
|
421
|
+
message: {
|
|
422
|
+
token: `0x${string}`;
|
|
423
|
+
to: `0x${string}`;
|
|
424
|
+
amount: bigint;
|
|
425
|
+
nonce: bigint;
|
|
426
|
+
deadline: bigint;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Built recovery transaction and signing metadata
|
|
432
|
+
*/
|
|
433
|
+
export interface SubaccountRecoveryResult {
|
|
434
|
+
transaction: TransactionData;
|
|
435
|
+
forwarderAddress: `0x${string}`;
|
|
436
|
+
asset: SubaccountAsset;
|
|
437
|
+
amount: string;
|
|
438
|
+
amountWei: string;
|
|
439
|
+
nonce: string;
|
|
440
|
+
deadline: string;
|
|
441
|
+
recipient: `0x${string}`;
|
|
442
|
+
tokenAddress: `0x${string}`;
|
|
443
|
+
signature: `0x${string}`;
|
|
444
|
+
}
|