@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/dist/index.js CHANGED
@@ -3,8 +3,22 @@ import {
3
3
  createAbstractClient
4
4
  } from "./chunk-P4ACSL6N.js";
5
5
  import {
6
- executeTx
7
- } from "./chunk-4XI6TBKX.js";
6
+ attachPrivyPolicyContext,
7
+ createPrivyAccount,
8
+ createPrivyAuthorizationPayload,
9
+ createPrivyClient,
10
+ createPrivySigner,
11
+ executeTx,
12
+ fetchPrivyPolicyVisibility,
13
+ generatePrivyAuthorizationSignature,
14
+ getPrivyPolicyContext,
15
+ normalizePrivyApiUrl,
16
+ normalizePrivyPolicy,
17
+ parsePrivyAuthorizationKey,
18
+ preflightPrivyTransactionPolicy,
19
+ serializePrivyAuthorizationPayload,
20
+ toPrivyPolicyViolationError
21
+ } from "./chunk-HFRJBEDT.js";
8
22
  import {
9
23
  TxError,
10
24
  toTxError
@@ -57,384 +71,6 @@ function createKeystoreSigner(options) {
57
71
  return { account, address: account.address, provider: "keystore" };
58
72
  }
59
73
 
60
- // src/signers/privy-signature.ts
61
- import { createPrivateKey, sign as signWithCrypto } from "crypto";
62
- var PRIVY_AUTHORIZATION_KEY_PREFIX = "wallet-auth:";
63
- var PRIVY_AUTHORIZATION_KEY_REGEX = /^wallet-auth:[A-Za-z0-9+/]+={0,2}$/;
64
- var DEFAULT_PRIVY_API_URL = "https://api.privy.io";
65
- function normalizePrivyApiUrl(apiUrl) {
66
- const value = (apiUrl ?? DEFAULT_PRIVY_API_URL).trim();
67
- if (value.length === 0) {
68
- throw new TxError(
69
- "PRIVY_AUTH_FAILED",
70
- "Invalid PRIVY_API_URL format: expected a non-empty http(s) URL"
71
- );
72
- }
73
- let parsed;
74
- try {
75
- parsed = new URL(value);
76
- } catch (cause) {
77
- throw new TxError(
78
- "PRIVY_AUTH_FAILED",
79
- "Invalid PRIVY_API_URL format: expected a non-empty http(s) URL",
80
- cause
81
- );
82
- }
83
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
84
- throw new TxError(
85
- "PRIVY_AUTH_FAILED",
86
- "Invalid PRIVY_API_URL format: expected a non-empty http(s) URL"
87
- );
88
- }
89
- return parsed.toString().replace(/\/+$/, "");
90
- }
91
- function parsePrivyAuthorizationKey(authorizationKey) {
92
- const normalizedKey = authorizationKey.trim();
93
- if (!PRIVY_AUTHORIZATION_KEY_REGEX.test(normalizedKey)) {
94
- throw new TxError(
95
- "PRIVY_AUTH_FAILED",
96
- "Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>"
97
- );
98
- }
99
- const rawPrivateKey = normalizedKey.slice(PRIVY_AUTHORIZATION_KEY_PREFIX.length);
100
- let derKey;
101
- try {
102
- derKey = Buffer.from(rawPrivateKey, "base64");
103
- } catch (cause) {
104
- throw new TxError(
105
- "PRIVY_AUTH_FAILED",
106
- "Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>",
107
- cause
108
- );
109
- }
110
- let keyObject;
111
- try {
112
- keyObject = createPrivateKey({
113
- key: derKey,
114
- format: "der",
115
- type: "pkcs8"
116
- });
117
- } catch (cause) {
118
- throw new TxError(
119
- "PRIVY_AUTH_FAILED",
120
- "Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>",
121
- cause
122
- );
123
- }
124
- if (keyObject.asymmetricKeyType !== "ec") {
125
- throw new TxError(
126
- "PRIVY_AUTH_FAILED",
127
- "Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>"
128
- );
129
- }
130
- const details = keyObject.asymmetricKeyDetails;
131
- const namedCurve = details !== void 0 && "namedCurve" in details ? details.namedCurve : void 0;
132
- if (namedCurve !== void 0 && namedCurve !== "prime256v1") {
133
- throw new TxError(
134
- "PRIVY_AUTH_FAILED",
135
- "Invalid PRIVY_AUTHORIZATION_KEY format: expected wallet-auth:<base64-pkcs8-p256-private-key>"
136
- );
137
- }
138
- return keyObject;
139
- }
140
- function createPrivyAuthorizationPayload(options) {
141
- const appId = options.appId.trim();
142
- if (appId.length === 0) {
143
- throw new TxError(
144
- "PRIVY_AUTH_FAILED",
145
- "Invalid PRIVY_APP_ID format: expected non-empty string"
146
- );
147
- }
148
- const url = options.url.trim().replace(/\/+$/, "");
149
- if (url.length === 0) {
150
- throw new TxError(
151
- "PRIVY_TRANSPORT_FAILED",
152
- "Failed to build Privy authorization payload: request URL is empty"
153
- );
154
- }
155
- return {
156
- version: 1,
157
- method: options.method,
158
- url,
159
- headers: {
160
- "privy-app-id": appId,
161
- ...options.idempotencyKey !== void 0 ? { "privy-idempotency-key": options.idempotencyKey } : {}
162
- },
163
- body: options.body
164
- };
165
- }
166
- function serializePrivyAuthorizationPayload(payload) {
167
- return canonicalizeJson(payload);
168
- }
169
- function generatePrivyAuthorizationSignature(payload, authorizationKey) {
170
- const privateKey = parsePrivyAuthorizationKey(authorizationKey);
171
- const serializedPayload = serializePrivyAuthorizationPayload(payload);
172
- const signature = signWithCrypto("sha256", Buffer.from(serializedPayload), privateKey);
173
- return signature.toString("base64");
174
- }
175
- function canonicalizeJson(value) {
176
- if (value === null) {
177
- return "null";
178
- }
179
- if (typeof value === "string" || typeof value === "boolean") {
180
- return JSON.stringify(value);
181
- }
182
- if (typeof value === "number") {
183
- if (!Number.isFinite(value)) {
184
- throw new TxError(
185
- "PRIVY_TRANSPORT_FAILED",
186
- "Failed to build Privy authorization payload: JSON payload contains a non-finite number"
187
- );
188
- }
189
- return JSON.stringify(value);
190
- }
191
- if (Array.isArray(value)) {
192
- return `[${value.map((item) => canonicalizeJson(item)).join(",")}]`;
193
- }
194
- if (typeof value === "object") {
195
- const record = value;
196
- const keys = Object.keys(record).sort((left, right) => left.localeCompare(right));
197
- const entries = [];
198
- for (const key of keys) {
199
- const entryValue = record[key];
200
- if (entryValue === void 0) {
201
- continue;
202
- }
203
- entries.push(`${JSON.stringify(key)}:${canonicalizeJson(entryValue)}`);
204
- }
205
- return `{${entries.join(",")}}`;
206
- }
207
- throw new TxError(
208
- "PRIVY_TRANSPORT_FAILED",
209
- "Failed to build Privy authorization payload: JSON payload contains unsupported value type"
210
- );
211
- }
212
-
213
- // src/signers/privy-client.ts
214
- function createPrivyClient(options) {
215
- const appId = options.appId.trim();
216
- const walletId = options.walletId.trim();
217
- if (appId.length === 0) {
218
- throw new TxError(
219
- "PRIVY_AUTH_FAILED",
220
- "Invalid PRIVY_APP_ID format: expected non-empty string"
221
- );
222
- }
223
- if (walletId.length === 0) {
224
- throw new TxError(
225
- "PRIVY_AUTH_FAILED",
226
- "Invalid PRIVY_WALLET_ID format: expected non-empty string"
227
- );
228
- }
229
- const apiUrl = normalizePrivyApiUrl(options.apiUrl);
230
- const fetchImplementation = options.fetchImplementation ?? fetch;
231
- return {
232
- appId,
233
- walletId,
234
- apiUrl,
235
- async createRpcIntent(request, requestOptions) {
236
- const url = `${apiUrl}/v1/intents/wallets/${walletId}/rpc`;
237
- const payload = createPrivyAuthorizationPayload({
238
- appId,
239
- method: "POST",
240
- url,
241
- body: request,
242
- ...requestOptions?.idempotencyKey !== void 0 ? { idempotencyKey: requestOptions.idempotencyKey } : {}
243
- });
244
- const signature = generatePrivyAuthorizationSignature(payload, options.authorizationKey);
245
- return sendPrivyRequest({
246
- fetchImplementation,
247
- method: "POST",
248
- url,
249
- body: request,
250
- operation: "create rpc intent",
251
- headers: {
252
- "privy-app-id": appId,
253
- "privy-authorization-signature": signature,
254
- ...requestOptions?.idempotencyKey !== void 0 ? { "privy-idempotency-key": requestOptions.idempotencyKey } : {}
255
- }
256
- });
257
- },
258
- async getWallet() {
259
- const url = `${apiUrl}/v1/wallets/${walletId}`;
260
- return sendPrivyRequest({
261
- fetchImplementation,
262
- method: "GET",
263
- url,
264
- operation: "get wallet",
265
- headers: {
266
- "privy-app-id": appId
267
- }
268
- });
269
- },
270
- async getPolicy(policyId) {
271
- if (policyId.trim().length === 0) {
272
- throw new TxError(
273
- "PRIVY_TRANSPORT_FAILED",
274
- "Failed to build Privy policy lookup request: policy id is empty"
275
- );
276
- }
277
- const url = `${apiUrl}/v1/policies/${policyId}`;
278
- return sendPrivyRequest({
279
- fetchImplementation,
280
- method: "GET",
281
- url,
282
- operation: "get policy",
283
- headers: {
284
- "privy-app-id": appId
285
- }
286
- });
287
- }
288
- };
289
- }
290
- async function sendPrivyRequest(options) {
291
- let response;
292
- try {
293
- response = await options.fetchImplementation(options.url, {
294
- method: options.method,
295
- headers: {
296
- ...options.headers,
297
- ...options.body !== void 0 ? { "content-type": "application/json" } : {}
298
- },
299
- ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {}
300
- });
301
- } catch (cause) {
302
- throw new TxError(
303
- "PRIVY_TRANSPORT_FAILED",
304
- `Privy ${options.operation} request failed: network error`,
305
- cause
306
- );
307
- }
308
- const payload = await parseJsonResponse(response, options.operation);
309
- if (!response.ok) {
310
- const message = extractPrivyErrorMessage(payload) ?? `HTTP ${response.status}`;
311
- if (response.status === 401 || response.status === 403) {
312
- throw new TxError(
313
- "PRIVY_AUTH_FAILED",
314
- `Privy authentication failed (${response.status}): ${message}`
315
- );
316
- }
317
- throw new TxError(
318
- "PRIVY_TRANSPORT_FAILED",
319
- `Privy ${options.operation} request failed (${response.status}): ${message}`
320
- );
321
- }
322
- if (!isRecord(payload)) {
323
- throw new TxError(
324
- "PRIVY_TRANSPORT_FAILED",
325
- `Privy ${options.operation} request failed: invalid JSON response shape`
326
- );
327
- }
328
- return payload;
329
- }
330
- async function parseJsonResponse(response, operation) {
331
- const text = await response.text();
332
- if (text.trim().length === 0) {
333
- return void 0;
334
- }
335
- try {
336
- return JSON.parse(text);
337
- } catch (cause) {
338
- throw new TxError(
339
- "PRIVY_TRANSPORT_FAILED",
340
- `Privy ${operation} request failed: invalid JSON response`,
341
- cause
342
- );
343
- }
344
- }
345
- function isRecord(value) {
346
- return typeof value === "object" && value !== null;
347
- }
348
- function extractPrivyErrorMessage(payload) {
349
- if (!isRecord(payload)) {
350
- return void 0;
351
- }
352
- const directMessage = payload.message;
353
- if (typeof directMessage === "string" && directMessage.length > 0) {
354
- return directMessage;
355
- }
356
- const errorValue = payload.error;
357
- if (typeof errorValue === "string" && errorValue.length > 0) {
358
- return errorValue;
359
- }
360
- if (isRecord(errorValue)) {
361
- const nestedMessage = errorValue.message;
362
- if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
363
- return nestedMessage;
364
- }
365
- }
366
- return void 0;
367
- }
368
-
369
- // src/signers/privy.ts
370
- import { zeroAddress } from "viem";
371
- var REQUIRED_FIELDS = [
372
- "privyAppId",
373
- "privyWalletId",
374
- "privyAuthorizationKey"
375
- ];
376
- var APP_ID_REGEX = /^[A-Za-z0-9_-]{8,128}$/;
377
- var WALLET_ID_REGEX = /^[A-Za-z0-9_-]{8,128}$/;
378
- async function createPrivySigner(options) {
379
- const missing = REQUIRED_FIELDS.filter((field) => {
380
- const value = options[field];
381
- return typeof value !== "string" || value.trim().length === 0;
382
- });
383
- if (missing.length > 0) {
384
- const missingLabels = missing.map((field) => {
385
- if (field === "privyAppId") {
386
- return "PRIVY_APP_ID";
387
- }
388
- if (field === "privyWalletId") {
389
- return "PRIVY_WALLET_ID";
390
- }
391
- return "PRIVY_AUTHORIZATION_KEY";
392
- }).join(", ");
393
- throw new TxError(
394
- "PRIVY_AUTH_FAILED",
395
- `Privy signer requires configuration: missing ${missingLabels}`
396
- );
397
- }
398
- const appId = options.privyAppId?.trim() ?? "";
399
- const walletId = options.privyWalletId?.trim() ?? "";
400
- const authorizationKey = options.privyAuthorizationKey?.trim() ?? "";
401
- if (!APP_ID_REGEX.test(appId)) {
402
- throw new TxError(
403
- "PRIVY_AUTH_FAILED",
404
- "Invalid PRIVY_APP_ID format: expected 8-128 chars using letters, numbers, hyphen, or underscore"
405
- );
406
- }
407
- if (!WALLET_ID_REGEX.test(walletId)) {
408
- throw new TxError(
409
- "PRIVY_AUTH_FAILED",
410
- "Invalid PRIVY_WALLET_ID format: expected 8-128 chars using letters, numbers, hyphen, or underscore"
411
- );
412
- }
413
- parsePrivyAuthorizationKey(authorizationKey);
414
- const apiUrl = normalizePrivyApiUrl(options.privyApiUrl);
415
- const client = createPrivyClient({
416
- appId,
417
- walletId,
418
- authorizationKey,
419
- apiUrl
420
- });
421
- const account = {
422
- address: zeroAddress,
423
- type: "json-rpc"
424
- };
425
- return {
426
- provider: "privy",
427
- account,
428
- address: account.address,
429
- privy: {
430
- appId,
431
- walletId,
432
- apiUrl,
433
- client
434
- }
435
- };
436
- }
437
-
438
74
  // src/resolve-signer.ts
