@gauntlet-xyz/sdk 3.0.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/api/activity.d.ts +3 -1
- package/dist/api/activity.d.ts.map +1 -1
- package/dist/api/activityRows.d.ts +11 -0
- package/dist/api/activityRows.d.ts.map +1 -0
- package/dist/api/caip.d.ts.map +1 -1
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/decimal.d.ts.map +1 -1
- package/dist/api/http.d.ts.map +1 -1
- package/dist/api/index.js +7 -8
- package/dist/api/positionHistory.d.ts.map +1 -1
- package/dist/attribution/index.d.ts +5 -2
- package/dist/attribution/index.d.ts.map +1 -1
- package/dist/{chunk-HC2BS7MK.js → chunk-JJOLVL5V.js} +113 -6
- package/dist/chunk-JJOLVL5V.js.map +7 -0
- package/dist/{chunk-S2TTA45N.js → chunk-JRSNLXFY.js} +2 -2
- package/dist/{chunk-SU6JAKIA.js → chunk-MEKISE2T.js} +131 -47
- package/dist/chunk-MEKISE2T.js.map +7 -0
- package/dist/{chunk-7TA2OJUK.js → chunk-YQXTLIJA.js} +209 -96
- package/dist/chunk-YQXTLIJA.js.map +7 -0
- package/dist/cjs/api/activity.d.ts +3 -1
- package/dist/cjs/api/activity.d.ts.map +1 -1
- package/dist/cjs/api/activity.js +14 -12
- package/dist/cjs/api/activity.js.map +2 -2
- package/dist/cjs/api/activityRows.d.ts +11 -0
- package/dist/cjs/api/activityRows.d.ts.map +1 -0
- package/dist/cjs/api/activityRows.js +139 -0
- package/dist/cjs/api/activityRows.js.map +7 -0
- package/dist/cjs/api/caip.d.ts.map +1 -1
- package/dist/cjs/api/caip.js +3 -1
- package/dist/cjs/api/caip.js.map +2 -2
- package/dist/cjs/api/client.d.ts.map +1 -1
- package/dist/cjs/api/client.js +29 -1
- package/dist/cjs/api/client.js.map +2 -2
- package/dist/cjs/api/decimal.d.ts.map +1 -1
- package/dist/cjs/api/decimal.js +1 -0
- package/dist/cjs/api/decimal.js.map +2 -2
- package/dist/cjs/api/http.d.ts.map +1 -1
- package/dist/cjs/api/http.js +1 -0
- package/dist/cjs/api/http.js.map +2 -2
- package/dist/cjs/api/positionHistory.d.ts.map +1 -1
- package/dist/cjs/api/positionHistory.js +17 -4
- package/dist/cjs/api/positionHistory.js.map +2 -2
- package/dist/cjs/attribution/index.d.ts +5 -2
- package/dist/cjs/attribution/index.d.ts.map +1 -1
- package/dist/cjs/attribution/index.js +3 -1
- package/dist/cjs/attribution/index.js.map +2 -2
- package/dist/cjs/evm/adapters/aera.d.ts.map +1 -1
- package/dist/cjs/evm/adapters/aera.js +16 -7
- package/dist/cjs/evm/adapters/aera.js.map +2 -2
- package/dist/cjs/evm/adapters/types.d.ts +1 -0
- package/dist/cjs/evm/adapters/types.d.ts.map +1 -1
- package/dist/cjs/evm/adapters/types.js.map +1 -1
- package/dist/cjs/evm/aeraContracts.d.ts.map +1 -1
- package/dist/cjs/evm/aeraContracts.js +21 -11
- package/dist/cjs/evm/aeraContracts.js.map +2 -2
- package/dist/cjs/evm/deposit.d.ts +3 -2
- package/dist/cjs/evm/deposit.d.ts.map +1 -1
- package/dist/cjs/evm/deposit.js +40 -10
- package/dist/cjs/evm/deposit.js.map +2 -2
- package/dist/cjs/evm/depositQuote.d.ts.map +1 -1
- package/dist/cjs/evm/depositQuote.js +1 -0
- package/dist/cjs/evm/depositQuote.js.map +2 -2
- package/dist/cjs/evm/depositReceiverApproval.d.ts.map +1 -1
- package/dist/cjs/evm/depositReceiverApproval.js +14 -9
- package/dist/cjs/evm/depositReceiverApproval.js.map +2 -2
- package/dist/cjs/evm/userCurrentBalance.d.ts +15 -1
- package/dist/cjs/evm/userCurrentBalance.d.ts.map +1 -1
- package/dist/cjs/evm/userCurrentBalance.js +94 -51
- package/dist/cjs/evm/userCurrentBalance.js.map +2 -2
- package/dist/cjs/evm/withdraw.d.ts +2 -0
- package/dist/cjs/evm/withdraw.d.ts.map +1 -1
- package/dist/cjs/evm/withdraw.js +14 -1
- package/dist/cjs/evm/withdraw.js.map +2 -2
- package/dist/cjs/evm/withdrawQuote.d.ts.map +1 -1
- package/dist/cjs/evm/withdrawQuote.js +1 -0
- package/dist/cjs/evm/withdrawQuote.js.map +2 -2
- package/dist/evm/adapters/aera.d.ts.map +1 -1
- package/dist/evm/adapters/types.d.ts +1 -0
- package/dist/evm/adapters/types.d.ts.map +1 -1
- package/dist/evm/aeraContracts.d.ts.map +1 -1
- package/dist/evm/deposit.d.ts +3 -2
- package/dist/evm/deposit.d.ts.map +1 -1
- package/dist/evm/depositQuote.d.ts.map +1 -1
- package/dist/evm/depositReceiverApproval.d.ts.map +1 -1
- package/dist/evm/index.js +6 -8
- package/dist/evm/userCurrentBalance.d.ts +15 -1
- package/dist/evm/userCurrentBalance.d.ts.map +1 -1
- package/dist/evm/withdraw.d.ts +2 -0
- package/dist/evm/withdraw.d.ts.map +1 -1
- package/dist/evm/withdrawQuote.d.ts.map +1 -1
- package/dist/index.js +11 -13
- package/dist/privy/index.js +2 -2
- package/package.json +1 -1
- package/src/api/activity.ts +18 -15
- package/src/api/activityRows.ts +143 -0
- package/src/api/caip.ts +3 -1
- package/src/api/client.ts +36 -1
- package/src/api/decimal.ts +1 -0
- package/src/api/http.ts +1 -0
- package/src/api/positionHistory.ts +20 -5
- package/src/attribution/index.ts +6 -2
- package/src/evm/adapters/aera.ts +26 -8
- package/src/evm/adapters/types.ts +1 -0
- package/src/evm/aeraContracts.ts +29 -14
- package/src/evm/deposit.ts +43 -12
- package/src/evm/depositQuote.ts +1 -0
- package/src/evm/depositReceiverApproval.ts +14 -9
- package/src/evm/userCurrentBalance.ts +144 -60
- package/src/evm/withdraw.ts +20 -1
- package/src/evm/withdrawQuote.ts +1 -0
- package/dist/chunk-7TA2OJUK.js.map +0 -7
- package/dist/chunk-HC2BS7MK.js.map +0 -7
- package/dist/chunk-STIV2CC4.js +0 -57
- package/dist/chunk-STIV2CC4.js.map +0 -7
- package/dist/chunk-SU6JAKIA.js.map +0 -7
- /package/dist/{chunk-S2TTA45N.js.map → chunk-JRSNLXFY.js.map} +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { GauntletSDKError } from '../errors';
|
|
2
|
+
import type { UserActivity } from './client';
|
|
3
|
+
import { decimalToBigInt, sharesToBigInt } from './decimal';
|
|
4
|
+
|
|
5
|
+
export function isPending(row: UserActivity): boolean {
|
|
6
|
+
return row.type === 'deposit_pending' || row.type === 'withdraw_pending';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function requireSameToken(leftRow: UserActivity, rightRow: UserActivity): void {
|
|
10
|
+
// The vault ID includes the chain ID; TokenRef has no chain field.
|
|
11
|
+
if (leftRow.vault_id.split(':')[0] !== rightRow.vault_id.split(':')[0]) {
|
|
12
|
+
throw new GauntletSDKError('Activity rows contain assets on different chains');
|
|
13
|
+
}
|
|
14
|
+
const left = leftRow.assets_delta.token;
|
|
15
|
+
const right = rightRow.assets_delta.token;
|
|
16
|
+
if (
|
|
17
|
+
left != null &&
|
|
18
|
+
right != null &&
|
|
19
|
+
(left.address.toLowerCase() !== right.address.toLowerCase() ||
|
|
20
|
+
(left.decimals ?? null) !== (right.decimals ?? null))
|
|
21
|
+
) {
|
|
22
|
+
throw new GauntletSDKError('Activity rows contain different asset denominations');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateActivityRow(row: UserActivity): void {
|
|
27
|
+
const shares = sharesToBigInt(row.shares_delta);
|
|
28
|
+
const assets = decimalToBigInt(row.assets_delta.native, 18);
|
|
29
|
+
if (
|
|
30
|
+
!Number.isSafeInteger(row.block_number) ||
|
|
31
|
+
row.block_number < 0 ||
|
|
32
|
+
!Number.isSafeInteger(row.block_timestamp) ||
|
|
33
|
+
row.block_timestamp < 0 ||
|
|
34
|
+
typeof row.vault_id !== 'string' ||
|
|
35
|
+
row.vault_id.length === 0 ||
|
|
36
|
+
typeof row.tx_hash !== 'string' ||
|
|
37
|
+
row.tx_hash.length === 0 ||
|
|
38
|
+
(row.request_hash != null &&
|
|
39
|
+
(typeof row.request_hash !== 'string' || row.request_hash.length === 0))
|
|
40
|
+
) {
|
|
41
|
+
throw new GauntletSDKError('The activity row has invalid event identifiers');
|
|
42
|
+
}
|
|
43
|
+
const token = row.assets_delta.token;
|
|
44
|
+
if (token != null && token.decimals != null) {
|
|
45
|
+
if (!Number.isInteger(token.decimals) || token.decimals < 0 || token.decimals > 255) {
|
|
46
|
+
throw new GauntletSDKError('The activity token has invalid decimals');
|
|
47
|
+
}
|
|
48
|
+
decimalToBigInt(row.assets_delta.native, token.decimals);
|
|
49
|
+
}
|
|
50
|
+
let valid: boolean;
|
|
51
|
+
switch (row.type) {
|
|
52
|
+
case 'deposit':
|
|
53
|
+
valid = shares >= 0n && assets >= 0n && (row.request_hash == null || assets === 0n);
|
|
54
|
+
break;
|
|
55
|
+
case 'deposit_pending':
|
|
56
|
+
valid = shares === 0n && assets > 0n && row.request_hash != null;
|
|
57
|
+
break;
|
|
58
|
+
case 'deposit_refunded':
|
|
59
|
+
valid = shares === 0n && assets < 0n && row.request_hash != null;
|
|
60
|
+
break;
|
|
61
|
+
case 'withdraw':
|
|
62
|
+
valid = shares <= 0n && assets <= 0n && (row.request_hash == null || shares === 0n);
|
|
63
|
+
break;
|
|
64
|
+
case 'withdraw_pending':
|
|
65
|
+
valid = shares < 0n && assets === 0n && row.request_hash != null;
|
|
66
|
+
break;
|
|
67
|
+
case 'withdraw_refunded':
|
|
68
|
+
valid = shares > 0n && assets === 0n && row.request_hash != null;
|
|
69
|
+
break;
|
|
70
|
+
case 'transfer_in':
|
|
71
|
+
valid = shares >= 0n && assets >= 0n && row.request_hash == null;
|
|
72
|
+
break;
|
|
73
|
+
case 'transfer_out':
|
|
74
|
+
valid = shares <= 0n && assets <= 0n && row.request_hash == null;
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
valid = false;
|
|
78
|
+
}
|
|
79
|
+
if (!valid) throw new GauntletSDKError(`Invalid activity deltas for ${row.type}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function orderActivityRows(rows: UserActivity[]): UserActivity[] {
|
|
83
|
+
rows.forEach(validateActivityRow);
|
|
84
|
+
return [...rows].sort(
|
|
85
|
+
(a, b) =>
|
|
86
|
+
a.block_number - b.block_number ||
|
|
87
|
+
a.block_timestamp - b.block_timestamp ||
|
|
88
|
+
Number(isPending(b)) - Number(isPending(a))
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Split request hashes by vault and by each request occurrence.
|
|
94
|
+
* Takes the output of `orderActivityRows`.
|
|
95
|
+
*/
|
|
96
|
+
export function activityLifecycles(ordered: UserActivity[]): UserActivity[][] {
|
|
97
|
+
const groups: UserActivity[][] = [];
|
|
98
|
+
const latest = new Map<string, UserActivity[]>();
|
|
99
|
+
for (const row of ordered) {
|
|
100
|
+
if (row.request_hash == null) {
|
|
101
|
+
groups.push([row]);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const key = `${row.vault_id}:${row.request_hash}`;
|
|
105
|
+
const previous = latest.get(key);
|
|
106
|
+
if (previous === undefined || isPending(row)) {
|
|
107
|
+
if (previous !== undefined && previous.every(isPending)) {
|
|
108
|
+
throw new GauntletSDKError('An activity request has duplicate pending rows');
|
|
109
|
+
}
|
|
110
|
+
const group = [row];
|
|
111
|
+
latest.set(key, group);
|
|
112
|
+
groups.push(group);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (previous.some((entry) => !isPending(entry))) {
|
|
116
|
+
throw new GauntletSDKError('An activity request has conflicting terminal rows');
|
|
117
|
+
}
|
|
118
|
+
const pending = previous[0];
|
|
119
|
+
requireSameToken(pending, row);
|
|
120
|
+
const expected =
|
|
121
|
+
pending.type === 'deposit_pending'
|
|
122
|
+
? ['deposit', 'deposit_refunded']
|
|
123
|
+
: ['withdraw', 'withdraw_refunded'];
|
|
124
|
+
if (!expected.includes(row.type)) {
|
|
125
|
+
throw new GauntletSDKError('The activity terminal does not match its request');
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
row.type === 'deposit_refunded' &&
|
|
129
|
+
decimalToBigInt(pending.assets_delta.native, 18) !==
|
|
130
|
+
-decimalToBigInt(row.assets_delta.native, 18)
|
|
131
|
+
) {
|
|
132
|
+
throw new GauntletSDKError('The deposit refund does not match its request');
|
|
133
|
+
}
|
|
134
|
+
if (
|
|
135
|
+
row.type === 'withdraw_refunded' &&
|
|
136
|
+
sharesToBigInt(pending.shares_delta) !== -sharesToBigInt(row.shares_delta)
|
|
137
|
+
) {
|
|
138
|
+
throw new GauntletSDKError('The withdrawal refund does not match its request');
|
|
139
|
+
}
|
|
140
|
+
previous.push(row);
|
|
141
|
+
}
|
|
142
|
+
return groups;
|
|
143
|
+
}
|
package/src/api/caip.ts
CHANGED
|
@@ -16,7 +16,9 @@ export interface ApiVaultId {
|
|
|
16
16
|
export function parseApiVaultId(id: string): ApiVaultId {
|
|
17
17
|
const match = /^(?:eip155:)?(\d+):(0x[0-9a-fA-F]{40})$/.exec(id);
|
|
18
18
|
if (!match) throw new InvalidCaipIdError(id);
|
|
19
|
-
|
|
19
|
+
const chainId = Number(match[1]);
|
|
20
|
+
if (!Number.isSafeInteger(chainId) || chainId <= 0) throw new InvalidCaipIdError(id);
|
|
21
|
+
return { chainId, address: match[2].toLowerCase() };
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function formatApiVaultId(chainId: number, address: string): string {
|
package/src/api/client.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { parseApiVaultId } from './caip';
|
|
2
|
+
import { GauntletApiError } from './errors';
|
|
1
3
|
import type { components } from './generated';
|
|
2
4
|
import { apiGet, type GauntletApiConfig, type QueryParams } from './http';
|
|
3
5
|
|
|
4
6
|
type Schemas = components['schemas'];
|
|
5
7
|
|
|
8
|
+
function activityError(message: string, code?: string): GauntletApiError {
|
|
9
|
+
return new GauntletApiError({ status: 200, path: '/v1/users/activity', message, code });
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export type UserActivity = Schemas['UserActivity'];
|
|
7
13
|
export type UserActivityResponse = Schemas['UserActivityResponse'];
|
|
8
14
|
export type UserPosition = Schemas['UserPosition'];
|
|
@@ -248,11 +254,40 @@ export class GauntletApi {
|
|
|
248
254
|
walletAddress: string,
|
|
249
255
|
options: Omit<ActivityOptions, 'next'> = {}
|
|
250
256
|
): AsyncGenerator<UserActivity> {
|
|
257
|
+
const vault = options.vaultId === undefined ? undefined : parseApiVaultId(options.vaultId);
|
|
258
|
+
// Gaia pages on (timestamp, logIndex), which rows do not expose. Rows
|
|
259
|
+
// can repeat legitimately inside one transaction, so the boundary check
|
|
260
|
+
// is the timestamp order.
|
|
261
|
+
const descending = (options.order ?? 'desc') === 'desc';
|
|
262
|
+
let lastTimestamp: number | undefined;
|
|
251
263
|
let next: string | undefined;
|
|
264
|
+
const cursors = new Set<string>();
|
|
252
265
|
do {
|
|
253
266
|
const page = await this.activity(walletAddress, { ...options, next });
|
|
254
|
-
|
|
267
|
+
if (page.meta.partial_errors?.length) {
|
|
268
|
+
throw activityError('The activity response is incomplete', 'INCOMPLETE_ACTIVITY');
|
|
269
|
+
}
|
|
270
|
+
for (const row of page.data) {
|
|
271
|
+
if (vault !== undefined) {
|
|
272
|
+
const rowVault = parseApiVaultId(row.vault_id);
|
|
273
|
+
if (rowVault.chainId !== vault.chainId || rowVault.address !== vault.address) {
|
|
274
|
+
throw activityError('The activity vault does not match the request');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (
|
|
278
|
+
lastTimestamp !== undefined &&
|
|
279
|
+
(descending ? row.block_timestamp > lastTimestamp : row.block_timestamp < lastTimestamp)
|
|
280
|
+
) {
|
|
281
|
+
throw activityError('The activity rows are out of order');
|
|
282
|
+
}
|
|
283
|
+
lastTimestamp = row.block_timestamp;
|
|
284
|
+
yield row;
|
|
285
|
+
}
|
|
255
286
|
next = page.meta.next_cursor ?? undefined;
|
|
287
|
+
if (next !== undefined) {
|
|
288
|
+
if (cursors.has(next)) throw activityError('The activity cursor repeats');
|
|
289
|
+
cursors.add(next);
|
|
290
|
+
}
|
|
256
291
|
} while (next);
|
|
257
292
|
}
|
|
258
293
|
|
package/src/api/decimal.ts
CHANGED
|
@@ -17,6 +17,7 @@ export function decimalToBigInt(value: string, decimals: number): bigint {
|
|
|
17
17
|
if (!Number.isInteger(decimals) || decimals < 0) {
|
|
18
18
|
throw new InvalidDecimalError(`decimals=${decimals}`);
|
|
19
19
|
}
|
|
20
|
+
if (typeof value !== 'string') throw new InvalidDecimalError(String(value));
|
|
20
21
|
const match = DECIMAL_RE.exec(value);
|
|
21
22
|
if (!match) throw new InvalidDecimalError(value);
|
|
22
23
|
|
package/src/api/http.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { activityLifecycles, orderActivityRows, requireSameToken } from './activityRows';
|
|
1
2
|
import { GauntletSDKError } from '../errors';
|
|
2
3
|
import type { GauntletApi, TokenRef, UserActivity } from './client';
|
|
3
4
|
import { bigIntToDecimal, decimalToBigInt, sharesToBigInt } from './decimal';
|
|
@@ -57,8 +58,11 @@ export function buildPositionHistory(rows: UserActivity[]): PositionHistory {
|
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const ordered =
|
|
61
|
-
|
|
61
|
+
const ordered = orderActivityRows(rows);
|
|
62
|
+
activityLifecycles(ordered);
|
|
63
|
+
const tokenRow = ordered.find((r) => r.assets_delta.token) ?? ordered[0];
|
|
64
|
+
const token = tokenRow.assets_delta.token ?? null;
|
|
65
|
+
for (const row of ordered) requireSameToken(tokenRow, row);
|
|
62
66
|
|
|
63
67
|
let sharesBalance = 0n;
|
|
64
68
|
let pendingDepositAssets = 0n;
|
|
@@ -95,9 +99,14 @@ export function buildPositionHistory(rows: UserActivity[]): PositionHistory {
|
|
|
95
99
|
if (row.request_hash != null) pendingDepositByHash.set(row.request_hash, assetsDelta);
|
|
96
100
|
break;
|
|
97
101
|
case 'deposit_refunded':
|
|
98
|
-
|
|
102
|
+
if (
|
|
103
|
+
row.request_hash == null ||
|
|
104
|
+
pendingDepositByHash.get(row.request_hash) !== -assetsDelta
|
|
105
|
+
) {
|
|
106
|
+
throw new GauntletSDKError('The deposit refund does not match its escrow');
|
|
107
|
+
}
|
|
99
108
|
pendingDepositAssets += assetsDelta;
|
|
100
|
-
|
|
109
|
+
pendingDepositByHash.delete(row.request_hash);
|
|
101
110
|
break;
|
|
102
111
|
case 'withdraw':
|
|
103
112
|
// Sync withdraws and async terminal solves both carry the asset
|
|
@@ -121,8 +130,11 @@ export function buildPositionHistory(rows: UserActivity[]): PositionHistory {
|
|
|
121
130
|
break;
|
|
122
131
|
}
|
|
123
132
|
case 'withdraw_refunded':
|
|
133
|
+
if (row.request_hash == null || pendingRedeemByHash.get(row.request_hash) !== sharesDelta) {
|
|
134
|
+
throw new GauntletSDKError('The withdrawal refund does not match its escrow');
|
|
135
|
+
}
|
|
124
136
|
pendingRedeemShares -= sharesDelta;
|
|
125
|
-
|
|
137
|
+
pendingRedeemByHash.delete(row.request_hash);
|
|
126
138
|
break;
|
|
127
139
|
case 'transfer_in':
|
|
128
140
|
case 'transfer_out':
|
|
@@ -131,6 +143,9 @@ export function buildPositionHistory(rows: UserActivity[]): PositionHistory {
|
|
|
131
143
|
throw new GauntletSDKError(`Unknown activity row type "${row.type}"`);
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
if (sharesBalance < 0n || pendingDepositAssets < 0n || pendingRedeemShares < 0n) {
|
|
147
|
+
throw new GauntletSDKError('The activity history produces a negative position balance');
|
|
148
|
+
}
|
|
134
149
|
points.push({
|
|
135
150
|
timestamp: new Date(row.block_timestamp * 1000),
|
|
136
151
|
txHash: row.tx_hash,
|
package/src/attribution/index.ts
CHANGED
|
@@ -8,13 +8,14 @@ export { encodeBuilderCode } from './erc8021';
|
|
|
8
8
|
export type PreparedPayloadWithAttribution = {
|
|
9
9
|
type: string;
|
|
10
10
|
to: Address;
|
|
11
|
+
chainId: number;
|
|
11
12
|
data: Hex;
|
|
12
13
|
account?: Address;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export type PreparedTx = {
|
|
16
17
|
payload: PreparedPayloadWithAttribution;
|
|
17
|
-
tx: TxStep;
|
|
18
|
+
tx: TxStep & { chainId: number };
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -38,7 +39,8 @@ export async function buildAttribution(client: GauntletClient): Promise<Hex> {
|
|
|
38
39
|
|
|
39
40
|
export async function encodeTransactionWithAttribution(
|
|
40
41
|
client: GauntletClient,
|
|
41
|
-
transaction: TxStep
|
|
42
|
+
transaction: TxStep,
|
|
43
|
+
chainId: number
|
|
42
44
|
): Promise<PreparedTx> {
|
|
43
45
|
const customAttribution = await buildAttribution(client);
|
|
44
46
|
const encodedTx = encodeFunctionData({
|
|
@@ -52,10 +54,12 @@ export async function encodeTransactionWithAttribution(
|
|
|
52
54
|
type: transaction.type,
|
|
53
55
|
account: transaction.account,
|
|
54
56
|
to: transaction.address,
|
|
57
|
+
chainId,
|
|
55
58
|
data: encodedTxWithAttribution,
|
|
56
59
|
},
|
|
57
60
|
tx: {
|
|
58
61
|
...transaction,
|
|
62
|
+
chainId,
|
|
59
63
|
attribution: customAttribution,
|
|
60
64
|
},
|
|
61
65
|
};
|
package/src/evm/adapters/aera.ts
CHANGED
|
@@ -430,17 +430,35 @@ export const aeraAdapter: EvmProtocolAdapter = {
|
|
|
430
430
|
throw new InvalidWithdrawParamsError();
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
433
|
+
const tokensOut =
|
|
434
|
+
'amount' in params && params.amount != null
|
|
435
|
+
? params.amount
|
|
436
|
+
: subtractSolverTip(
|
|
437
|
+
await getAsyncRedeemTokenOut(
|
|
438
|
+
publicClient,
|
|
439
|
+
runtime,
|
|
440
|
+
vault.vaultAddress,
|
|
441
|
+
asset.address,
|
|
442
|
+
shares
|
|
443
|
+
),
|
|
444
|
+
solverTip
|
|
445
|
+
);
|
|
446
|
+
minTokenOut = applySlippageDown(tokensOut, slippageBps);
|
|
441
447
|
maxUnitsIn = shares;
|
|
442
448
|
}
|
|
443
449
|
|
|
450
|
+
if (params.maxShares !== undefined) {
|
|
451
|
+
if (
|
|
452
|
+
isAsync ||
|
|
453
|
+
typeof params.maxShares !== 'bigint' ||
|
|
454
|
+
params.maxShares <= 0n ||
|
|
455
|
+
(!isV2ExactTokenWithdraw && shares !== undefined && shares > params.maxShares)
|
|
456
|
+
) {
|
|
457
|
+
throw new InvalidWithdrawParamsError();
|
|
458
|
+
}
|
|
459
|
+
if (maxUnitsIn > params.maxShares) maxUnitsIn = params.maxShares;
|
|
460
|
+
}
|
|
461
|
+
|
|
444
462
|
const steps: EvmTxStep[] = [];
|
|
445
463
|
|
|
446
464
|
if (isAsync) {
|
|
@@ -57,6 +57,7 @@ export type AdapterWithdrawParams = {
|
|
|
57
57
|
maxPriceAge?: bigint;
|
|
58
58
|
aeraRuntime?: AeraRuntimeContracts;
|
|
59
59
|
syncWithdrawQuote?: SyncWithdrawQuoteBounds;
|
|
60
|
+
maxShares?: bigint;
|
|
60
61
|
} & (
|
|
61
62
|
| { shares: bigint; amount?: never; entireAmount?: never }
|
|
62
63
|
| { amount: bigint; shares?: never; entireAmount?: never }
|
package/src/evm/aeraContracts.ts
CHANGED
|
@@ -17,8 +17,6 @@ import { MAX_BPS } from '../constants';
|
|
|
17
17
|
import { ContractVersion, type EvmVaultDeployment } from './types';
|
|
18
18
|
import { readAtBlock } from './aeraContracts/readOptions';
|
|
19
19
|
|
|
20
|
-
const contractVersionCache = new Map<string, ContractVersion>();
|
|
21
|
-
|
|
22
20
|
const contractVersionAbi = [
|
|
23
21
|
{
|
|
24
22
|
type: 'function',
|
|
@@ -81,10 +79,6 @@ export function applySlippageUp(amount: bigint, slippageBps: number): bigint {
|
|
|
81
79
|
return (amount * (MAX_BPS + BigInt(slippageBps)) + MAX_BPS - 1n) / MAX_BPS;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
|
-
function contractVersionCacheKey(client: PublicClient, address: Address): string {
|
|
85
|
-
return `${client.chain?.id ?? 'unknown'}:${address.toLowerCase()}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
82
|
function isVersionUnavailableError(error: unknown): boolean {
|
|
89
83
|
if (!(error instanceof ContractFunctionExecutionError) || error.functionName !== 'version') {
|
|
90
84
|
return false;
|
|
@@ -122,15 +116,35 @@ export function getMultiDepositorVault<T extends Client>(
|
|
|
122
116
|
});
|
|
123
117
|
}
|
|
124
118
|
|
|
119
|
+
// Latest-block versions per client. Historical reads and failed reads are not cached.
|
|
120
|
+
const latestContractVersions = new WeakMap<PublicClient, Map<string, ContractVersion>>();
|
|
121
|
+
|
|
125
122
|
export async function resolveContractVersion(
|
|
126
123
|
client: PublicClient,
|
|
127
124
|
address: Address,
|
|
128
125
|
options: AeraReadOptions = {}
|
|
129
126
|
): Promise<ContractVersion> {
|
|
130
|
-
|
|
131
|
-
const cached = contractVersionCache.get(cacheKey);
|
|
132
|
-
if (cached) return cached;
|
|
127
|
+
if (options.blockNumber !== undefined) return readContractVersion(client, address, options);
|
|
133
128
|
|
|
129
|
+
let versions = latestContractVersions.get(client);
|
|
130
|
+
if (versions === undefined) {
|
|
131
|
+
versions = new Map();
|
|
132
|
+
latestContractVersions.set(client, versions);
|
|
133
|
+
}
|
|
134
|
+
const key = address.toLowerCase();
|
|
135
|
+
const cached = versions.get(key);
|
|
136
|
+
if (cached !== undefined) return cached;
|
|
137
|
+
|
|
138
|
+
const version = await readContractVersion(client, address, options);
|
|
139
|
+
versions.set(key, version);
|
|
140
|
+
return version;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function readContractVersion(
|
|
144
|
+
client: PublicClient,
|
|
145
|
+
address: Address,
|
|
146
|
+
options: AeraReadOptions
|
|
147
|
+
): Promise<ContractVersion> {
|
|
134
148
|
try {
|
|
135
149
|
const versionString = await client.readContract({
|
|
136
150
|
address,
|
|
@@ -138,14 +152,15 @@ export async function resolveContractVersion(
|
|
|
138
152
|
functionName: 'version',
|
|
139
153
|
...readAtBlock(options),
|
|
140
154
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return
|
|
155
|
+
if (!/^[12]\./.test(versionString)) {
|
|
156
|
+
throw new UnsupportedFeatureError(`Aera: unsupported contract version ${versionString}`);
|
|
157
|
+
}
|
|
158
|
+
return versionString.startsWith('2.') ? ContractVersion.V2 : ContractVersion.V1;
|
|
145
159
|
} catch (error) {
|
|
146
160
|
if (!isVersionUnavailableError(error)) throw error;
|
|
147
161
|
|
|
148
|
-
|
|
162
|
+
const code = await client.getCode({ address, ...readAtBlock(options) });
|
|
163
|
+
if (code === undefined || code === '0x') throw error;
|
|
149
164
|
return ContractVersion.V1;
|
|
150
165
|
}
|
|
151
166
|
}
|
package/src/evm/deposit.ts
CHANGED
|
@@ -48,8 +48,9 @@ export interface EvmDepositParams {
|
|
|
48
48
|
/**
|
|
49
49
|
* Builds the ordered list of EVM transactions required to deposit into a Gauntlet vault.
|
|
50
50
|
*
|
|
51
|
-
* Returns
|
|
52
|
-
*
|
|
51
|
+
* Returns ordered transactions: ERC-20 approvals (if the allowance is insufficient),
|
|
52
|
+
* followed by the deposit or async deposit request. Some tokens require an approval
|
|
53
|
+
* of zero before a new nonzero approval. Each transaction is
|
|
53
54
|
* returned as a `PreparedTx` object ready to be signed and sent.
|
|
54
55
|
*
|
|
55
56
|
* @param client - A configured `GauntletClient` instance with an EVM public client and wallet.
|
|
@@ -77,6 +78,7 @@ export async function getDepositTx(
|
|
|
77
78
|
client: GauntletClient,
|
|
78
79
|
params: EvmDepositParams
|
|
79
80
|
): Promise<PreparedTx[]> {
|
|
81
|
+
params = structuredClone(params);
|
|
80
82
|
const slippageBps = params.slippageBps ?? DEFAULT_BPS;
|
|
81
83
|
if (!Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > Number(MAX_BPS)) {
|
|
82
84
|
throw new InvalidSlippageBPSError(slippageBps);
|
|
@@ -141,7 +143,7 @@ export async function getDepositTx(
|
|
|
141
143
|
: vault.vaultAddress;
|
|
142
144
|
|
|
143
145
|
// NOTE: this could be moved to the buildDeposit level
|
|
144
|
-
const { sufficient } = await adapter.checkAllowance({
|
|
146
|
+
const { sufficient, current } = await adapter.checkAllowance({
|
|
145
147
|
publicClient,
|
|
146
148
|
token: token.address,
|
|
147
149
|
owner: account,
|
|
@@ -152,14 +154,43 @@ export async function getDepositTx(
|
|
|
152
154
|
const steps: EvmTxStep[] = [];
|
|
153
155
|
|
|
154
156
|
if (!sufficient) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
const approval = adapter.buildApproval({
|
|
158
|
+
token: token.address,
|
|
159
|
+
spender,
|
|
160
|
+
amount: params.amount,
|
|
161
|
+
account,
|
|
162
|
+
});
|
|
163
|
+
if (current > 0n && params.amount > 0n) {
|
|
164
|
+
try {
|
|
165
|
+
await publicClient.estimateContractGas({
|
|
166
|
+
address: approval.address,
|
|
167
|
+
abi: approval.abi,
|
|
168
|
+
functionName: 'approve',
|
|
169
|
+
args: approval.args,
|
|
170
|
+
account,
|
|
171
|
+
});
|
|
172
|
+
} catch (approvalError) {
|
|
173
|
+
const reset = adapter.buildApproval({
|
|
174
|
+
token: token.address,
|
|
175
|
+
spender,
|
|
176
|
+
amount: 0n,
|
|
177
|
+
account,
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
await publicClient.estimateContractGas({
|
|
181
|
+
address: reset.address,
|
|
182
|
+
abi: reset.abi,
|
|
183
|
+
functionName: 'approve',
|
|
184
|
+
args: reset.args,
|
|
185
|
+
account,
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
throw approvalError;
|
|
189
|
+
}
|
|
190
|
+
steps.push(reset);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
steps.push(approval);
|
|
163
194
|
}
|
|
164
195
|
|
|
165
196
|
const depositSteps = await adapter.buildDeposit({
|
|
@@ -178,5 +209,5 @@ export async function getDepositTx(
|
|
|
178
209
|
});
|
|
179
210
|
steps.push(...depositSteps);
|
|
180
211
|
|
|
181
|
-
return Promise.all(steps.map((step) => encodeTransactionWithAttribution(client, step)));
|
|
212
|
+
return Promise.all(steps.map((step) => encodeTransactionWithAttribution(client, step, chainId)));
|
|
182
213
|
}
|
package/src/evm/depositQuote.ts
CHANGED
|
@@ -95,6 +95,7 @@ export async function getSyncDepositQuote(
|
|
|
95
95
|
client: GauntletClient,
|
|
96
96
|
params: SyncDepositQuoteParams
|
|
97
97
|
): Promise<SyncDepositQuote> {
|
|
98
|
+
params = structuredClone(params);
|
|
98
99
|
const slippageBps = params.slippageBps ?? DEFAULT_BPS;
|
|
99
100
|
if (!Number.isInteger(slippageBps) || slippageBps < 0 || slippageBps > Number(MAX_BPS)) {
|
|
100
101
|
throw new InvalidSlippageBPSError(slippageBps);
|
|
@@ -25,6 +25,7 @@ export async function getDepositReceiverApprovalTx(
|
|
|
25
25
|
client: GauntletClient,
|
|
26
26
|
params: EvmDepositReceiverApprovalParams
|
|
27
27
|
): Promise<PreparedTx> {
|
|
28
|
+
params = { ...params };
|
|
28
29
|
const resolved = await resolveVault(client, params.vaultId, params.chainId);
|
|
29
30
|
if (!resolved) throw new VaultNotFoundError(params.vaultId);
|
|
30
31
|
|
|
@@ -39,13 +40,17 @@ export async function getDepositReceiverApprovalTx(
|
|
|
39
40
|
throw new UnsupportedFeatureError('Aera: deposit receiver approval on V1');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
return encodeTransactionWithAttribution(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
return encodeTransactionWithAttribution(
|
|
44
|
+
client,
|
|
45
|
+
{
|
|
46
|
+
type: 'setDepositReceiverApproval',
|
|
47
|
+
...provisionerV2.setDepositReceiverApprovalTxRequest(
|
|
48
|
+
runtime.provisioner.address,
|
|
49
|
+
params.depositor,
|
|
50
|
+
params.approved ?? true,
|
|
51
|
+
receiver
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
chainId
|
|
55
|
+
);
|
|
51
56
|
}
|