@spectratools/tx-shared 0.4.2 → 0.5.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 +6 -2
- package/dist/chunk-HFRJBEDT.js +1144 -0
- package/dist/errors.d.ts +1 -1
- package/dist/execute-tx-DO8p_9dP.d.ts +160 -0
- package/dist/execute-tx.d.ts +4 -63
- package/dist/execute-tx.js +1 -1
- package/dist/index.d.ts +54 -47
- package/dist/index.js +25 -381
- package/package.json +1 -1
- package/dist/chunk-4XI6TBKX.js +0 -130
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TxError
|
|
3
|
+
} from "./chunk-6T4D5UCR.js";
|
|
4
|
+
|
|
5
|
+
// src/signers/privy-client.ts
|
|
6
|
+
import { isAddress } from "viem";
|
|
7
|
+
|
|
8
|
+
// src/signers/privy-signature.ts
|
|
9
|
+
import { createPrivateKey, sign as signWithCrypto } from "crypto";
|
|
10
|
+
var PRIVY_AUTHORIZATION_KEY_PREFIX = "wallet-auth:";
|
|
11
|
+
var PRIVY_AUTHORIZATION_KEY_REGEX = /^wallet-auth:[A-Za-z0-9+/]+={0,2}$/;
|
|
12
|
+
var DEFAULT_PRIVY_API_URL = "https://api.privy.io";
|
|
13
|
+
function normalizePrivyApiUrl(apiUrl) {
|
|
14
|
+
const value = (apiUrl ?? DEFAULT_PRIVY_API_URL).trim();
|
|
15
|
+
if (value.length === 0) {
|
|
16
|
+
throw new TxError(
|
|
17
|
+
"PRIVY_AUTH_FAILED",
|
|
18
|
+
"Invalid PRIVY_API_URL format: expected a non-empty http(s) URL"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = new URL(value);
|
|
24
|
+
} catch (cause) {
|
|
25
|
+
throw new TxError(
|
|
26
|
+
"PRIVY_AUTH_FAILED",
|
|
27
|
+
"Invalid PRIVY_API_URL format: expected a non-empty http(s) URL",
|
|
28
|
+
cause
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
32
|
+
throw new TxError(
|
|
33
|
+
"PRIVY_AUTH_FAILED",
|
|
34
|
+
"Invalid PRIVY_API_URL format: expected a non-empty http(s) URL"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
38
|
+
}
|
|
39
|
+
function parsePrivyAuthorizationKey(authorizationKey) {
|
|
40
|
+
const normalizedKey = authorizationKey.trim();
|
|
41
|
+
if (!PRIVY_AUTHORIZATION_KEY_REGEX.test(normalizedKey)) {
|
|
42
|
+
throw new TxError(
|
|
43
|
+
"PRIVY_AUTH_FAILED",
|
|
44
|
+
"Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const rawPrivateKey = normalizedKey.slice(PRIVY_AUTHORIZATION_KEY_PREFIX.length);
|
|
48
|
+
let derKey;
|
|
49
|
+
try {
|
|
50
|
+
derKey = Buffer.from(rawPrivateKey, "base64");
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
throw new TxError(
|
|
53
|
+
"PRIVY_AUTH_FAILED",
|
|
54
|
+
"Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>",
|
|
55
|
+
cause
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
let keyObject;
|
|
59
|
+
try {
|
|
60
|
+
keyObject = createPrivateKey({
|
|
61
|
+
key: derKey,
|
|
62
|
+
format: "der",
|
|
63
|
+
type: "pkcs8"
|
|
64
|
+
});
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
throw new TxError(
|
|
67
|
+
"PRIVY_AUTH_FAILED",
|
|
68
|
+
"Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>",
|
|
69
|
+
cause
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (keyObject.asymmetricKeyType !== "ec") {
|
|
73
|
+
throw new TxError(
|
|
74
|
+
"PRIVY_AUTH_FAILED",
|
|
75
|
+
"Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const details = keyObject.asymmetricKeyDetails;
|
|
79
|
+
const namedCurve = details !== void 0 && "namedCurve" in details ? details.namedCurve : void 0;
|
|
80
|
+
if (namedCurve !== void 0 && namedCurve !== "prime256v1") {
|
|
81
|
+
throw new TxError(
|
|
82
|
+
"PRIVY_AUTH_FAILED",
|
|
83
|
+
"Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return keyObject;
|
|
87
|
+
}
|
|
88
|
+
function createPrivyAuthorizationPayload(options) {
|
|
89
|
+
const appId = options.appId.trim();
|
|
90
|
+
if (appId.length === 0) {
|
|
91
|
+
throw new TxError(
|
|
92
|
+
"PRIVY_AUTH_FAILED",
|
|
93
|
+
"Invalid PRIVY_APP_ID format: expected non-empty string"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const url = options.url.trim().replace(/\/+$/, "");
|
|
97
|
+
if (url.length === 0) {
|
|
98
|
+
throw new TxError(
|
|
99
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
100
|
+
"Failed to build Privy authorization payload: request URL is empty"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
version: 1,
|
|
105
|
+
method: options.method,
|
|
106
|
+
url,
|
|
107
|
+
headers: {
|
|
108
|
+
"privy-app-id": appId,
|
|
109
|
+
...options.idempotencyKey !== void 0 ? { "privy-idempotency-key": options.idempotencyKey } : {}
|
|
110
|
+
},
|
|
111
|
+
body: options.body
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function serializePrivyAuthorizationPayload(payload) {
|
|
115
|
+
return canonicalizeJson(payload);
|
|
116
|
+
}
|
|
117
|
+
function generatePrivyAuthorizationSignature(payload, authorizationKey) {
|
|
118
|
+
const privateKey = parsePrivyAuthorizationKey(authorizationKey);
|
|
119
|
+
const serializedPayload = serializePrivyAuthorizationPayload(payload);
|
|
120
|
+
const signature = signWithCrypto("sha256", Buffer.from(serializedPayload), privateKey);
|
|
121
|
+
return signature.toString("base64");
|
|
122
|
+
}
|
|
123
|
+
function canonicalizeJson(value) {
|
|
124
|
+
if (value === null) {
|
|
125
|
+
return "null";
|
|
126
|
+
}
|
|
127
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
128
|
+
return JSON.stringify(value);
|
|
129
|
+
}
|
|
130
|
+
if (typeof value === "number") {
|
|
131
|
+
if (!Number.isFinite(value)) {
|
|
132
|
+
throw new TxError(
|
|
133
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
134
|
+
"Failed to build Privy authorization payload: JSON payload contains a non-finite number"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return JSON.stringify(value);
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
return `[${value.map((item) => canonicalizeJson(item)).join(",")}]`;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value === "object") {
|
|
143
|
+
const record = value;
|
|
144
|
+
const keys = Object.keys(record).sort((left, right) => left.localeCompare(right));
|
|
145
|
+
const entries = [];
|
|
146
|
+
for (const key of keys) {
|
|
147
|
+
const entryValue = record[key];
|
|
148
|
+
if (entryValue === void 0) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
entries.push(`${JSON.stringify(key)}:${canonicalizeJson(entryValue)}`);
|
|
152
|
+
}
|
|
153
|
+
return `{${entries.join(",")}}`;
|
|
154
|
+
}
|
|
155
|
+
throw new TxError(
|
|
156
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
157
|
+
"Failed to build Privy authorization payload: JSON payload contains unsupported value type"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/signers/privy-client.ts
|
|
162
|
+
function createPrivyClient(options) {
|
|
163
|
+
const appId = options.appId.trim();
|
|
164
|
+
const walletId = options.walletId.trim();
|
|
165
|
+
if (appId.length === 0) {
|
|
166
|
+
throw new TxError(
|
|
167
|
+
"PRIVY_AUTH_FAILED",
|
|
168
|
+
"Invalid PRIVY_APP_ID format: expected non-empty string"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (walletId.length === 0) {
|
|
172
|
+
throw new TxError(
|
|
173
|
+
"PRIVY_AUTH_FAILED",
|
|
174
|
+
"Invalid PRIVY_WALLET_ID format: expected non-empty string"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const apiUrl = normalizePrivyApiUrl(options.apiUrl);
|
|
178
|
+
const fetchImplementation = options.fetchImplementation ?? fetch;
|
|
179
|
+
return {
|
|
180
|
+
appId,
|
|
181
|
+
walletId,
|
|
182
|
+
apiUrl,
|
|
183
|
+
async createRpcIntent(request, requestOptions) {
|
|
184
|
+
const url = `${apiUrl}/v1/intents/wallets/${walletId}/rpc`;
|
|
185
|
+
const payload = createPrivyAuthorizationPayload({
|
|
186
|
+
appId,
|
|
187
|
+
method: "POST",
|
|
188
|
+
url,
|
|
189
|
+
body: request,
|
|
190
|
+
...requestOptions?.idempotencyKey !== void 0 ? { idempotencyKey: requestOptions.idempotencyKey } : {}
|
|
191
|
+
});
|
|
192
|
+
const signature = generatePrivyAuthorizationSignature(payload, options.authorizationKey);
|
|
193
|
+
return sendPrivyRequest({
|
|
194
|
+
fetchImplementation,
|
|
195
|
+
method: "POST",
|
|
196
|
+
url,
|
|
197
|
+
body: request,
|
|
198
|
+
operation: "create rpc intent",
|
|
199
|
+
headers: {
|
|
200
|
+
"privy-app-id": appId,
|
|
201
|
+
"privy-authorization-signature": signature,
|
|
202
|
+
...requestOptions?.idempotencyKey !== void 0 ? { "privy-idempotency-key": requestOptions.idempotencyKey } : {}
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
async getWallet() {
|
|
207
|
+
const url = `${apiUrl}/v1/wallets/${walletId}`;
|
|
208
|
+
return sendPrivyRequest({
|
|
209
|
+
fetchImplementation,
|
|
210
|
+
method: "GET",
|
|
211
|
+
url,
|
|
212
|
+
operation: "get wallet",
|
|
213
|
+
headers: {
|
|
214
|
+
"privy-app-id": appId
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
async getPolicy(policyId) {
|
|
219
|
+
if (policyId.trim().length === 0) {
|
|
220
|
+
throw new TxError(
|
|
221
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
222
|
+
"Failed to build Privy policy lookup request: policy id is empty"
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const url = `${apiUrl}/v1/policies/${policyId}`;
|
|
226
|
+
return sendPrivyRequest({
|
|
227
|
+
fetchImplementation,
|
|
228
|
+
method: "GET",
|
|
229
|
+
url,
|
|
230
|
+
operation: "get policy",
|
|
231
|
+
headers: {
|
|
232
|
+
"privy-app-id": appId
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
var ALLOWLIST_KEYS = /* @__PURE__ */ new Set([
|
|
239
|
+
"allowedcontracts",
|
|
240
|
+
"allowlistedcontracts",
|
|
241
|
+
"contractallowlist",
|
|
242
|
+
"toallowlist",
|
|
243
|
+
"allowedtoaddresses"
|
|
244
|
+
]);
|
|
245
|
+
var MAX_VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
246
|
+
"maxvalue",
|
|
247
|
+
"maxvaluewei",
|
|
248
|
+
"maxnativevalue",
|
|
249
|
+
"maxnativevaluewei",
|
|
250
|
+
"maxtransferamount",
|
|
251
|
+
"maxtransferamountwei",
|
|
252
|
+
"valuecap",
|
|
253
|
+
"valuecapwei"
|
|
254
|
+
]);
|
|
255
|
+
function normalizePrivyPolicy(policy) {
|
|
256
|
+
const constraints = extractConstraintsFromValue(policy.rules);
|
|
257
|
+
const allowlistedContracts = Array.from(constraints.allowlistedContracts).sort((left, right) => left.localeCompare(right)).map((value) => value);
|
|
258
|
+
const maxValueWei = selectLowestValue(constraints.maxValueWeiCandidates);
|
|
259
|
+
return {
|
|
260
|
+
id: policy.id,
|
|
261
|
+
ownerId: policy.owner_id,
|
|
262
|
+
ruleCount: Array.isArray(policy.rules) ? policy.rules.length : 0,
|
|
263
|
+
allowlistedContracts,
|
|
264
|
+
...maxValueWei !== void 0 ? { maxValueWei } : {}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function fetchPrivyPolicyVisibility(client) {
|
|
268
|
+
const wallet = await client.getWallet();
|
|
269
|
+
const policyIds = normalizePolicyIds(wallet.policy_ids);
|
|
270
|
+
const policies = [];
|
|
271
|
+
for (const policyId of policyIds) {
|
|
272
|
+
const policy = await client.getPolicy(policyId);
|
|
273
|
+
policies.push(normalizePrivyPolicy(policy));
|
|
274
|
+
}
|
|
275
|
+
const contractAllowlistSet = /* @__PURE__ */ new Set();
|
|
276
|
+
const maxValueCandidates = [];
|
|
277
|
+
for (const policy of policies) {
|
|
278
|
+
for (const contractAddress of policy.allowlistedContracts) {
|
|
279
|
+
contractAllowlistSet.add(contractAddress.toLowerCase());
|
|
280
|
+
}
|
|
281
|
+
if (policy.maxValueWei !== void 0) {
|
|
282
|
+
maxValueCandidates.push(policy.maxValueWei);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const contractAllowlist = Array.from(contractAllowlistSet).sort((left, right) => left.localeCompare(right)).map((value) => value);
|
|
286
|
+
const maxValueWei = selectLowestValue(maxValueCandidates);
|
|
287
|
+
return {
|
|
288
|
+
walletId: wallet.id,
|
|
289
|
+
policyIds,
|
|
290
|
+
policies,
|
|
291
|
+
contractAllowlist,
|
|
292
|
+
...maxValueWei !== void 0 ? { maxValueWei } : {}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function preflightPrivyTransactionPolicy(client, request) {
|
|
296
|
+
const visibility = await fetchPrivyPolicyVisibility(client);
|
|
297
|
+
const violations = [];
|
|
298
|
+
if (request.to !== void 0 && visibility.contractAllowlist.length > 0) {
|
|
299
|
+
const toAddress = request.to.toLowerCase();
|
|
300
|
+
const isAllowed = visibility.contractAllowlist.some(
|
|
301
|
+
(allowedContract) => allowedContract.toLowerCase() === toAddress
|
|
302
|
+
);
|
|
303
|
+
if (!isAllowed) {
|
|
304
|
+
const constrainedPolicyIds = visibility.policies.filter((policy) => policy.allowlistedContracts.length > 0).map((policy) => policy.id).sort((left, right) => left.localeCompare(right));
|
|
305
|
+
violations.push({
|
|
306
|
+
type: "contract-allowlist",
|
|
307
|
+
message: `Target contract ${request.to} is not allowlisted by active Privy policies`,
|
|
308
|
+
policyIds: constrainedPolicyIds,
|
|
309
|
+
actual: request.to,
|
|
310
|
+
expected: visibility.contractAllowlist.join(", ")
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (request.value !== void 0 && visibility.maxValueWei !== void 0) {
|
|
315
|
+
if (request.value > visibility.maxValueWei) {
|
|
316
|
+
const constrainedPolicyIds = visibility.policies.filter((policy) => policy.maxValueWei === visibility.maxValueWei).map((policy) => policy.id).sort((left, right) => left.localeCompare(right));
|
|
317
|
+
violations.push({
|
|
318
|
+
type: "native-value-cap",
|
|
319
|
+
message: `Native value ${request.value.toString()} exceeds Privy policy max ${visibility.maxValueWei.toString()}`,
|
|
320
|
+
policyIds: constrainedPolicyIds,
|
|
321
|
+
actual: request.value.toString(),
|
|
322
|
+
expected: visibility.maxValueWei.toString()
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
status: violations.length > 0 ? "blocked" : "allowed",
|
|
328
|
+
visibility,
|
|
329
|
+
violations
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function toPrivyPolicyViolationError(result) {
|
|
333
|
+
const message = result.violations.length > 0 ? result.violations.map((violation) => {
|
|
334
|
+
const policies = violation.policyIds.length > 0 ? ` (policies: ${violation.policyIds.join(", ")})` : "";
|
|
335
|
+
return `${violation.message}${policies}`;
|
|
336
|
+
}).join("; ") : "Privy policy preflight blocked transaction";
|
|
337
|
+
return new TxError(
|
|
338
|
+
"PRIVY_POLICY_BLOCKED",
|
|
339
|
+
`Privy policy preflight blocked transaction: ${message}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
async function sendPrivyRequest(options) {
|
|
343
|
+
let response;
|
|
344
|
+
try {
|
|
345
|
+
response = await options.fetchImplementation(options.url, {
|
|
346
|
+
method: options.method,
|
|
347
|
+
headers: {
|
|
348
|
+
...options.headers,
|
|
349
|
+
...options.body !== void 0 ? { "content-type": "application/json" } : {}
|
|
350
|
+
},
|
|
351
|
+
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
|
|
352
|
+
});
|
|
353
|
+
} catch (cause) {
|
|
354
|
+
throw new TxError(
|
|
355
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
356
|
+
`Privy ${options.operation} request failed: network error`,
|
|
357
|
+
cause
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const payload = await parseJsonResponse(response, options.operation);
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const message = extractPrivyErrorMessage(payload) ?? `HTTP ${response.status}`;
|
|
363
|
+
if (response.status === 401 || response.status === 403) {
|
|
364
|
+
throw new TxError(
|
|
365
|
+
"PRIVY_AUTH_FAILED",
|
|
366
|
+
`Privy authentication failed (${response.status}): ${message}`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
throw new TxError(
|
|
370
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
371
|
+
`Privy ${options.operation} request failed (${response.status}): ${message}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
if (!isRecord(payload)) {
|
|
375
|
+
throw new TxError(
|
|
376
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
377
|
+
`Privy ${options.operation} request failed: invalid JSON response shape`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return payload;
|
|
381
|
+
}
|
|
382
|
+
async function parseJsonResponse(response, operation) {
|
|
383
|
+
const text = await response.text();
|
|
384
|
+
if (text.trim().length === 0) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
return JSON.parse(text);
|
|
389
|
+
} catch (cause) {
|
|
390
|
+
throw new TxError(
|
|
391
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
392
|
+
`Privy ${operation} request failed: invalid JSON response`,
|
|
393
|
+
cause
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function extractConstraintsFromValue(value) {
|
|
398
|
+
const constraints = {
|
|
399
|
+
allowlistedContracts: /* @__PURE__ */ new Set(),
|
|
400
|
+
maxValueWeiCandidates: []
|
|
401
|
+
};
|
|
402
|
+
visitConstraintNode(value, constraints);
|
|
403
|
+
return constraints;
|
|
404
|
+
}
|
|
405
|
+
function visitConstraintNode(value, constraints) {
|
|
406
|
+
if (Array.isArray(value)) {
|
|
407
|
+
for (const item of value) {
|
|
408
|
+
visitConstraintNode(item, constraints);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (!isRecord(value)) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
416
|
+
const normalizedKey = normalizeConstraintKey(key);
|
|
417
|
+
if (ALLOWLIST_KEYS.has(normalizedKey)) {
|
|
418
|
+
for (const address of parseAddressList(entry)) {
|
|
419
|
+
constraints.allowlistedContracts.add(address.toLowerCase());
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (MAX_VALUE_KEYS.has(normalizedKey)) {
|
|
423
|
+
const parsedValue = parseWeiValue(entry);
|
|
424
|
+
if (parsedValue !== void 0) {
|
|
425
|
+
constraints.maxValueWeiCandidates.push(parsedValue);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
visitConstraintNode(entry, constraints);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function normalizeConstraintKey(value) {
|
|
432
|
+
return value.replace(/[^A-Za-z0-9]/g, "").toLowerCase();
|
|
433
|
+
}
|
|
434
|
+
function parseAddressList(value) {
|
|
435
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
436
|
+
const addresses = [];
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
if (typeof entry !== "string") {
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (!isAddress(entry)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
addresses.push(entry);
|
|
445
|
+
}
|
|
446
|
+
return addresses;
|
|
447
|
+
}
|
|
448
|
+
function parseWeiValue(value) {
|
|
449
|
+
if (typeof value === "bigint") {
|
|
450
|
+
return value >= 0n ? value : void 0;
|
|
451
|
+
}
|
|
452
|
+
if (typeof value === "number") {
|
|
453
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
454
|
+
return void 0;
|
|
455
|
+
}
|
|
456
|
+
return BigInt(value);
|
|
457
|
+
}
|
|
458
|
+
if (typeof value !== "string") {
|
|
459
|
+
return void 0;
|
|
460
|
+
}
|
|
461
|
+
const trimmed = value.trim();
|
|
462
|
+
if (trimmed.length === 0) {
|
|
463
|
+
return void 0;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
if (/^0x[0-9a-fA-F]+$/.test(trimmed)) {
|
|
467
|
+
return BigInt(trimmed);
|
|
468
|
+
}
|
|
469
|
+
if (/^[0-9]+$/.test(trimmed)) {
|
|
470
|
+
return BigInt(trimmed);
|
|
471
|
+
}
|
|
472
|
+
return void 0;
|
|
473
|
+
} catch {
|
|
474
|
+
return void 0;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function selectLowestValue(values) {
|
|
478
|
+
if (values.length === 0) {
|
|
479
|
+
return void 0;
|
|
480
|
+
}
|
|
481
|
+
let minimum = values[0];
|
|
482
|
+
for (const value of values.slice(1)) {
|
|
483
|
+
if (value < minimum) {
|
|
484
|
+
minimum = value;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return minimum;
|
|
488
|
+
}
|
|
489
|
+
function normalizePolicyIds(policyIds) {
|
|
490
|
+
if (!Array.isArray(policyIds)) {
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
const ids = /* @__PURE__ */ new Set();
|
|
494
|
+
for (const entry of policyIds) {
|
|
495
|
+
if (typeof entry !== "string") {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const normalized = entry.trim();
|
|
499
|
+
if (normalized.length === 0) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
ids.add(normalized);
|
|
503
|
+
}
|
|
504
|
+
return Array.from(ids).sort((left, right) => left.localeCompare(right));
|
|
505
|
+
}
|
|
506
|
+
function isRecord(value) {
|
|
507
|
+
return typeof value === "object" && value !== null;
|
|
508
|
+
}
|
|
509
|
+
function extractPrivyErrorMessage(payload) {
|
|
510
|
+
if (!isRecord(payload)) {
|
|
511
|
+
return void 0;
|
|
512
|
+
}
|
|
513
|
+
const directMessage = payload.message;
|
|
514
|
+
if (typeof directMessage === "string" && directMessage.length > 0) {
|
|
515
|
+
return directMessage;
|
|
516
|
+
}
|
|
517
|
+
const errorValue = payload.error;
|
|
518
|
+
if (typeof errorValue === "string" && errorValue.length > 0) {
|
|
519
|
+
return errorValue;
|
|
520
|
+
}
|
|
521
|
+
if (isRecord(errorValue)) {
|
|
522
|
+
const nestedMessage = errorValue.message;
|
|
523
|
+
if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
|
|
524
|
+
return nestedMessage;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/signers/privy-account.ts
|
|
531
|
+
import {
|
|
532
|
+
isAddress as isAddress2,
|
|
533
|
+
toHex
|
|
534
|
+
} from "viem";
|
|
535
|
+
var DEFAULT_CHAIN_ID = 2741;
|
|
536
|
+
var HASH_REGEX = /^0x[a-fA-F0-9]{64}$/;
|
|
537
|
+
var HEX_REGEX = /^0x(?:[a-fA-F0-9]{2})+$/;
|
|
538
|
+
async function createPrivyAccount(options) {
|
|
539
|
+
const wallet = await options.client.getWallet();
|
|
540
|
+
if (typeof wallet.address !== "string" || !isAddress2(wallet.address)) {
|
|
541
|
+
throw new TxError(
|
|
542
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
543
|
+
"Privy get wallet request failed: wallet address is missing or invalid"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
const address = wallet.address;
|
|
547
|
+
const defaultChainId = options.chainId ?? DEFAULT_CHAIN_ID;
|
|
548
|
+
return {
|
|
549
|
+
address,
|
|
550
|
+
type: "json-rpc",
|
|
551
|
+
async sendTransaction(request) {
|
|
552
|
+
const chainId = normalizeChainId(request.chainId ?? defaultChainId);
|
|
553
|
+
const rpcRequest = createSendTransactionRpcRequest(address, chainId, request);
|
|
554
|
+
const response = await options.client.createRpcIntent(rpcRequest);
|
|
555
|
+
return parsePrivyTransactionHash(response);
|
|
556
|
+
},
|
|
557
|
+
async signMessage({ message }) {
|
|
558
|
+
const rpcRequest = createPersonalSignRpcRequest(message);
|
|
559
|
+
const response = await options.client.createRpcIntent(rpcRequest);
|
|
560
|
+
return parsePrivySignature(response, "personal_sign");
|
|
561
|
+
},
|
|
562
|
+
async signTypedData(parameters) {
|
|
563
|
+
const rpcRequest = createSignTypedDataRpcRequest(parameters);
|
|
564
|
+
const response = await options.client.createRpcIntent(rpcRequest);
|
|
565
|
+
return parsePrivySignature(response, "eth_signTypedData_v4");
|
|
566
|
+
},
|
|
567
|
+
async signTransaction(transaction) {
|
|
568
|
+
const request = transaction;
|
|
569
|
+
const chainId = normalizeChainId(request.chainId ?? defaultChainId);
|
|
570
|
+
const rpcRequest = createSignTransactionRpcRequest(address, chainId, request);
|
|
571
|
+
const response = await options.client.createRpcIntent(rpcRequest);
|
|
572
|
+
return parsePrivySignedTransaction(response);
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function createSendTransactionRpcRequest(from, chainId, request) {
|
|
577
|
+
const transaction = createPrivyTransactionPayload(from, chainId, request, "eth_sendTransaction");
|
|
578
|
+
return {
|
|
579
|
+
method: "eth_sendTransaction",
|
|
580
|
+
caip2: `eip155:${chainId}`,
|
|
581
|
+
params: {
|
|
582
|
+
transaction
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function createSignTransactionRpcRequest(from, chainId, request) {
|
|
587
|
+
const transaction = createPrivyTransactionPayload(from, chainId, request, "eth_signTransaction");
|
|
588
|
+
return {
|
|
589
|
+
method: "eth_signTransaction",
|
|
590
|
+
params: {
|
|
591
|
+
transaction
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function createPersonalSignRpcRequest(message) {
|
|
596
|
+
if (typeof message === "string") {
|
|
597
|
+
return {
|
|
598
|
+
method: "personal_sign",
|
|
599
|
+
params: {
|
|
600
|
+
message,
|
|
601
|
+
encoding: "utf-8"
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const raw = message.raw;
|
|
606
|
+
const encodedMessage = raw instanceof Uint8Array ? toHex(raw) : raw;
|
|
607
|
+
if (!isHexString(encodedMessage)) {
|
|
608
|
+
throw new TxError(
|
|
609
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
610
|
+
"Failed to build Privy personal_sign payload: message.raw must be a hex string or Uint8Array"
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
method: "personal_sign",
|
|
615
|
+
params: {
|
|
616
|
+
message: encodedMessage,
|
|
617
|
+
encoding: "hex"
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function createSignTypedDataRpcRequest(parameters) {
|
|
622
|
+
const primaryType = parameters.primaryType;
|
|
623
|
+
if (typeof primaryType !== "string" || primaryType.trim().length === 0) {
|
|
624
|
+
throw new TxError(
|
|
625
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
626
|
+
"Failed to build Privy eth_signTypedData_v4 payload: primaryType is required"
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
method: "eth_signTypedData_v4",
|
|
631
|
+
params: {
|
|
632
|
+
typed_data: {
|
|
633
|
+
domain: normalizeJsonValue(parameters.domain ?? {}),
|
|
634
|
+
types: normalizeJsonValue(parameters.types),
|
|
635
|
+
message: normalizeJsonValue(parameters.message),
|
|
636
|
+
primary_type: primaryType
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function createPrivyTransactionPayload(from, chainId, request, rpcMethod) {
|
|
642
|
+
if (request.from !== void 0 && request.from.toLowerCase() !== from.toLowerCase()) {
|
|
643
|
+
throw new TxError(
|
|
644
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
645
|
+
`Failed to build Privy ${rpcMethod} payload: from does not match wallet address`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const transaction = {
|
|
649
|
+
from,
|
|
650
|
+
chain_id: chainId
|
|
651
|
+
};
|
|
652
|
+
if (request.to !== void 0) {
|
|
653
|
+
transaction.to = request.to;
|
|
654
|
+
}
|
|
655
|
+
if (request.data !== void 0) {
|
|
656
|
+
transaction.data = request.data;
|
|
657
|
+
}
|
|
658
|
+
if (request.value !== void 0) {
|
|
659
|
+
transaction.value = normalizeNumberish(request.value, rpcMethod, "value");
|
|
660
|
+
}
|
|
661
|
+
if (request.nonce !== void 0) {
|
|
662
|
+
transaction.nonce = normalizeNumberish(request.nonce, rpcMethod, "nonce");
|
|
663
|
+
}
|
|
664
|
+
if (request.gas !== void 0) {
|
|
665
|
+
transaction.gas_limit = normalizeNumberish(request.gas, rpcMethod, "gas");
|
|
666
|
+
}
|
|
667
|
+
if (request.gasPrice !== void 0) {
|
|
668
|
+
transaction.gas_price = normalizeNumberish(request.gasPrice, rpcMethod, "gasPrice");
|
|
669
|
+
}
|
|
670
|
+
if (request.maxFeePerGas !== void 0) {
|
|
671
|
+
transaction.max_fee_per_gas = normalizeNumberish(
|
|
672
|
+
request.maxFeePerGas,
|
|
673
|
+
rpcMethod,
|
|
674
|
+
"maxFeePerGas"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (request.maxPriorityFeePerGas !== void 0) {
|
|
678
|
+
transaction.max_priority_fee_per_gas = normalizeNumberish(
|
|
679
|
+
request.maxPriorityFeePerGas,
|
|
680
|
+
rpcMethod,
|
|
681
|
+
"maxPriorityFeePerGas"
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
if (request.type !== void 0) {
|
|
685
|
+
transaction.type = request.type;
|
|
686
|
+
}
|
|
687
|
+
return transaction;
|
|
688
|
+
}
|
|
689
|
+
function parsePrivyTransactionHash(response) {
|
|
690
|
+
assertIntentExecuted(response, "eth_sendTransaction");
|
|
691
|
+
const hash = extractDataString(response, "hash");
|
|
692
|
+
if (hash === void 0 || !HASH_REGEX.test(hash)) {
|
|
693
|
+
throw new TxError(
|
|
694
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
695
|
+
`Privy rpc intent ${response.intent_id} executed without a valid transaction hash`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
return hash;
|
|
699
|
+
}
|
|
700
|
+
function parsePrivySignature(response, method) {
|
|
701
|
+
assertIntentExecuted(response, method);
|
|
702
|
+
const signature = extractDataString(response, "signature");
|
|
703
|
+
if (signature === void 0 || !isHexString(signature)) {
|
|
704
|
+
throw new TxError(
|
|
705
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
706
|
+
`Privy rpc intent ${response.intent_id} executed without a valid signature`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return signature;
|
|
710
|
+
}
|
|
711
|
+
function parsePrivySignedTransaction(response) {
|
|
712
|
+
assertIntentExecuted(response, "eth_signTransaction");
|
|
713
|
+
const signedTransaction = extractDataString(response, "signed_transaction");
|
|
714
|
+
if (signedTransaction === void 0 || !isHexString(signedTransaction)) {
|
|
715
|
+
throw new TxError(
|
|
716
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
717
|
+
`Privy rpc intent ${response.intent_id} executed without a valid signed transaction`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
return signedTransaction;
|
|
721
|
+
}
|
|
722
|
+
function assertIntentExecuted(response, rpcMethod) {
|
|
723
|
+
const intentId = response.intent_id;
|
|
724
|
+
if (response.status === "executed") {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const reason = extractIntentReason(response) ?? "no reason provided";
|
|
728
|
+
if (response.status === "failed") {
|
|
729
|
+
if (rpcMethod === "eth_sendTransaction") {
|
|
730
|
+
throw new TxError("TX_REVERTED", `Privy rpc intent ${intentId} failed: ${reason}`);
|
|
731
|
+
}
|
|
732
|
+
throw new TxError("PRIVY_TRANSPORT_FAILED", `Privy rpc intent ${intentId} failed: ${reason}`);
|
|
733
|
+
}
|
|
734
|
+
if (response.status === "rejected" || response.status === "dismissed" || response.status === "expired") {
|
|
735
|
+
throw new TxError(
|
|
736
|
+
"PRIVY_AUTH_FAILED",
|
|
737
|
+
`Privy rpc intent ${intentId} ${response.status}: ${reason}`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
throw new TxError(
|
|
741
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
742
|
+
`Privy rpc intent ${intentId} did not execute (status: ${response.status})`
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
function extractDataString(response, key) {
|
|
746
|
+
if (!isRecord2(response.action_result)) {
|
|
747
|
+
return void 0;
|
|
748
|
+
}
|
|
749
|
+
const responseBody = response.action_result.response_body;
|
|
750
|
+
if (!isRecord2(responseBody)) {
|
|
751
|
+
return void 0;
|
|
752
|
+
}
|
|
753
|
+
const data = responseBody.data;
|
|
754
|
+
if (!isRecord2(data)) {
|
|
755
|
+
return void 0;
|
|
756
|
+
}
|
|
757
|
+
const value = data[key];
|
|
758
|
+
return typeof value === "string" ? value : void 0;
|
|
759
|
+
}
|
|
760
|
+
function extractIntentReason(response) {
|
|
761
|
+
if (typeof response.dismissal_reason === "string" && response.dismissal_reason.length > 0) {
|
|
762
|
+
return response.dismissal_reason;
|
|
763
|
+
}
|
|
764
|
+
if (isRecord2(response.action_result)) {
|
|
765
|
+
const responseBody = response.action_result.response_body;
|
|
766
|
+
if (isRecord2(responseBody)) {
|
|
767
|
+
const error = responseBody.error;
|
|
768
|
+
if (typeof error === "string" && error.length > 0) {
|
|
769
|
+
return error;
|
|
770
|
+
}
|
|
771
|
+
if (isRecord2(error)) {
|
|
772
|
+
const errorMessage2 = error.message;
|
|
773
|
+
if (typeof errorMessage2 === "string" && errorMessage2.length > 0) {
|
|
774
|
+
return errorMessage2;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const message = responseBody.message;
|
|
778
|
+
if (typeof message === "string" && message.length > 0) {
|
|
779
|
+
return message;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return void 0;
|
|
784
|
+
}
|
|
785
|
+
function normalizeChainId(chainId) {
|
|
786
|
+
if (typeof chainId === "number") {
|
|
787
|
+
if (!Number.isInteger(chainId) || chainId <= 0) {
|
|
788
|
+
throw new TxError(
|
|
789
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
790
|
+
"Failed to build Privy chain payload: chainId must be a positive integer"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return chainId;
|
|
794
|
+
}
|
|
795
|
+
if (typeof chainId === "bigint") {
|
|
796
|
+
if (chainId <= 0n || chainId > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
797
|
+
throw new TxError(
|
|
798
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
799
|
+
"Failed to build Privy chain payload: chainId must be a positive safe integer"
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
return Number(chainId);
|
|
803
|
+
}
|
|
804
|
+
const trimmed = chainId.trim();
|
|
805
|
+
if (trimmed.length === 0) {
|
|
806
|
+
throw new TxError(
|
|
807
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
808
|
+
"Failed to build Privy chain payload: chainId is empty"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (/^0x[0-9a-fA-F]+$/.test(trimmed)) {
|
|
812
|
+
const parsed2 = Number.parseInt(trimmed.slice(2), 16);
|
|
813
|
+
if (!Number.isSafeInteger(parsed2) || parsed2 <= 0) {
|
|
814
|
+
throw new TxError(
|
|
815
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
816
|
+
"Failed to build Privy chain payload: chainId must be a positive safe integer"
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
return parsed2;
|
|
820
|
+
}
|
|
821
|
+
if (!/^[0-9]+$/.test(trimmed)) {
|
|
822
|
+
throw new TxError(
|
|
823
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
824
|
+
"Failed to build Privy chain payload: chainId must be a positive safe integer"
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
const parsed = Number(trimmed);
|
|
828
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
829
|
+
throw new TxError(
|
|
830
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
831
|
+
"Failed to build Privy chain payload: chainId must be a positive safe integer"
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return parsed;
|
|
835
|
+
}
|
|
836
|
+
function normalizeNumberish(value, rpcMethod, fieldName) {
|
|
837
|
+
if (typeof value === "bigint") {
|
|
838
|
+
if (value < 0n) {
|
|
839
|
+
throw new TxError(
|
|
840
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
841
|
+
`Failed to build Privy ${rpcMethod} payload: ${fieldName} cannot be negative`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
return value.toString(10);
|
|
845
|
+
}
|
|
846
|
+
if (typeof value === "number") {
|
|
847
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
848
|
+
throw new TxError(
|
|
849
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
850
|
+
`Failed to build Privy ${rpcMethod} payload: ${fieldName} must be a non-negative number`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return Number.isInteger(value) ? value : value.toString();
|
|
854
|
+
}
|
|
855
|
+
const trimmed = value.trim();
|
|
856
|
+
if (trimmed.length === 0) {
|
|
857
|
+
throw new TxError(
|
|
858
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
859
|
+
`Failed to build Privy ${rpcMethod} payload: ${fieldName} is empty`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return trimmed;
|
|
863
|
+
}
|
|
864
|
+
function normalizeJsonValue(value) {
|
|
865
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
866
|
+
return value;
|
|
867
|
+
}
|
|
868
|
+
if (typeof value === "bigint") {
|
|
869
|
+
return value.toString(10);
|
|
870
|
+
}
|
|
871
|
+
if (value instanceof Uint8Array) {
|
|
872
|
+
return toHex(value);
|
|
873
|
+
}
|
|
874
|
+
if (Array.isArray(value)) {
|
|
875
|
+
return value.map((item) => normalizeJsonValue(item));
|
|
876
|
+
}
|
|
877
|
+
if (isRecord2(value)) {
|
|
878
|
+
const normalized = {};
|
|
879
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
880
|
+
if (entry === void 0) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
normalized[key] = normalizeJsonValue(entry);
|
|
884
|
+
}
|
|
885
|
+
return normalized;
|
|
886
|
+
}
|
|
887
|
+
throw new TxError(
|
|
888
|
+
"PRIVY_TRANSPORT_FAILED",
|
|
889
|
+
"Failed to build Privy typed data payload: unsupported value type"
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
function isHexString(value) {
|
|
893
|
+
return HEX_REGEX.test(value);
|
|
894
|
+
}
|
|
895
|
+
function isRecord2(value) {
|
|
896
|
+
return typeof value === "object" && value !== null;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/signers/privy.ts
|
|
900
|
+
var REQUIRED_FIELDS = [
|
|
901
|
+
"privyAppId",
|
|
902
|
+
"privyWalletId",
|
|
903
|
+
"privyAuthorizationKey"
|
|
904
|
+
];
|
|
905
|
+
var APP_ID_REGEX = /^[A-Za-z0-9_-]{8,128}$/;
|
|
906
|
+
var WALLET_ID_REGEX = /^[A-Za-z0-9_-]{8,128}$/;
|
|
907
|
+
var PRIVY_POLICY_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("spectratools.tx-shared.privy.policy-context");
|
|
908
|
+
function attachPrivyPolicyContext(account, context) {
|
|
909
|
+
Object.defineProperty(account, PRIVY_POLICY_CONTEXT_SYMBOL, {
|
|
910
|
+
value: context,
|
|
911
|
+
configurable: false,
|
|
912
|
+
enumerable: false,
|
|
913
|
+
writable: false
|
|
914
|
+
});
|
|
915
|
+
return account;
|
|
916
|
+
}
|
|
917
|
+
function getPrivyPolicyContext(account) {
|
|
918
|
+
if (typeof account !== "object" || account === null) {
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
const withContext = account;
|
|
922
|
+
return withContext[PRIVY_POLICY_CONTEXT_SYMBOL];
|
|
923
|
+
}
|
|
924
|
+
async function createPrivySigner(options) {
|
|
925
|
+
const missing = REQUIRED_FIELDS.filter((field) => {
|
|
926
|
+
const value = options[field];
|
|
927
|
+
return typeof value !== "string" || value.trim().length === 0;
|
|
928
|
+
});
|
|
929
|
+
if (missing.length > 0) {
|
|
930
|
+
const missingLabels = missing.map((field) => {
|
|
931
|
+
if (field === "privyAppId") {
|
|
932
|
+
return "PRIVY_APP_ID";
|
|
933
|
+
}
|
|
934
|
+
if (field === "privyWalletId") {
|
|
935
|
+
return "PRIVY_WALLET_ID";
|
|
936
|
+
}
|
|
937
|
+
return "PRIVY_AUTHORIZATION_KEY";
|
|
938
|
+
}).join(", ");
|
|
939
|
+
throw new TxError(
|
|
940
|
+
"PRIVY_AUTH_FAILED",
|
|
941
|
+
`Privy signer requires configuration: missing ${missingLabels}`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
const appId = options.privyAppId?.trim() ?? "";
|
|
945
|
+
const walletId = options.privyWalletId?.trim() ?? "";
|
|
946
|
+
const authorizationKey = options.privyAuthorizationKey?.trim() ?? "";
|
|
947
|
+
if (!APP_ID_REGEX.test(appId)) {
|
|
948
|
+
throw new TxError(
|
|
949
|
+
"PRIVY_AUTH_FAILED",
|
|
950
|
+
"Invalid PRIVY_APP_ID format: expected 8-128 chars using letters, numbers, hyphen, or underscore"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
if (!WALLET_ID_REGEX.test(walletId)) {
|
|
954
|
+
throw new TxError(
|
|
955
|
+
"PRIVY_AUTH_FAILED",
|
|
956
|
+
"Invalid PRIVY_WALLET_ID format: expected 8-128 chars using letters, numbers, hyphen, or underscore"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
parsePrivyAuthorizationKey(authorizationKey);
|
|
960
|
+
const apiUrl = normalizePrivyApiUrl(options.privyApiUrl);
|
|
961
|
+
const client = createPrivyClient({
|
|
962
|
+
appId,
|
|
963
|
+
walletId,
|
|
964
|
+
authorizationKey,
|
|
965
|
+
apiUrl
|
|
966
|
+
});
|
|
967
|
+
const account = attachPrivyPolicyContext(await createPrivyAccount({ client }), {
|
|
968
|
+
appId,
|
|
969
|
+
walletId,
|
|
970
|
+
apiUrl,
|
|
971
|
+
client
|
|
972
|
+
});
|
|
973
|
+
return {
|
|
974
|
+
provider: "privy",
|
|
975
|
+
account,
|
|
976
|
+
address: account.address,
|
|
977
|
+
privy: {
|
|
978
|
+
appId,
|
|
979
|
+
walletId,
|
|
980
|
+
apiUrl,
|
|
981
|
+
client
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/execute-tx.ts
|
|
987
|
+
async function executeTx(options) {
|
|
988
|
+
const {
|
|
989
|
+
publicClient,
|
|
990
|
+
walletClient,
|
|
991
|
+
account,
|
|
992
|
+
address,
|
|
993
|
+
abi,
|
|
994
|
+
functionName,
|
|
995
|
+
chain,
|
|
996
|
+
args,
|
|
997
|
+
value,
|
|
998
|
+
gasLimit,
|
|
999
|
+
maxFeePerGas,
|
|
1000
|
+
nonce,
|
|
1001
|
+
dryRun = false
|
|
1002
|
+
} = options;
|
|
1003
|
+
let estimatedGas;
|
|
1004
|
+
try {
|
|
1005
|
+
estimatedGas = await publicClient.estimateContractGas({
|
|
1006
|
+
account,
|
|
1007
|
+
address,
|
|
1008
|
+
abi,
|
|
1009
|
+
functionName,
|
|
1010
|
+
args,
|
|
1011
|
+
value
|
|
1012
|
+
});
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
throw mapError(error, "estimation");
|
|
1015
|
+
}
|
|
1016
|
+
let simulationResult;
|
|
1017
|
+
try {
|
|
1018
|
+
const sim = await publicClient.simulateContract({
|
|
1019
|
+
account,
|
|
1020
|
+
address,
|
|
1021
|
+
abi,
|
|
1022
|
+
functionName,
|
|
1023
|
+
args,
|
|
1024
|
+
value
|
|
1025
|
+
});
|
|
1026
|
+
simulationResult = sim.result;
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
throw mapError(error, "simulation");
|
|
1029
|
+
}
|
|
1030
|
+
const privyPolicy = await runPrivyPolicyPreflight({
|
|
1031
|
+
account,
|
|
1032
|
+
address,
|
|
1033
|
+
...value !== void 0 ? { value } : {}
|
|
1034
|
+
});
|
|
1035
|
+
if (dryRun) {
|
|
1036
|
+
return {
|
|
1037
|
+
status: "dry-run",
|
|
1038
|
+
estimatedGas,
|
|
1039
|
+
simulationResult,
|
|
1040
|
+
...privyPolicy !== void 0 ? { privyPolicy } : {}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
if (privyPolicy?.status === "blocked") {
|
|
1044
|
+
throw toPrivyPolicyViolationError(privyPolicy);
|
|
1045
|
+
}
|
|
1046
|
+
let hash;
|
|
1047
|
+
try {
|
|
1048
|
+
hash = await walletClient.writeContract({
|
|
1049
|
+
account,
|
|
1050
|
+
address,
|
|
1051
|
+
abi,
|
|
1052
|
+
functionName,
|
|
1053
|
+
args,
|
|
1054
|
+
value,
|
|
1055
|
+
chain,
|
|
1056
|
+
gas: gasLimit ?? estimatedGas,
|
|
1057
|
+
maxFeePerGas,
|
|
1058
|
+
nonce
|
|
1059
|
+
});
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
throw mapError(error, "submit");
|
|
1062
|
+
}
|
|
1063
|
+
let receipt;
|
|
1064
|
+
try {
|
|
1065
|
+
receipt = await publicClient.waitForTransactionReceipt({ hash });
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
throw mapError(error, "receipt");
|
|
1068
|
+
}
|
|
1069
|
+
if (receipt.status === "reverted") {
|
|
1070
|
+
throw new TxError("TX_REVERTED", `Transaction ${hash} reverted on-chain`);
|
|
1071
|
+
}
|
|
1072
|
+
return receiptToTxResult(receipt);
|
|
1073
|
+
}
|
|
1074
|
+
function receiptToTxResult(receipt) {
|
|
1075
|
+
return {
|
|
1076
|
+
hash: receipt.transactionHash,
|
|
1077
|
+
blockNumber: receipt.blockNumber,
|
|
1078
|
+
gasUsed: receipt.gasUsed,
|
|
1079
|
+
status: receipt.status === "success" ? "success" : "reverted",
|
|
1080
|
+
from: receipt.from,
|
|
1081
|
+
to: receipt.to,
|
|
1082
|
+
effectiveGasPrice: receipt.effectiveGasPrice
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function runPrivyPolicyPreflight(request) {
|
|
1086
|
+
const context = getPrivyPolicyContext(request.account);
|
|
1087
|
+
if (context === void 0) {
|
|
1088
|
+
return void 0;
|
|
1089
|
+
}
|
|
1090
|
+
return preflightPrivyTransactionPolicy(context.client, {
|
|
1091
|
+
to: request.address,
|
|
1092
|
+
...request.value !== void 0 ? { value: request.value } : {}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
function mapError(error, phase) {
|
|
1096
|
+
const msg = errorMessage(error);
|
|
1097
|
+
if (matchesInsufficientFunds(msg)) {
|
|
1098
|
+
return new TxError("INSUFFICIENT_FUNDS", `Insufficient funds: ${msg}`, error);
|
|
1099
|
+
}
|
|
1100
|
+
if (matchesNonceConflict(msg)) {
|
|
1101
|
+
return new TxError("NONCE_CONFLICT", `Nonce conflict: ${msg}`, error);
|
|
1102
|
+
}
|
|
1103
|
+
if (phase === "estimation" || phase === "simulation") {
|
|
1104
|
+
return new TxError("GAS_ESTIMATION_FAILED", `Gas estimation/simulation failed: ${msg}`, error);
|
|
1105
|
+
}
|
|
1106
|
+
if (matchesRevert(msg)) {
|
|
1107
|
+
return new TxError("TX_REVERTED", `Transaction reverted: ${msg}`, error);
|
|
1108
|
+
}
|
|
1109
|
+
return new TxError("TX_REVERTED", `Transaction failed (${phase}): ${msg}`, error);
|
|
1110
|
+
}
|
|
1111
|
+
function errorMessage(error) {
|
|
1112
|
+
if (error instanceof Error) return error.message;
|
|
1113
|
+
return String(error);
|
|
1114
|
+
}
|
|
1115
|
+
function matchesInsufficientFunds(msg) {
|
|
1116
|
+
const lower = msg.toLowerCase();
|
|
1117
|
+
return lower.includes("insufficient funds") || lower.includes("insufficient balance") || lower.includes("sender doesn't have enough funds");
|
|
1118
|
+
}
|
|
1119
|
+
function matchesNonceConflict(msg) {
|
|
1120
|
+
const lower = msg.toLowerCase();
|
|
1121
|
+
return lower.includes("nonce too low") || lower.includes("nonce has already been used") || lower.includes("already known") || lower.includes("replacement transaction underpriced");
|
|
1122
|
+
}
|
|
1123
|
+
function matchesRevert(msg) {
|
|
1124
|
+
const lower = msg.toLowerCase();
|
|
1125
|
+
return lower.includes("revert") || lower.includes("execution reverted") || lower.includes("transaction failed");
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export {
|
|
1129
|
+
normalizePrivyApiUrl,
|
|
1130
|
+
parsePrivyAuthorizationKey,
|
|
1131
|
+
createPrivyAuthorizationPayload,
|
|
1132
|
+
serializePrivyAuthorizationPayload,
|
|
1133
|
+
generatePrivyAuthorizationSignature,
|
|
1134
|
+
createPrivyClient,
|
|
1135
|
+
normalizePrivyPolicy,
|
|
1136
|
+
fetchPrivyPolicyVisibility,
|
|
1137
|
+
preflightPrivyTransactionPolicy,
|
|
1138
|
+
toPrivyPolicyViolationError,
|
|
1139
|
+
createPrivyAccount,
|
|
1140
|
+
attachPrivyPolicyContext,
|
|
1141
|
+
getPrivyPolicyContext,
|
|
1142
|
+
createPrivySigner,
|
|
1143
|
+
executeTx
|
|
1144
|
+
};
|