@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 +2 -2
- package/dist/index.d.ts +36 -5
- package/dist/index.js +225 -9
- package/package.json +1 -1
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
|
|
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
|
-
-
|
|
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:
|
|
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
|
-
*
|
|
152
|
-
*
|
|
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 (!
|
|
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
|
|
562
|
+
function isRecord2(value) {
|
|
346
563
|
return typeof value === "object" && value !== null;
|
|
347
564
|
}
|
|
348
565
|
function extractPrivyErrorMessage(payload) {
|
|
349
|
-
if (!
|
|
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 (
|
|
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
|
-
|
|
423
|
-
|
|
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,
|