@spectratools/tx-shared 0.2.0 → 0.4.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 +198 -1
- package/dist/chunk-4XI6TBKX.js +130 -0
- package/dist/execute-tx.d.ts +63 -0
- package/dist/execute-tx.js +7 -0
- package/dist/index.d.ts +70 -1
- package/dist/index.js +148 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -1,3 +1,200 @@
|
|
|
1
1
|
# @spectratools/tx-shared
|
|
2
2
|
|
|
3
|
-
Shared transaction primitives for Spectra tools:
|
|
3
|
+
Shared transaction primitives for Spectra tools:
|
|
4
|
+
|
|
5
|
+
- signer resolution (`resolveSigner`)
|
|
6
|
+
- transaction lifecycle execution (`executeTx`)
|
|
7
|
+
- shared signer CLI/env parsing helpers (`toSignerOptions`)
|
|
8
|
+
- structured transaction errors (`TxError`)
|
|
9
|
+
- Abstract chain helpers (`abstractMainnet`, `createAbstractClient`)
|
|
10
|
+
|
|
11
|
+
This package is designed for consuming CLIs (for example `@spectratools/assembly-cli`) so write-capable commands follow the same signer precedence, dry-run behavior, and error handling.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add @spectratools/tx-shared
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Signer Providers
|
|
20
|
+
|
|
21
|
+
`resolveSigner()` uses deterministic precedence:
|
|
22
|
+
|
|
23
|
+
1. `privateKey`
|
|
24
|
+
2. `keystorePath` (+ `keystorePassword`)
|
|
25
|
+
3. `privy` / `PRIVY_*`
|
|
26
|
+
|
|
27
|
+
If no provider is configured, it throws `TxError` with code `SIGNER_NOT_CONFIGURED`.
|
|
28
|
+
|
|
29
|
+
### Provider setup
|
|
30
|
+
|
|
31
|
+
#### 1) Private key
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
export PRIVATE_KEY="0x<64-hex-chars>"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or pass `privateKey` directly in code.
|
|
38
|
+
|
|
39
|
+
#### 2) Keystore
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# CLI convention
|
|
43
|
+
--keystore /path/to/keystore.json --password "$KEYSTORE_PASSWORD"
|
|
44
|
+
|
|
45
|
+
# env fallback for password
|
|
46
|
+
export KEYSTORE_PASSWORD="..."
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`resolveSigner()` requires a password when `keystorePath` is provided.
|
|
50
|
+
|
|
51
|
+
#### 3) Privy
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
export PRIVY_APP_ID="..."
|
|
55
|
+
export PRIVY_WALLET_ID="..."
|
|
56
|
+
export PRIVY_AUTHORIZATION_KEY="..."
|
|
57
|
+
```
|
|
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).
|
|
60
|
+
|
|
61
|
+
## `resolveSigner()` usage
|
|
62
|
+
|
|
63
|
+
### Direct options
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { resolveSigner } from '@spectratools/tx-shared';
|
|
67
|
+
|
|
68
|
+
const signer = await resolveSigner({
|
|
69
|
+
privateKey: process.env.PRIVATE_KEY,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(signer.provider); // 'private-key' | 'keystore' | 'privy'
|
|
73
|
+
console.log(signer.address); // 0x...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### From shared CLI flags + env
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import {
|
|
80
|
+
resolveSigner,
|
|
81
|
+
signerEnvSchema,
|
|
82
|
+
signerFlagSchema,
|
|
83
|
+
toSignerOptions,
|
|
84
|
+
} from '@spectratools/tx-shared';
|
|
85
|
+
|
|
86
|
+
const flags = signerFlagSchema.parse({
|
|
87
|
+
'private-key': process.env.PRIVATE_KEY,
|
|
88
|
+
privy: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const env = signerEnvSchema.parse(process.env);
|
|
92
|
+
const signer = await resolveSigner(toSignerOptions(flags, env));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## `executeTx()` lifecycle
|
|
96
|
+
|
|
97
|
+
`executeTx()` performs this flow:
|
|
98
|
+
|
|
99
|
+
1. estimate gas (`estimateContractGas`)
|
|
100
|
+
2. simulate (`simulateContract`)
|
|
101
|
+
3. submit (`writeContract`) unless `dryRun: true`
|
|
102
|
+
4. wait for receipt (`waitForTransactionReceipt`)
|
|
103
|
+
5. normalize result into a shared output shape
|
|
104
|
+
|
|
105
|
+
### Live transaction example
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { executeTx } from '@spectratools/tx-shared';
|
|
109
|
+
|
|
110
|
+
const result = await executeTx({
|
|
111
|
+
publicClient,
|
|
112
|
+
walletClient,
|
|
113
|
+
account: signer.account,
|
|
114
|
+
address,
|
|
115
|
+
abi,
|
|
116
|
+
functionName: 'register',
|
|
117
|
+
value: registrationFee,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
console.log(result.hash, result.status, result.gasUsed.toString());
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Dry-run example
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const result = await executeTx({
|
|
127
|
+
publicClient,
|
|
128
|
+
walletClient,
|
|
129
|
+
account: signer.account,
|
|
130
|
+
address,
|
|
131
|
+
abi,
|
|
132
|
+
functionName: 'register',
|
|
133
|
+
value: registrationFee,
|
|
134
|
+
dryRun: true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (result.status === 'dry-run') {
|
|
138
|
+
console.log('estimatedGas', result.estimatedGas.toString());
|
|
139
|
+
console.log('simulationResult', result.simulationResult);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Structured errors
|
|
144
|
+
|
|
145
|
+
`executeTx()` and signer helpers throw `TxError` with stable `code` values:
|
|
146
|
+
|
|
147
|
+
- `INSUFFICIENT_FUNDS`
|
|
148
|
+
- `NONCE_CONFLICT`
|
|
149
|
+
- `GAS_ESTIMATION_FAILED`
|
|
150
|
+
- `TX_REVERTED`
|
|
151
|
+
- `SIGNER_NOT_CONFIGURED`
|
|
152
|
+
- `KEYSTORE_DECRYPT_FAILED`
|
|
153
|
+
- `PRIVY_AUTH_FAILED`
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { TxError } from '@spectratools/tx-shared';
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// resolveSigner(...) + executeTx(...)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof TxError) {
|
|
162
|
+
switch (error.code) {
|
|
163
|
+
case 'INSUFFICIENT_FUNDS':
|
|
164
|
+
case 'NONCE_CONFLICT':
|
|
165
|
+
case 'GAS_ESTIMATION_FAILED':
|
|
166
|
+
case 'TX_REVERTED':
|
|
167
|
+
case 'SIGNER_NOT_CONFIGURED':
|
|
168
|
+
case 'KEYSTORE_DECRYPT_FAILED':
|
|
169
|
+
case 'PRIVY_AUTH_FAILED':
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Troubleshooting
|
|
179
|
+
|
|
180
|
+
- **`SIGNER_NOT_CONFIGURED`**
|
|
181
|
+
- check signer input precedence
|
|
182
|
+
- confirm at least one provider is configured
|
|
183
|
+
- **`KEYSTORE_DECRYPT_FAILED`**
|
|
184
|
+
- verify file path and password
|
|
185
|
+
- ensure keystore is valid V3 JSON
|
|
186
|
+
- **`PRIVY_AUTH_FAILED`**
|
|
187
|
+
- verify all `PRIVY_*` variables are set
|
|
188
|
+
- note: provider is not live yet (see #117)
|
|
189
|
+
- **`GAS_ESTIMATION_FAILED` / `TX_REVERTED`**
|
|
190
|
+
- validate function args and `value`
|
|
191
|
+
- run with `dryRun: true` first
|
|
192
|
+
- **`NONCE_CONFLICT`**
|
|
193
|
+
- refresh nonce and retry once with an explicit override if needed
|
|
194
|
+
|
|
195
|
+
## Consumer integration examples
|
|
196
|
+
|
|
197
|
+
- tx-shared assembly-style example:
|
|
198
|
+
- [`src/examples/assembly-write.ts`](./src/examples/assembly-write.ts)
|
|
199
|
+
- assembly consumer reference wiring:
|
|
200
|
+
- [`../assembly/src/examples/tx-shared-register.ts`](../assembly/src/examples/tx-shared-register.ts)
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PublicClient, WalletClient, Account, Address, Abi, Chain } from 'viem';
|
|
2
|
+
import { TxResult } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** Options for the {@link executeTx} lifecycle. */
|
|
5
|
+
interface ExecuteTxOptions {
|
|
6
|
+
/** Viem public client used for gas estimation, simulation, and receipt retrieval. */
|
|
7
|
+
publicClient: PublicClient;
|
|
8
|
+
/** Viem wallet client used to submit the transaction. */
|
|
9
|
+
walletClient: WalletClient;
|
|
10
|
+
/** Signer account. */
|
|
11
|
+
account: Account;
|
|
12
|
+
/** Target contract address. */
|
|
13
|
+
address: Address;
|
|
14
|
+
/** Contract ABI (must include the target function). */
|
|
15
|
+
abi: Abi;
|
|
16
|
+
/** Name of the contract function to call. */
|
|
17
|
+
functionName: string;
|
|
18
|
+
/** Optional chain to use for the transaction. */
|
|
19
|
+
chain?: Chain;
|
|
20
|
+
/** Arguments passed to the contract function. */
|
|
21
|
+
args?: unknown[];
|
|
22
|
+
/** Native value (in wei) to send with the transaction. */
|
|
23
|
+
value?: bigint;
|
|
24
|
+
/** Gas limit override. When provided the estimate is still performed but this value is used for submission. */
|
|
25
|
+
gasLimit?: bigint;
|
|
26
|
+
/** Max fee per gas override (EIP-1559). */
|
|
27
|
+
maxFeePerGas?: bigint;
|
|
28
|
+
/** Nonce override. */
|
|
29
|
+
nonce?: number;
|
|
30
|
+
/**
|
|
31
|
+
* When `true` the transaction is simulated but **not** broadcast.
|
|
32
|
+
* Returns a {@link DryRunResult} instead of a {@link TxResult}.
|
|
33
|
+
*/
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** Result returned when `dryRun` is `true`. */
|
|
37
|
+
interface DryRunResult {
|
|
38
|
+
/** Discriminator — always `'dry-run'` for dry-run results. */
|
|
39
|
+
status: 'dry-run';
|
|
40
|
+
/** Estimated gas units for the call. */
|
|
41
|
+
estimatedGas: bigint;
|
|
42
|
+
/** Simulated return value from `simulateContract`. */
|
|
43
|
+
simulationResult: unknown;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Execute a full transaction lifecycle:
|
|
47
|
+
*
|
|
48
|
+
* 1. **Estimate** gas via `publicClient.estimateContractGas`.
|
|
49
|
+
* 2. **Simulate** contract call via `publicClient.simulateContract`.
|
|
50
|
+
* - If `dryRun` is `true`, return a {@link DryRunResult} without broadcasting.
|
|
51
|
+
* 3. **Submit** the transaction via `walletClient.writeContract`.
|
|
52
|
+
* 4. **Wait** for the receipt via `publicClient.waitForTransactionReceipt`.
|
|
53
|
+
* 5. **Normalize** the receipt into a {@link TxResult}.
|
|
54
|
+
*
|
|
55
|
+
* Errors thrown by viem are mapped to structured {@link TxError} codes:
|
|
56
|
+
* - `INSUFFICIENT_FUNDS` — sender lacks balance for value + gas.
|
|
57
|
+
* - `NONCE_CONFLICT` — nonce already used or too low.
|
|
58
|
+
* - `GAS_ESTIMATION_FAILED` — gas estimation or simulation reverted.
|
|
59
|
+
* - `TX_REVERTED` — on-chain revert (includes reason when available).
|
|
60
|
+
*/
|
|
61
|
+
declare function executeTx(options: ExecuteTxOptions): Promise<TxResult | DryRunResult>;
|
|
62
|
+
|
|
63
|
+
export { type DryRunResult, type ExecuteTxOptions, executeTx };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,73 @@
|
|
|
1
|
-
|
|
1
|
+
import { SignerOptions, TxSigner } from './types.js';
|
|
2
|
+
export { SignerProvider, TxResult } from './types.js';
|
|
2
3
|
export { TxError, TxErrorCode, toTxError } from './errors.js';
|
|
3
4
|
export { abstractMainnet, createAbstractClient } from './chain.js';
|
|
5
|
+
export { DryRunResult, ExecuteTxOptions, executeTx } from './execute-tx.js';
|
|
6
|
+
import { z } from 'incur';
|
|
4
7
|
import 'viem';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the active signer provider using deterministic precedence:
|
|
11
|
+
* private key -> keystore -> privy -> SIGNER_NOT_CONFIGURED.
|
|
12
|
+
*/
|
|
13
|
+
declare function resolveSigner(opts: SignerOptions): Promise<TxSigner>;
|
|
14
|
+
|
|
15
|
+
/** Shared signer-related CLI flags for write-capable commands. */
|
|
16
|
+
declare const signerFlagSchema: z.ZodObject<{
|
|
17
|
+
'private-key': z.ZodOptional<z.ZodString>;
|
|
18
|
+
keystore: z.ZodOptional<z.ZodString>;
|
|
19
|
+
password: z.ZodOptional<z.ZodString>;
|
|
20
|
+
privy: z.ZodDefault<z.ZodBoolean>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
/** Shared signer-related environment variables for write-capable commands. */
|
|
23
|
+
declare const signerEnvSchema: z.ZodObject<{
|
|
24
|
+
PRIVATE_KEY: z.ZodOptional<z.ZodString>;
|
|
25
|
+
KEYSTORE_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
26
|
+
PRIVY_APP_ID: z.ZodOptional<z.ZodString>;
|
|
27
|
+
PRIVY_WALLET_ID: z.ZodOptional<z.ZodString>;
|
|
28
|
+
PRIVY_AUTHORIZATION_KEY: z.ZodOptional<z.ZodString>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
type SignerFlags = z.infer<typeof signerFlagSchema>;
|
|
31
|
+
type SignerEnv = z.infer<typeof signerEnvSchema>;
|
|
32
|
+
/** Map parsed CLI context into tx-shared SignerOptions. */
|
|
33
|
+
declare function toSignerOptions(flags: SignerFlags, env: SignerEnv): SignerOptions;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a {@link TxSigner} from a raw private key.
|
|
37
|
+
*
|
|
38
|
+
* @param privateKey - `0x`-prefixed 32-byte hex string.
|
|
39
|
+
* @returns A signer backed by `viem/accounts.privateKeyToAccount`.
|
|
40
|
+
* @throws {TxError} `SIGNER_NOT_CONFIGURED` when the key format is invalid.
|
|
41
|
+
*/
|
|
42
|
+
declare function createPrivateKeySigner(privateKey: string): TxSigner;
|
|
43
|
+
|
|
44
|
+
/** Options for {@link createKeystoreSigner}. */
|
|
45
|
+
interface KeystoreSignerOptions {
|
|
46
|
+
/** Path to the V3 keystore JSON file. */
|
|
47
|
+
keystorePath: string;
|
|
48
|
+
/** Password used to decrypt the keystore. */
|
|
49
|
+
keystorePassword: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a {@link TxSigner} by decrypting a V3 keystore file.
|
|
53
|
+
*
|
|
54
|
+
* Uses `ox` for key derivation (scrypt / pbkdf2) and AES-128-CTR decryption.
|
|
55
|
+
*
|
|
56
|
+
* @throws {TxError} `KEYSTORE_DECRYPT_FAILED` when the file cannot be read, parsed, or decrypted.
|
|
57
|
+
*/
|
|
58
|
+
declare function createKeystoreSigner(options: KeystoreSignerOptions): TxSigner;
|
|
59
|
+
|
|
60
|
+
interface PrivySignerOptions {
|
|
61
|
+
privyAppId?: string;
|
|
62
|
+
privyWalletId?: string;
|
|
63
|
+
privyAuthorizationKey?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Privy signer adapter entrypoint.
|
|
67
|
+
*
|
|
68
|
+
* Full Privy integration is tracked in issue #117. Until that lands,
|
|
69
|
+
* this adapter provides deterministic, structured failures for callers.
|
|
70
|
+
*/
|
|
71
|
+
declare function createPrivySigner(options: PrivySignerOptions): Promise<TxSigner>;
|
|
72
|
+
|
|
73
|
+
export { type KeystoreSignerOptions, type PrivySignerOptions, type SignerEnv, type SignerFlags, SignerOptions, TxSigner, createKeystoreSigner, createPrivateKeySigner, createPrivySigner, resolveSigner, signerEnvSchema, signerFlagSchema, toSignerOptions };
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,162 @@ import {
|
|
|
2
2
|
abstractMainnet,
|
|
3
3
|
createAbstractClient
|
|
4
4
|
} from "./chunk-P4ACSL6N.js";
|
|
5
|
+
import {
|
|
6
|
+
executeTx
|
|
7
|
+
} from "./chunk-4XI6TBKX.js";
|
|
5
8
|
import {
|
|
6
9
|
TxError,
|
|
7
10
|
toTxError
|
|
8
11
|
} from "./chunk-6T4D5UCR.js";
|
|
9
12
|
import "./chunk-6F4PWJZI.js";
|
|
13
|
+
|
|
14
|
+
// src/signers/private-key.ts
|
|
15
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
16
|
+
var PRIVATE_KEY_REGEX = /^0x[0-9a-fA-F]{64}$/;
|
|
17
|
+
function createPrivateKeySigner(privateKey) {
|
|
18
|
+
if (!PRIVATE_KEY_REGEX.test(privateKey)) {
|
|
19
|
+
throw new TxError(
|
|
20
|
+
"SIGNER_NOT_CONFIGURED",
|
|
21
|
+
"Invalid private key format: expected 0x-prefixed 32-byte hex string"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const account = privateKeyToAccount(privateKey);
|
|
25
|
+
return { account, address: account.address, provider: "private-key" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/signers/keystore.ts
|
|
29
|
+
import { readFileSync } from "fs";
|
|
30
|
+
import { Keystore } from "ox";
|
|
31
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
32
|
+
function createKeystoreSigner(options) {
|
|
33
|
+
const { keystorePath, keystorePassword } = options;
|
|
34
|
+
let keystoreJson;
|
|
35
|
+
try {
|
|
36
|
+
const raw = readFileSync(keystorePath, "utf-8");
|
|
37
|
+
keystoreJson = JSON.parse(raw);
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
throw new TxError(
|
|
40
|
+
"KEYSTORE_DECRYPT_FAILED",
|
|
41
|
+
`Failed to read keystore file: ${keystorePath}`,
|
|
42
|
+
cause
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
let privateKey;
|
|
46
|
+
try {
|
|
47
|
+
const key = Keystore.toKey(keystoreJson, { password: keystorePassword });
|
|
48
|
+
privateKey = Keystore.decrypt(keystoreJson, key);
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
throw new TxError(
|
|
51
|
+
"KEYSTORE_DECRYPT_FAILED",
|
|
52
|
+
`Failed to decrypt keystore: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
53
|
+
cause
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const account = privateKeyToAccount2(privateKey);
|
|
57
|
+
return { account, address: account.address, provider: "keystore" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/signers/privy.ts
|
|
61
|
+
var REQUIRED_FIELDS = [
|
|
62
|
+
"privyAppId",
|
|
63
|
+
"privyWalletId",
|
|
64
|
+
"privyAuthorizationKey"
|
|
65
|
+
];
|
|
66
|
+
async function createPrivySigner(options) {
|
|
67
|
+
const missing = REQUIRED_FIELDS.filter((field) => options[field] === void 0);
|
|
68
|
+
if (missing.length > 0) {
|
|
69
|
+
const missingLabels = missing.map((field) => {
|
|
70
|
+
if (field === "privyAppId") {
|
|
71
|
+
return "PRIVY_APP_ID";
|
|
72
|
+
}
|
|
73
|
+
if (field === "privyWalletId") {
|
|
74
|
+
return "PRIVY_WALLET_ID";
|
|
75
|
+
}
|
|
76
|
+
return "PRIVY_AUTHORIZATION_KEY";
|
|
77
|
+
}).join(", ");
|
|
78
|
+
throw new TxError(
|
|
79
|
+
"PRIVY_AUTH_FAILED",
|
|
80
|
+
`Privy signer requires configuration: missing ${missingLabels}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
throw new TxError(
|
|
84
|
+
"PRIVY_AUTH_FAILED",
|
|
85
|
+
"Privy signer is not yet available in tx-shared. Track progress in issue #117."
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/resolve-signer.ts
|
|
90
|
+
function hasPrivyConfig(opts) {
|
|
91
|
+
return opts.privyAppId !== void 0 || opts.privyWalletId !== void 0 || opts.privyAuthorizationKey !== void 0;
|
|
92
|
+
}
|
|
93
|
+
async function resolveSigner(opts) {
|
|
94
|
+
if (opts.privateKey !== void 0) {
|
|
95
|
+
return createPrivateKeySigner(opts.privateKey);
|
|
96
|
+
}
|
|
97
|
+
if (opts.keystorePath !== void 0) {
|
|
98
|
+
if (opts.keystorePassword === void 0) {
|
|
99
|
+
throw new TxError(
|
|
100
|
+
"SIGNER_NOT_CONFIGURED",
|
|
101
|
+
"Keystore password is required when --keystore is provided (use --password or KEYSTORE_PASSWORD)."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return createKeystoreSigner({
|
|
105
|
+
keystorePath: opts.keystorePath,
|
|
106
|
+
keystorePassword: opts.keystorePassword
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (opts.privy === true || hasPrivyConfig(opts)) {
|
|
110
|
+
return createPrivySigner({
|
|
111
|
+
...opts.privyAppId !== void 0 ? { privyAppId: opts.privyAppId } : {},
|
|
112
|
+
...opts.privyWalletId !== void 0 ? { privyWalletId: opts.privyWalletId } : {},
|
|
113
|
+
...opts.privyAuthorizationKey !== void 0 ? { privyAuthorizationKey: opts.privyAuthorizationKey } : {}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
throw new TxError(
|
|
117
|
+
"SIGNER_NOT_CONFIGURED",
|
|
118
|
+
"No signer configured. Set --private-key, or --keystore + --password, or enable --privy with PRIVY_* credentials."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/incur-params.ts
|
|
123
|
+
import { z } from "incur";
|
|
124
|
+
var signerFlagSchema = z.object({
|
|
125
|
+
"private-key": z.string().optional().describe("Raw private key (0x-prefixed 32-byte hex) for signing transactions"),
|
|
126
|
+
keystore: z.string().optional().describe("Path to an encrypted V3 keystore JSON file"),
|
|
127
|
+
password: z.string().optional().describe("Keystore password (non-interactive mode)"),
|
|
128
|
+
privy: z.boolean().default(false).describe("Use Privy server wallet signer mode")
|
|
129
|
+
});
|
|
130
|
+
var signerEnvSchema = z.object({
|
|
131
|
+
PRIVATE_KEY: z.string().optional().describe("Raw private key (0x-prefixed 32-byte hex)"),
|
|
132
|
+
KEYSTORE_PASSWORD: z.string().optional().describe("Password for decrypting --keystore"),
|
|
133
|
+
PRIVY_APP_ID: z.string().optional().describe("Privy app id used to authorize wallet intents"),
|
|
134
|
+
PRIVY_WALLET_ID: z.string().optional().describe("Privy wallet id used for transaction intents"),
|
|
135
|
+
PRIVY_AUTHORIZATION_KEY: z.string().optional().describe("Privy authorization private key used to sign intent requests")
|
|
136
|
+
});
|
|
137
|
+
function toSignerOptions(flags, env) {
|
|
138
|
+
const privateKey = flags["private-key"] ?? env.PRIVATE_KEY;
|
|
139
|
+
const keystorePassword = flags.password ?? env.KEYSTORE_PASSWORD;
|
|
140
|
+
return {
|
|
141
|
+
...privateKey !== void 0 ? { privateKey } : {},
|
|
142
|
+
...flags.keystore !== void 0 ? { keystorePath: flags.keystore } : {},
|
|
143
|
+
...keystorePassword !== void 0 ? { keystorePassword } : {},
|
|
144
|
+
...flags.privy ? { privy: true } : {},
|
|
145
|
+
...env.PRIVY_APP_ID !== void 0 ? { privyAppId: env.PRIVY_APP_ID } : {},
|
|
146
|
+
...env.PRIVY_WALLET_ID !== void 0 ? { privyWalletId: env.PRIVY_WALLET_ID } : {},
|
|
147
|
+
...env.PRIVY_AUTHORIZATION_KEY !== void 0 ? { privyAuthorizationKey: env.PRIVY_AUTHORIZATION_KEY } : {}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
10
150
|
export {
|
|
11
151
|
TxError,
|
|
12
152
|
abstractMainnet,
|
|
13
153
|
createAbstractClient,
|
|
154
|
+
createKeystoreSigner,
|
|
155
|
+
createPrivateKeySigner,
|
|
156
|
+
createPrivySigner,
|
|
157
|
+
executeTx,
|
|
158
|
+
resolveSigner,
|
|
159
|
+
signerEnvSchema,
|
|
160
|
+
signerFlagSchema,
|
|
161
|
+
toSignerOptions,
|
|
14
162
|
toTxError
|
|
15
163
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectratools/tx-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Shared transaction primitives, signer types, and chain config for spectra tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,9 +30,14 @@
|
|
|
30
30
|
"./chain": {
|
|
31
31
|
"types": "./dist/chain.d.ts",
|
|
32
32
|
"default": "./dist/chain.js"
|
|
33
|
+
},
|
|
34
|
+
"./execute-tx": {
|
|
35
|
+
"types": "./dist/execute-tx.d.ts",
|
|
36
|
+
"default": "./dist/execute-tx.js"
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"dependencies": {
|
|
40
|
+
"incur": "^0.2.2",
|
|
36
41
|
"ox": "^0.14.0",
|
|
37
42
|
"viem": "^2.47.0"
|
|
38
43
|
},
|