@spectratools/tx-shared 0.4.2 → 0.4.3

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 CHANGED
@@ -56,7 +56,7 @@ export PRIVY_WALLET_ID="..."
56
56
  export PRIVY_AUTHORIZATION_KEY="..."
57
57
  ```
58
58
 
59
- > Privy signer execution is intentionally stubbed right now and throws `PRIVY_AUTH_FAILED` with a deterministic message. Full implementation is tracked in [issue #117](https://github.com/spectra-the-bot/spectra-tools/issues/117).
59
+ > Privy signer resolution now performs wallet address lookup and exposes a Privy-backed account helper for `eth_sendTransaction` intents. Additional signing methods (for example `personal_sign` and typed-data) are tracked in [issue #117](https://github.com/spectra-the-bot/spectra-tools/issues/117).
60
60
 
61
61
  ## `resolveSigner()` usage
62
62
 
@@ -185,7 +185,7 @@ try {
185
185
  - ensure keystore is valid V3 JSON
186
186
  - **`PRIVY_AUTH_FAILED`**
187
187
  - verify all `PRIVY_*` variables are set
188
- - note: provider is not live yet (see #117)
188
+ - check signer/owner policy constraints for the Privy wallet
189
189
  - **`GAS_ESTIMATION_FAILED` / `TX_REVERTED`**
190
190
  - validate function args and `value`
191
191
  - run with `dryRun: true` first
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@ export { TxError, TxErrorCode, toTxError } from './errors.js';
4
4
  export { abstractMainnet, createAbstractClient } from './chain.js';
5
5
  export { DryRunResult, ExecuteTxOptions, executeTx } from './execute-tx.js';
6
6
  import { z } from 'incur';
7
+ import { Account, Address, Hex, Hash } from 'viem';
7
8
  import { KeyObject } from 'node:crypto';
8
- import 'viem';
9
9
 
10
10
  /**
11
11
  * Resolve the active signer provider using deterministic precedence:
@@ -65,11 +65,17 @@ interface PrivyRpcIntentRequest {
65
65
  params: Record<string, unknown>;
66
66
  [key: string]: unknown;
67
67
  }
68
+ type PrivyIntentStatus = 'pending' | 'executed' | 'failed' | 'expired' | 'rejected' | 'dismissed' | string;
68
69
  interface PrivyRpcIntentResponse {
69
70
  intent_id: string;
70
- status: string;
71
+ status: PrivyIntentStatus;
71
72
  resource_id?: string;
72
73
  request_details?: Record<string, unknown>;
74
+ dismissal_reason?: string;
75
+ action_result?: {
76
+ response_body?: Record<string, unknown>;
77
+ [key: string]: unknown;
78
+ };
73
79
  [key: string]: unknown;
74
80
  }
75
81
  interface PrivyWalletResponse {
@@ -105,6 +111,30 @@ interface PrivyClient {
105
111
  }
106
112
  declare function createPrivyClient(options: CreatePrivyClientOptions): PrivyClient;
107
113
 
114
+ type PrivyNumberish = bigint | number | string;
115
+ interface PrivySendTransactionRequest {
116
+ to?: Address;
117
+ data?: Hex;
118
+ value?: PrivyNumberish;
119
+ nonce?: PrivyNumberish;
120
+ gas?: PrivyNumberish;
121
+ gasPrice?: PrivyNumberish;
122
+ maxFeePerGas?: PrivyNumberish;
123
+ maxPriorityFeePerGas?: PrivyNumberish;
124
+ chainId?: PrivyNumberish;
125
+ type?: number;
126
+ }
127
+ type PrivyAccount = Extract<Account, {
128
+ type: 'json-rpc';
129
+ }> & {
130
+ sendTransaction: (request: PrivySendTransactionRequest) => Promise<Hash>;
131
+ };
132
+ interface CreatePrivyAccountOptions {
133
+ client: PrivyClient;
134
+ chainId?: number;
135
+ }
136
+ declare function createPrivyAccount(options: CreatePrivyAccountOptions): Promise<PrivyAccount>;
137
+
108
138
  type PrivyAuthorizationMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
109
139
  interface PrivyAuthorizationPayloadHeaders {
110
140
  'privy-app-id': string;
@@ -137,6 +167,7 @@ interface PrivySignerOptions {
137
167
  privyApiUrl?: string;
138
168
  }
139
169
  interface PrivySigner extends TxSigner {
170
+ account: PrivyAccount;
140
171
  provider: 'privy';
141
172
  privy: {
142
173
  appId: string;
@@ -148,9 +179,9 @@ interface PrivySigner extends TxSigner {
148
179
  /**
149
180
  * Create a Privy signer envelope with reusable transport and request-signing primitives.
150
181
  *
151
- * This initializes the shared client utilities used by tx-shared Privy integrations.
152
- * Transaction account execution is implemented in follow-up work for issue #191.
182
+ * The resolved account is backed by Privy RPC intents and includes a `sendTransaction`
183
+ * helper that submits `eth_sendTransaction` intents and returns the resulting tx hash.
153
184
  */
154
185
  declare function createPrivySigner(options: PrivySignerOptions): Promise<PrivySigner>;
155
186
 
156
- export { type KeystoreSignerOptions, type PrivyAuthorizationPayload, type PrivyAuthorizationPayloadHeaders, type PrivyClient, type PrivySigner, type PrivySignerOptions, type SignerEnv, type SignerFlags, SignerOptions, TxSigner, createKeystoreSigner, createPrivateKeySigner, createPrivyAuthorizationPayload, createPrivyClient, createPrivySigner, generatePrivyAuthorizationSignature, normalizePrivyApiUrl, parsePrivyAuthorizationKey, resolveSigner, serializePrivyAuthorizationPayload, signerEnvSchema, signerFlagSchema, toSignerOptions };
187
+ export { type KeystoreSignerOptions, type PrivyAccount, type PrivyAuthorizationPayload, type PrivyAuthorizationPayloadHeaders, type PrivyClient, type PrivySendTransactionRequest, type PrivySigner, type PrivySignerOptions, type SignerEnv, type SignerFlags, SignerOptions, TxSigner, createKeystoreSigner, createPrivateKeySigner, createPrivyAccount, createPrivyAuthorizationPayload, createPrivyClient, createPrivySigner, generatePrivyAuthorizationSignature, normalizePrivyApiUrl, parsePrivyAuthorizationKey, resolveSigner, serializePrivyAuthorizationPayload, signerEnvSchema, signerFlagSchema, toSignerOptions };
package/dist/index.js CHANGED
@@ -57,6 +57,223 @@ function createKeystoreSigner(options) {
57
57
  return { account, address: account.address, provider: "keystore" };
58
58
  }
59
59
 
60
+ // src/signers/privy-account.ts
61
+ import { isAddress } from "viem";
62
+ var DEFAULT_CHAIN_ID = 2741;
63
+ var HASH_REGEX = /^0x[a-fA-F0-9]{64}$/;
64
+ async function createPrivyAccount(options) {
65
+ const wallet = await options.client.getWallet();
66
+ if (typeof wallet.address !== "string" || !isAddress(wallet.address)) {
67
+ throw new TxError(
68
+ "PRIVY_TRANSPORT_FAILED",
69
+ "Privy get wallet request failed: wallet address is missing or invalid"
70
+ );
71
+ }
72
+ const address = wallet.address;
73
+ const defaultChainId = options.chainId ?? DEFAULT_CHAIN_ID;
74
+ return {
75
+ address,
76
+ type: "json-rpc",
77
+ async sendTransaction(request) {
78
+ const chainId = normalizeChainId(request.chainId ?? defaultChainId);
79
+ const rpcRequest = createSendTransactionRpcRequest(address, chainId, request);
80
+ const response = await options.client.createRpcIntent(rpcRequest);
81
+ return parsePrivyTransactionHash(response);
82
+ }
83
+ };
84
+ }
85
+ function createSendTransactionRpcRequest(from, chainId, request) {
86
+ const transaction = {
87
+ from
88
+ };
89
+ if (request.to !== void 0) {
90
+ transaction.to = request.to;
91
+ }
92
+ if (request.data !== void 0) {
93
+ transaction.data = request.data;
94
+ }
95
+ if (request.value !== void 0) {
96
+ transaction.value = normalizeNumberish(request.value, "value");
97
+ }
98
+ if (request.nonce !== void 0) {
99
+ transaction.nonce = normalizeNumberish(request.nonce, "nonce");
100
+ }
101
+ if (request.gas !== void 0) {
102
+ transaction.gas_limit = normalizeNumberish(request.gas, "gas");
103
+ }
104
+ if (request.gasPrice !== void 0) {
105
+ transaction.gas_price = normalizeNumberish(request.gasPrice, "gasPrice");
106
+ }
107
+ if (request.maxFeePerGas !== void 0) {
108
+ transaction.max_fee_per_gas = normalizeNumberish(request.maxFeePerGas, "maxFeePerGas");
109
+ }
110
+ if (request.maxPriorityFeePerGas !== void 0) {
111
+ transaction.max_priority_fee_per_gas = normalizeNumberish(
112
+ request.maxPriorityFeePerGas,
113
+ "maxPriorityFeePerGas"
114
+ );
115
+ }
116
+ if (request.type !== void 0) {
117
+ transaction.type = request.type;
118
+ }
119
+ transaction.chain_id = chainId;
120
+ return {
121
+ method: "eth_sendTransaction",
122
+ caip2: `eip155:${chainId}`,
123
+ params: {
124
+ transaction
125
+ }
126
+ };
127
+ }
128
+ function parsePrivyTransactionHash(response) {
129
+ const intentId = response.intent_id;
130
+ if (response.status !== "executed") {
131
+ const reason = extractIntentReason(response) ?? "no reason provided";
132
+ if (response.status === "failed") {
133
+ throw new TxError("TX_REVERTED", `Privy rpc intent ${intentId} failed: ${reason}`);
134
+ }
135
+ if (response.status === "rejected" || response.status === "dismissed" || response.status === "expired") {
136
+ throw new TxError(
137
+ "PRIVY_AUTH_FAILED",
138
+ `Privy rpc intent ${intentId} ${response.status}: ${reason}`
139
+ );
140
+ }
141
+ throw new TxError(
142
+ "PRIVY_TRANSPORT_FAILED",
143
+ `Privy rpc intent ${intentId} did not execute (status: ${response.status})`
144
+ );
145
+ }
146
+ const hash = extractIntentHash(response);
147
+ if (hash === void 0 || !HASH_REGEX.test(hash)) {
148
+ throw new TxError(
149
+ "PRIVY_TRANSPORT_FAILED",
150
+ `Privy rpc intent ${intentId} executed without a valid transaction hash`
151
+ );
152
+ }
153
+ return hash;
154
+ }
155
+ function extractIntentHash(response) {
156
+ if (!isRecord(response.action_result)) {
157
+ return void 0;
158
+ }
159
+ const responseBody = response.action_result.response_body;
160
+ if (!isRecord(responseBody)) {
161
+ return void 0;
162
+ }
163
+ const data = responseBody.data;
164
+ if (!isRecord(data)) {
165
+ return void 0;
166
+ }
167
+ return typeof data.hash === "string" ? data.hash : void 0;
168
+ }
169
+ function extractIntentReason(response) {
170
+ if (typeof response.dismissal_reason === "string" && response.dismissal_reason.length > 0) {
171
+ return response.dismissal_reason;
172
+ }
173
+ if (isRecord(response.action_result)) {
174
+ const responseBody = response.action_result.response_body;
175
+ if (isRecord(responseBody)) {
176
+ const error = responseBody.error;
177
+ if (typeof error === "string" && error.length > 0) {
178
+ return error;
179
+ }
180
+ if (isRecord(error)) {
181
+ const errorMessage = error.message;
182
+ if (typeof errorMessage === "string" && errorMessage.length > 0) {
183
+ return errorMessage;
184
+ }
185
+ }
186
+ const message = responseBody.message;
187
+ if (typeof message === "string" && message.length > 0) {
188
+ return message;
189
+ }
190
+ }
191
+ }
192
+ return void 0;
193
+ }
194
+ function normalizeChainId(chainId) {
195
+ if (typeof chainId === "number") {
196
+ if (!Number.isInteger(chainId) || chainId <= 0) {
197
+ throw new TxError(
198
+ "PRIVY_TRANSPORT_FAILED",
199
+ "Failed to build Privy eth_sendTransaction payload: chainId must be a positive integer"
200
+ );
201
+ }
202
+ return chainId;
203
+ }
204
+ if (typeof chainId === "bigint") {
205
+ if (chainId <= 0n || chainId > BigInt(Number.MAX_SAFE_INTEGER)) {
206
+ throw new TxError(
207
+ "PRIVY_TRANSPORT_FAILED",
208
+ "Failed to build Privy eth_sendTransaction payload: chainId must be a positive safe integer"
209
+ );
210
+ }
211
+ return Number(chainId);
212
+ }
213
+ const trimmed = chainId.trim();
214
+ if (trimmed.length === 0) {
215
+ throw new TxError(
216
+ "PRIVY_TRANSPORT_FAILED",
217
+ "Failed to build Privy eth_sendTransaction payload: chainId is empty"
218
+ );
219
+ }
220
+ if (/^0x[0-9a-fA-F]+$/.test(trimmed)) {
221
+ const parsed2 = Number.parseInt(trimmed.slice(2), 16);
222
+ if (!Number.isSafeInteger(parsed2) || parsed2 <= 0) {
223
+ throw new TxError(
224
+ "PRIVY_TRANSPORT_FAILED",
225
+ "Failed to build Privy eth_sendTransaction payload: chainId must be a positive safe integer"
226
+ );
227
+ }
228
+ return parsed2;
229
+ }
230
+ if (!/^[0-9]+$/.test(trimmed)) {
231
+ throw new TxError(
232
+ "PRIVY_TRANSPORT_FAILED",
233
+ "Failed to build Privy eth_sendTransaction payload: chainId must be a positive safe integer"
234
+ );
235
+ }
236
+ const parsed = Number(trimmed);
237
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
238
+ throw new TxError(
239
+ "PRIVY_TRANSPORT_FAILED",
240
+ "Failed to build Privy eth_sendTransaction payload: chainId must be a positive safe integer"
241
+ );
242
+ }
243
+ return parsed;
244
+ }
245
+ function normalizeNumberish(value, fieldName) {
246
+ if (typeof value === "bigint") {
247
+ if (value < 0n) {
248
+ throw new TxError(
249
+ "PRIVY_TRANSPORT_FAILED",
250
+ `Failed to build Privy eth_sendTransaction payload: ${fieldName} cannot be negative`
251
+ );
252
+ }
253
+ return value.toString(10);
254
+ }
255
+ if (typeof value === "number") {
256
+ if (!Number.isFinite(value) || value < 0) {
257
+ throw new TxError(
258
+ "PRIVY_TRANSPORT_FAILED",
259
+ `Failed to build Privy eth_sendTransaction payload: ${fieldName} must be a non-negative number`
260
+ );
261
+ }
262
+ return Number.isInteger(value) ? value : value.toString();
263
+ }
264
+ const trimmed = value.trim();
265
+ if (trimmed.length === 0) {
266
+ throw new TxError(
267
+ "PRIVY_TRANSPORT_FAILED",
268
+ `Failed to build Privy eth_sendTransaction payload: ${fieldName} is empty`
269
+ );
270
+ }
271
+ return trimmed;
272
+ }
273
+ function isRecord(value) {
274
+ return typeof value === "object" && value !== null;
275
+ }
276
+
60
277
  // src/signers/privy-signature.ts
61
278
  import { createPrivateKey, sign as signWithCrypto } from "crypto";
62
279
  var PRIVY_AUTHORIZATION_KEY_PREFIX = "wallet-auth:";
@@ -319,7 +536,7 @@ async function sendPrivyRequest(options) {
319
536
  `Privy ${options.operation} request failed (${response.status}): ${message}`
320
537
  );
321
538
  }
322
- if (!isRecord(payload)) {
539
+ if (!isRecord2(payload)) {
323
540
  throw new TxError(
324
541
  "PRIVY_TRANSPORT_FAILED",
325
542
  `Privy ${options.operation} request failed: invalid JSON response shape`
@@ -342,11 +559,11 @@ async function parseJsonResponse(response, operation) {
342
559
  );
343
560
  }
344
561
  }
345
- function isRecord(value) {
562
+ function isRecord2(value) {
346
563
  return typeof value === "object" && value !== null;
347
564
  }
348
565
  function extractPrivyErrorMessage(payload) {
349
- if (!isRecord(payload)) {
566
+ if (!isRecord2(payload)) {
350
567
  return void 0;
351
568
  }
352
569
  const directMessage = payload.message;
@@ -357,7 +574,7 @@ function extractPrivyErrorMessage(payload) {
357
574
  if (typeof errorValue === "string" && errorValue.length > 0) {
358
575
  return errorValue;
359
576
  }
360
- if (isRecord(errorValue)) {
577
+ if (isRecord2(errorValue)) {
361
578
  const nestedMessage = errorValue.message;
362
579
  if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
363
580
  return nestedMessage;
@@ -367,7 +584,6 @@ function extractPrivyErrorMessage(payload) {
367
584
  }
368
585
 
369
586
  // src/signers/privy.ts
370
- import { zeroAddress } from "viem";
371
587
  var REQUIRED_FIELDS = [
372
588
  "privyAppId",
373
589
  "privyWalletId",
@@ -418,10 +634,9 @@ async function createPrivySigner(options) {
418
634
  authorizationKey,
419
635
  apiUrl
420
636
  });
421
- const account = {
422
- address: zeroAddress,
423
- type: "json-rpc"
424
- };
637
+ const account = await createPrivyAccount({
638
+ client
639
+ });
425
640
  return {
426
641
  provider: "privy",
427
642
  account,
@@ -507,6 +722,7 @@ export {
507
722
  createAbstractClient,
508
723
  createKeystoreSigner,
509
724
  createPrivateKeySigner,
725
+ createPrivyAccount,
510
726
  createPrivyAuthorizationPayload,
511
727
  createPrivyClient,
512
728
  createPrivySigner,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectratools/tx-shared",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Shared transaction primitives, signer types, and chain config for spectra tools",
5
5
  "type": "module",
6
6
  "license": "MIT",