@veil-cash/sdk 0.5.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 +31 -2
- package/SDK.md +55 -1
- package/dist/cli/index.cjs +806 -22
- package/dist/index.cjs +554 -20
- 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 +537 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/abi.ts +172 -0
- package/src/addresses.ts +14 -0
- package/src/cli/commands/subaccount.ts +354 -0
- package/src/cli/errors.ts +4 -0
- package/src/cli/index.ts +5 -1
- package/src/cli/wallet.ts +2 -2
- package/src/index.ts +35 -0
- package/src/relay.ts +36 -24
- package/src/subaccount.ts +476 -0
- package/src/types.ts +134 -0
package/src/index.ts
CHANGED
|
@@ -100,8 +100,10 @@ export type { ProofInput } from './prover.js';
|
|
|
100
100
|
// Addresses and config
|
|
101
101
|
export {
|
|
102
102
|
ADDRESSES,
|
|
103
|
+
FORWARDER_CONTRACT_VERSION,
|
|
103
104
|
POOL_CONFIG,
|
|
104
105
|
getAddresses,
|
|
106
|
+
getForwarderFactoryAddress,
|
|
105
107
|
getPoolAddress,
|
|
106
108
|
getQueueAddress,
|
|
107
109
|
getRelayUrl,
|
|
@@ -119,10 +121,32 @@ export {
|
|
|
119
121
|
export {
|
|
120
122
|
ENTRY_ABI,
|
|
121
123
|
ERC20_ABI,
|
|
124
|
+
FORWARDER_ABI,
|
|
125
|
+
FORWARDER_FACTORY_ABI,
|
|
122
126
|
QUEUE_ABI,
|
|
123
127
|
POOL_ABI,
|
|
124
128
|
} from './abi.js';
|
|
125
129
|
|
|
130
|
+
// Subaccount functions
|
|
131
|
+
export {
|
|
132
|
+
MAX_SUBACCOUNT_SLOTS,
|
|
133
|
+
deriveSubaccountChildPrivateKey,
|
|
134
|
+
deriveSubaccountSalt,
|
|
135
|
+
deriveSubaccountChildOwner,
|
|
136
|
+
deriveSubaccountChildDepositKey,
|
|
137
|
+
deriveSubaccountSlot,
|
|
138
|
+
predictSubaccountForwarder,
|
|
139
|
+
isSubaccountForwarderDeployed,
|
|
140
|
+
deploySubaccountForwarder,
|
|
141
|
+
sweepSubaccountForwarder,
|
|
142
|
+
getSubaccountStatus,
|
|
143
|
+
buildSubaccountWithdrawTypedData,
|
|
144
|
+
signSubaccountWithdraw,
|
|
145
|
+
isSubaccountWithdrawNonceUsed,
|
|
146
|
+
findNextSubaccountWithdrawNonce,
|
|
147
|
+
buildSubaccountRecoveryTx,
|
|
148
|
+
} from './subaccount.js';
|
|
149
|
+
|
|
126
150
|
// Utilities
|
|
127
151
|
export {
|
|
128
152
|
poseidonHash,
|
|
@@ -166,4 +190,15 @@ export type {
|
|
|
166
190
|
WithdrawResult,
|
|
167
191
|
TransferResult,
|
|
168
192
|
UtxoSelectionResult,
|
|
193
|
+
SubaccountAsset,
|
|
194
|
+
SubaccountSlot,
|
|
195
|
+
SubaccountDeployRequest,
|
|
196
|
+
SubaccountSweepRequest,
|
|
197
|
+
SubaccountRelayResult,
|
|
198
|
+
SubaccountAssetBalance,
|
|
199
|
+
SubaccountBalances,
|
|
200
|
+
SubaccountQueueStatus,
|
|
201
|
+
SubaccountStatusResult,
|
|
202
|
+
SubaccountWithdrawTypedData,
|
|
203
|
+
SubaccountRecoveryResult,
|
|
169
204
|
} from './types.js';
|
package/src/relay.ts
CHANGED
|
@@ -50,6 +50,35 @@ export class RelayError extends Error {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export async function postRelayJson<T>(
|
|
54
|
+
endpoint: string,
|
|
55
|
+
body: unknown,
|
|
56
|
+
relayUrl?: string,
|
|
57
|
+
): Promise<T> {
|
|
58
|
+
const url = relayUrl || getRelayUrl();
|
|
59
|
+
const response = await fetch(`${url}${endpoint}`, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify(body),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const errorData = data as RelayErrorResponse;
|
|
71
|
+
throw new RelayError(
|
|
72
|
+
errorData.error || errorData.message || 'Relay request failed',
|
|
73
|
+
response.status,
|
|
74
|
+
errorData.retryAfter,
|
|
75
|
+
errorData.network,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return data as T;
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
/**
|
|
54
83
|
* Submit a withdrawal or transfer to the relay service
|
|
55
84
|
*
|
|
@@ -107,34 +136,17 @@ export async function submitRelay(options: SubmitRelayOptions): Promise<RelayRes
|
|
|
107
136
|
}
|
|
108
137
|
|
|
109
138
|
const relayUrl = customRelayUrl || getRelayUrl();
|
|
110
|
-
const endpoint =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
headers: {
|
|
115
|
-
'Content-Type': 'application/json',
|
|
116
|
-
},
|
|
117
|
-
body: JSON.stringify({
|
|
139
|
+
const endpoint = `/relay/${pool}`;
|
|
140
|
+
return postRelayJson<RelayResponse>(
|
|
141
|
+
endpoint,
|
|
142
|
+
{
|
|
118
143
|
type,
|
|
119
144
|
proofArgs,
|
|
120
145
|
extData,
|
|
121
146
|
metadata,
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const data = await response.json();
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
const errorData = data as RelayErrorResponse;
|
|
129
|
-
throw new RelayError(
|
|
130
|
-
errorData.error || errorData.message || 'Relay request failed',
|
|
131
|
-
response.status,
|
|
132
|
-
errorData.retryAfter,
|
|
133
|
-
errorData.network
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return data as RelayResponse;
|
|
147
|
+
},
|
|
148
|
+
relayUrl,
|
|
149
|
+
);
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
/**
|
|
@@ -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
|
+
}
|