@spectratools/tx-shared 0.3.0 → 0.4.2
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/errors.d.ts +1 -1
- package/dist/index.d.ts +126 -3
- package/dist/index.js +456 -0
- package/dist/types.d.ts +1 -0
- package/package.json +2 -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)
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
type TxErrorCode = 'INSUFFICIENT_FUNDS' | 'NONCE_CONFLICT' | 'TX_REVERTED' | 'GAS_ESTIMATION_FAILED' | 'SIGNER_NOT_CONFIGURED' | 'KEYSTORE_DECRYPT_FAILED' | 'PRIVY_AUTH_FAILED';
|
|
1
|
+
type TxErrorCode = 'INSUFFICIENT_FUNDS' | 'NONCE_CONFLICT' | 'TX_REVERTED' | 'GAS_ESTIMATION_FAILED' | 'SIGNER_NOT_CONFIGURED' | 'KEYSTORE_DECRYPT_FAILED' | 'PRIVY_AUTH_FAILED' | 'PRIVY_TRANSPORT_FAILED';
|
|
2
2
|
declare class TxError extends Error {
|
|
3
3
|
readonly code: TxErrorCode;
|
|
4
4
|
constructor(code: TxErrorCode, message: string, cause?: unknown);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
|
-
import { TxSigner } from './types.js';
|
|
2
|
-
export {
|
|
1
|
+
import { SignerOptions, TxSigner } from './types.js';
|
|
2
|
+
export { SignerProvider, TxResult } from './types.js';
|
|
3
3
|
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
|
+
import { z } from 'incur';
|
|
7
|
+
import { KeyObject } from 'node:crypto';
|
|
6
8
|
import 'viem';
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the active signer provider using deterministic precedence:
|
|
12
|
+
* private key -> keystore -> privy -> SIGNER_NOT_CONFIGURED.
|
|
13
|
+
*/
|
|
14
|
+
declare function resolveSigner(opts: SignerOptions): Promise<TxSigner>;
|
|
15
|
+
|
|
16
|
+
/** Shared signer-related CLI flags for write-capable commands. */
|
|
17
|
+
declare const signerFlagSchema: z.ZodObject<{
|
|
18
|
+
'private-key': z.ZodOptional<z.ZodString>;
|
|
19
|
+
keystore: z.ZodOptional<z.ZodString>;
|
|
20
|
+
password: z.ZodOptional<z.ZodString>;
|
|
21
|
+
privy: z.ZodDefault<z.ZodBoolean>;
|
|
22
|
+
'privy-api-url': z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
/** Shared signer-related environment variables for write-capable commands. */
|
|
25
|
+
declare const signerEnvSchema: z.ZodObject<{
|
|
26
|
+
PRIVATE_KEY: z.ZodOptional<z.ZodString>;
|
|
27
|
+
KEYSTORE_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
28
|
+
PRIVY_APP_ID: z.ZodOptional<z.ZodString>;
|
|
29
|
+
PRIVY_WALLET_ID: z.ZodOptional<z.ZodString>;
|
|
30
|
+
PRIVY_AUTHORIZATION_KEY: z.ZodOptional<z.ZodString>;
|
|
31
|
+
PRIVY_API_URL: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
type SignerFlags = z.infer<typeof signerFlagSchema>;
|
|
34
|
+
type SignerEnv = z.infer<typeof signerEnvSchema>;
|
|
35
|
+
/** Map parsed CLI context into tx-shared SignerOptions. */
|
|
36
|
+
declare function toSignerOptions(flags: SignerFlags, env: SignerEnv): SignerOptions;
|
|
37
|
+
|
|
8
38
|
/**
|
|
9
39
|
* Create a {@link TxSigner} from a raw private key.
|
|
10
40
|
*
|
|
@@ -30,4 +60,97 @@ interface KeystoreSignerOptions {
|
|
|
30
60
|
*/
|
|
31
61
|
declare function createKeystoreSigner(options: KeystoreSignerOptions): TxSigner;
|
|
32
62
|
|
|
33
|
-
|
|
63
|
+
interface PrivyRpcIntentRequest {
|
|
64
|
+
method: string;
|
|
65
|
+
params: Record<string, unknown>;
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
}
|
|
68
|
+
interface PrivyRpcIntentResponse {
|
|
69
|
+
intent_id: string;
|
|
70
|
+
status: string;
|
|
71
|
+
resource_id?: string;
|
|
72
|
+
request_details?: Record<string, unknown>;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
interface PrivyWalletResponse {
|
|
76
|
+
id: string;
|
|
77
|
+
address: string;
|
|
78
|
+
owner_id: string | null;
|
|
79
|
+
policy_ids: string[];
|
|
80
|
+
[key: string]: unknown;
|
|
81
|
+
}
|
|
82
|
+
interface PrivyPolicyResponse {
|
|
83
|
+
id: string;
|
|
84
|
+
owner_id: string | null;
|
|
85
|
+
rules: unknown[];
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
interface CreatePrivyClientOptions {
|
|
89
|
+
appId: string;
|
|
90
|
+
walletId: string;
|
|
91
|
+
authorizationKey: string;
|
|
92
|
+
apiUrl?: string;
|
|
93
|
+
fetchImplementation?: typeof fetch;
|
|
94
|
+
}
|
|
95
|
+
interface PrivyRequestOptions {
|
|
96
|
+
idempotencyKey?: string;
|
|
97
|
+
}
|
|
98
|
+
interface PrivyClient {
|
|
99
|
+
readonly appId: string;
|
|
100
|
+
readonly walletId: string;
|
|
101
|
+
readonly apiUrl: string;
|
|
102
|
+
createRpcIntent(request: PrivyRpcIntentRequest, options?: PrivyRequestOptions): Promise<PrivyRpcIntentResponse>;
|
|
103
|
+
getWallet(): Promise<PrivyWalletResponse>;
|
|
104
|
+
getPolicy(policyId: string): Promise<PrivyPolicyResponse>;
|
|
105
|
+
}
|
|
106
|
+
declare function createPrivyClient(options: CreatePrivyClientOptions): PrivyClient;
|
|
107
|
+
|
|
108
|
+
type PrivyAuthorizationMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
109
|
+
interface PrivyAuthorizationPayloadHeaders {
|
|
110
|
+
'privy-app-id': string;
|
|
111
|
+
'privy-idempotency-key'?: string;
|
|
112
|
+
}
|
|
113
|
+
interface PrivyAuthorizationPayload<TBody extends Record<string, unknown>> {
|
|
114
|
+
version: 1;
|
|
115
|
+
method: PrivyAuthorizationMethod;
|
|
116
|
+
url: string;
|
|
117
|
+
headers: PrivyAuthorizationPayloadHeaders;
|
|
118
|
+
body: TBody;
|
|
119
|
+
}
|
|
120
|
+
interface CreatePrivyAuthorizationPayloadOptions<TBody extends Record<string, unknown>> {
|
|
121
|
+
appId: string;
|
|
122
|
+
method: PrivyAuthorizationMethod;
|
|
123
|
+
url: string;
|
|
124
|
+
body: TBody;
|
|
125
|
+
idempotencyKey?: string;
|
|
126
|
+
}
|
|
127
|
+
declare function normalizePrivyApiUrl(apiUrl?: string): string;
|
|
128
|
+
declare function parsePrivyAuthorizationKey(authorizationKey: string): KeyObject;
|
|
129
|
+
declare function createPrivyAuthorizationPayload<TBody extends Record<string, unknown>>(options: CreatePrivyAuthorizationPayloadOptions<TBody>): PrivyAuthorizationPayload<TBody>;
|
|
130
|
+
declare function serializePrivyAuthorizationPayload(payload: PrivyAuthorizationPayload<Record<string, unknown>>): string;
|
|
131
|
+
declare function generatePrivyAuthorizationSignature(payload: PrivyAuthorizationPayload<Record<string, unknown>>, authorizationKey: string): string;
|
|
132
|
+
|
|
133
|
+
interface PrivySignerOptions {
|
|
134
|
+
privyAppId?: string;
|
|
135
|
+
privyWalletId?: string;
|
|
136
|
+
privyAuthorizationKey?: string;
|
|
137
|
+
privyApiUrl?: string;
|
|
138
|
+
}
|
|
139
|
+
interface PrivySigner extends TxSigner {
|
|
140
|
+
provider: 'privy';
|
|
141
|
+
privy: {
|
|
142
|
+
appId: string;
|
|
143
|
+
walletId: string;
|
|
144
|
+
apiUrl: string;
|
|
145
|
+
client: PrivyClient;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create a Privy signer envelope with reusable transport and request-signing primitives.
|
|
150
|
+
*
|
|
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.
|
|
153
|
+
*/
|
|
154
|
+
declare function createPrivySigner(options: PrivySignerOptions): Promise<PrivySigner>;
|
|
155
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -56,12 +56,468 @@ function createKeystoreSigner(options) {
|
|
|
56
56
|
const account = privateKeyToAccount2(privateKey);
|
|
57
57
|
return { account, address: account.address, provider: "keystore" };
|
|
58
58
|
}
|
|
59
|
+
|
|
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
|
+
// src/resolve-signer.ts
|
|
439
|
+
function hasPrivyConfig(opts) {
|
|
440
|
+
return opts.privyAppId !== void 0 || opts.privyWalletId !== void 0 || opts.privyAuthorizationKey !== void 0;
|
|
441
|
+
}
|
|
442
|
+
async function resolveSigner(opts) {
|
|
443
|
+
if (opts.privateKey !== void 0) {
|
|
444
|
+
return createPrivateKeySigner(opts.privateKey);
|
|
445
|
+
}
|
|
446
|
+
if (opts.keystorePath !== void 0) {
|
|
447
|
+
if (opts.keystorePassword === void 0) {
|
|
448
|
+
throw new TxError(
|
|
449
|
+
"SIGNER_NOT_CONFIGURED",
|
|
450
|
+
"Keystore password is required when --keystore is provided (use --password or KEYSTORE_PASSWORD)."
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return createKeystoreSigner({
|
|
454
|
+
keystorePath: opts.keystorePath,
|
|
455
|
+
keystorePassword: opts.keystorePassword
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (opts.privy === true || hasPrivyConfig(opts)) {
|
|
459
|
+
return createPrivySigner({
|
|
460
|
+
...opts.privyAppId !== void 0 ? { privyAppId: opts.privyAppId } : {},
|
|
461
|
+
...opts.privyWalletId !== void 0 ? { privyWalletId: opts.privyWalletId } : {},
|
|
462
|
+
...opts.privyAuthorizationKey !== void 0 ? { privyAuthorizationKey: opts.privyAuthorizationKey } : {},
|
|
463
|
+
...opts.privyApiUrl !== void 0 ? { privyApiUrl: opts.privyApiUrl } : {}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
throw new TxError(
|
|
467
|
+
"SIGNER_NOT_CONFIGURED",
|
|
468
|
+
"No signer configured. Set --private-key, or --keystore + --password, or enable --privy with PRIVY_* credentials."
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/incur-params.ts
|
|
473
|
+
import { z } from "incur";
|
|
474
|
+
var signerFlagSchema = z.object({
|
|
475
|
+
"private-key": z.string().optional().describe("Raw private key (0x-prefixed 32-byte hex) for signing transactions"),
|
|
476
|
+
keystore: z.string().optional().describe("Path to an encrypted V3 keystore JSON file"),
|
|
477
|
+
password: z.string().optional().describe("Keystore password (non-interactive mode)"),
|
|
478
|
+
privy: z.boolean().default(false).describe("Use Privy server wallet signer mode"),
|
|
479
|
+
"privy-api-url": z.string().optional().describe("Override Privy API base URL (defaults to https://api.privy.io)")
|
|
480
|
+
});
|
|
481
|
+
var signerEnvSchema = z.object({
|
|
482
|
+
PRIVATE_KEY: z.string().optional().describe("Raw private key (0x-prefixed 32-byte hex)"),
|
|
483
|
+
KEYSTORE_PASSWORD: z.string().optional().describe("Password for decrypting --keystore"),
|
|
484
|
+
PRIVY_APP_ID: z.string().optional().describe("Privy app id used to authorize wallet intents"),
|
|
485
|
+
PRIVY_WALLET_ID: z.string().optional().describe("Privy wallet id used for transaction intents"),
|
|
486
|
+
PRIVY_AUTHORIZATION_KEY: z.string().optional().describe("Privy authorization private key used to sign intent requests"),
|
|
487
|
+
PRIVY_API_URL: z.string().optional().describe("Optional Privy API base URL override (default https://api.privy.io)")
|
|
488
|
+
});
|
|
489
|
+
function toSignerOptions(flags, env) {
|
|
490
|
+
const privateKey = flags["private-key"] ?? env.PRIVATE_KEY;
|
|
491
|
+
const keystorePassword = flags.password ?? env.KEYSTORE_PASSWORD;
|
|
492
|
+
const privyApiUrl = flags["privy-api-url"] ?? env.PRIVY_API_URL;
|
|
493
|
+
return {
|
|
494
|
+
...privateKey !== void 0 ? { privateKey } : {},
|
|
495
|
+
...flags.keystore !== void 0 ? { keystorePath: flags.keystore } : {},
|
|
496
|
+
...keystorePassword !== void 0 ? { keystorePassword } : {},
|
|
497
|
+
...flags.privy ? { privy: true } : {},
|
|
498
|
+
...env.PRIVY_APP_ID !== void 0 ? { privyAppId: env.PRIVY_APP_ID } : {},
|
|
499
|
+
...env.PRIVY_WALLET_ID !== void 0 ? { privyWalletId: env.PRIVY_WALLET_ID } : {},
|
|
500
|
+
...env.PRIVY_AUTHORIZATION_KEY !== void 0 ? { privyAuthorizationKey: env.PRIVY_AUTHORIZATION_KEY } : {},
|
|
501
|
+
...privyApiUrl !== void 0 ? { privyApiUrl } : {}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
59
504
|
export {
|
|
60
505
|
TxError,
|
|
61
506
|
abstractMainnet,
|
|
62
507
|
createAbstractClient,
|
|
63
508
|
createKeystoreSigner,
|
|
64
509
|
createPrivateKeySigner,
|
|
510
|
+
createPrivyAuthorizationPayload,
|
|
511
|
+
createPrivyClient,
|
|
512
|
+
createPrivySigner,
|
|
65
513
|
executeTx,
|
|
514
|
+
generatePrivyAuthorizationSignature,
|
|
515
|
+
normalizePrivyApiUrl,
|
|
516
|
+
parsePrivyAuthorizationKey,
|
|
517
|
+
resolveSigner,
|
|
518
|
+
serializePrivyAuthorizationPayload,
|
|
519
|
+
signerEnvSchema,
|
|
520
|
+
signerFlagSchema,
|
|
521
|
+
toSignerOptions,
|
|
66
522
|
toTxError
|
|
67
523
|
};
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectratools/tx-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Shared transaction primitives, signer types, and chain config for spectra tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"incur": "^0.2.2",
|
|
40
41
|
"ox": "^0.14.0",
|
|
41
42
|
"viem": "^2.47.0"
|
|
42
43
|
},
|