439
75
  function hasPrivyConfig(opts) {
440
76
  return opts.privyAppId !== void 0 || opts.privyWalletId !== void 0 || opts.privyAuthorizationKey !== void 0;
@@ -456,12 +92,13 @@ async function resolveSigner(opts) {
456
92
  });
457
93
  }
458
94
  if (opts.privy === true || hasPrivyConfig(opts)) {
459
- return createPrivySigner({
95
+ const privySigner = await createPrivySigner({
460
96
  ...opts.privyAppId !== void 0 ? { privyAppId: opts.privyAppId } : {},
461
97
  ...opts.privyWalletId !== void 0 ? { privyWalletId: opts.privyWalletId } : {},
462
98
  ...opts.privyAuthorizationKey !== void 0 ? { privyAuthorizationKey: opts.privyAuthorizationKey } : {},
463
99
  ...opts.privyApiUrl !== void 0 ? { privyApiUrl: opts.privyApiUrl } : {}
464
100
  });
101
+ return privySigner;
465
102
  }
466
103
  throw new TxError(
467
104
  "SIGNER_NOT_CONFIGURED",
@@ -504,20 +141,27 @@ function toSignerOptions(flags, env) {
504
141
  export {
505
142
  TxError,
506
143
  abstractMainnet,
144
+ attachPrivyPolicyContext,
507
145
  createAbstractClient,
508
146
  createKeystoreSigner,
509
147
  createPrivateKeySigner,
148
+ createPrivyAccount,
510
149
  createPrivyAuthorizationPayload,
511
150
  createPrivyClient,
512
151
  createPrivySigner,
513
152
  executeTx,
153
+ fetchPrivyPolicyVisibility,
514
154
  generatePrivyAuthorizationSignature,
155
+ getPrivyPolicyContext,
515
156
  normalizePrivyApiUrl,
157
+ normalizePrivyPolicy,
516
158
  parsePrivyAuthorizationKey,
159
+ preflightPrivyTransactionPolicy,
517
160
  resolveSigner,
518
161
  serializePrivyAuthorizationPayload,
519
162
  signerEnvSchema,
520
163
  signerFlagSchema,
164
+ toPrivyPolicyViolationError,
521
165
  toSignerOptions,
522
166
  toTxError
523
167
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/tx-shared",
3
- "version": "0.4.2",
3
+ "version": "0.5.1",
4
4
  "description": "Shared transaction primitives, signer types, and chain config for spectra tools",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,130 +0,0 @@
1
- import {
2
- TxError
3
- } from "./chunk-6T4D5UCR.js";
4
-
5
- // src/execute-tx.ts
6
- async function executeTx(options) {
7
- const {
8
- publicClient,
9
- walletClient,
10
- account,
11
- address,
12
- abi,
13
- functionName,
14
- chain,
15
- args,
16
- value,
17
- gasLimit,
18
- maxFeePerGas,
19
- nonce,
20
- dryRun = false
21
- } = options;
22
- let estimatedGas;
23
- try {
24
- estimatedGas = await publicClient.estimateContractGas({
25
- account,
26
- address,
27
- abi,
28
- functionName,
29
- args,
30
- value
31
- });
32
- } catch (error) {
33
- throw mapError(error, "estimation");
34
- }
35
- let simulationResult;
36
- try {
37
- const sim = await publicClient.simulateContract({
38
- account,
39
- address,
40
- abi,
41
- functionName,
42
- args,
43
- value
44
- });
45
- simulationResult = sim.result;
46
- } catch (error) {
47
- throw mapError(error, "simulation");
48
- }
49
- if (dryRun) {
50
- return {
51
- status: "dry-run",
52
- estimatedGas,
53
- simulationResult
54
- };
55
- }
56
- let hash;
57
- try {
58
- hash = await walletClient.writeContract({
59
- account,
60
- address,
61
- abi,
62
- functionName,
63
- args,
64
- value,
65
- chain,
66
- gas: gasLimit ?? estimatedGas,
67
- maxFeePerGas,
68
- nonce
69
- });
70
- } catch (error) {
71
- throw mapError(error, "submit");
72
- }
73
- let receipt;
74
- try {
75
- receipt = await publicClient.waitForTransactionReceipt({ hash });
76
- } catch (error) {
77
- throw mapError(error, "receipt");
78
- }
79
- if (receipt.status === "reverted") {
80
- throw new TxError("TX_REVERTED", `Transaction ${hash} reverted on-chain`);
81
- }
82
- return receiptToTxResult(receipt);
83
- }
84
- function receiptToTxResult(receipt) {
85
- return {
86
- hash: receipt.transactionHash,
87
- blockNumber: receipt.blockNumber,
88
- gasUsed: receipt.gasUsed,
89
- status: receipt.status === "success" ? "success" : "reverted",
90
- from: receipt.from,
91
- to: receipt.to,
92
- effectiveGasPrice: receipt.effectiveGasPrice
93
- };
94
- }
95
- function mapError(error, phase) {
96
- const msg = errorMessage(error);
97
- if (matchesInsufficientFunds(msg)) {
98
- return new TxError("INSUFFICIENT_FUNDS", `Insufficient funds: ${msg}`, error);
99
- }
100
- if (matchesNonceConflict(msg)) {
101
- return new TxError("NONCE_CONFLICT", `Nonce conflict: ${msg}`, error);
102
- }
103
- if (phase === "estimation" || phase === "simulation") {
104
- return new TxError("GAS_ESTIMATION_FAILED", `Gas estimation/simulation failed: ${msg}`, error);
105
- }
106
- if (matchesRevert(msg)) {
107
- return new TxError("TX_REVERTED", `Transaction reverted: ${msg}`, error);
108
- }
109
- return new TxError("TX_REVERTED", `Transaction failed (${phase}): ${msg}`, error);
110
- }
111
- function errorMessage(error) {
112
- if (error instanceof Error) return error.message;
113
- return String(error);
114
- }
115
- function matchesInsufficientFunds(msg) {
116
- const lower = msg.toLowerCase();
117
- return lower.includes("insufficient funds") || lower.includes("insufficient balance") || lower.includes("sender doesn't have enough funds");
118
- }
119
- function matchesNonceConflict(msg) {
120
- const lower = msg.toLowerCase();
121
- return lower.includes("nonce too low") || lower.includes("nonce has already been used") || lower.includes("already known") || lower.includes("replacement transaction underpriced");
122
- }
123
- function matchesRevert(msg) {
124
- const lower = msg.toLowerCase();
125
- return lower.includes("revert") || lower.includes("execution reverted") || lower.includes("transaction failed");
126
- }
127
-
128
- export {
129
- executeTx
130
- };