@spectratools/tx-shared
v0.6.2
Published
Shared transaction primitives, signer types, and chain config for spectra tools
Readme
@spectratools/tx-shared
Shared transaction primitives for Spectra tools:
- signer resolution (
resolveSigner) - transaction lifecycle execution (
executeTx) - shared signer CLI/env parsing helpers (
toSignerOptions) - structured transaction errors (
TxError) - Abstract chain helpers (
abstractMainnet,createAbstractClient)
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.
Install
pnpm add @spectratools/tx-sharedSigner providers
resolveSigner() uses deterministic precedence:
privateKeykeystorePath(+keystorePassword)- Privy (
privyflag and/orPRIVY_*env)
If no provider is configured, it throws TxError with code SIGNER_NOT_CONFIGURED.
Provider setup
1) Private key
export PRIVATE_KEY="0x<64-hex-chars>"Or pass privateKey directly in code.
2) Keystore
# CLI convention
--keystore /path/to/keystore.json --password "$KEYSTORE_PASSWORD"
# env fallback for password
export KEYSTORE_PASSWORD="..."resolveSigner() requires a password when keystorePath is provided.
3) Privy
Required env:
export PRIVY_APP_ID="..."
export PRIVY_WALLET_ID="..."
export PRIVY_AUTHORIZATION_KEY="wallet-auth:<base64-pkcs8-p256-private-key>"Optional env:
export PRIVY_API_URL="https://api.sandbox.privy.io"Optional CLI flags (from signerFlagSchema):
--privy
--privy-api-url "https://api.sandbox.privy.io"Notes:
--privyis optional whenPRIVY_APP_ID+PRIVY_WALLET_ID+PRIVY_AUTHORIZATION_KEYare present;resolveSigner()auto-detects Privy config.--privy-api-url/PRIVY_API_URLoverride the defaulthttps://api.privy.io.- Privy account methods are mapped as:
sendTransaction→eth_sendTransactionsignMessage→personal_signsignTypedData→eth_signTypedData_v4signTransaction→eth_signTransaction(returns serialized tx hex; broadcast separately if needed)
resolveSigner() usage
Direct options
import { resolveSigner } from '@spectratools/tx-shared';
const signer = await resolveSigner({
privateKey: process.env.PRIVATE_KEY,
});
console.log(signer.provider); // 'private-key' | 'keystore' | 'privy'
console.log(signer.address); // 0x...From shared CLI flags + env
import {
resolveSigner,
signerEnvSchema,
signerFlagSchema,
toSignerOptions,
} from '@spectratools/tx-shared';
const flags = signerFlagSchema.parse({
privy: true,
'privy-api-url': process.env.PRIVY_API_URL,
});
const env = signerEnvSchema.parse(process.env);
const signer = await resolveSigner(toSignerOptions(flags, env));executeTx() lifecycle
executeTx() performs this flow:
- estimate gas (
estimateContractGas) - simulate (
simulateContract) - preflight Privy policies (when signer is Privy-backed)
- submit (
writeContract) unlessdryRun: true - wait for receipt (
waitForTransactionReceipt) - normalize result into a shared output shape
Privy transaction flow example (dry-run + live)
import { http, createPublicClient, createWalletClient, parseAbi } from 'viem';
import {
abstractMainnet,
executeTx,
resolveSigner,
signerEnvSchema,
signerFlagSchema,
toSignerOptions,
} from '@spectratools/tx-shared';
const flags = signerFlagSchema.parse({
privy: true,
'privy-api-url': process.env.PRIVY_API_URL,
});
const env = signerEnvSchema.parse(process.env);
const signer = await resolveSigner(toSignerOptions(flags, env));
const publicClient = createPublicClient({
chain: abstractMainnet,
transport: http(process.env.ABSTRACT_RPC_URL),
});
const walletClient = createWalletClient({
account: signer.account,
chain: abstractMainnet,
transport: http(process.env.ABSTRACT_RPC_URL),
});
const registryAbi = parseAbi(['function register() payable']);
const dryRun = await executeTx({
publicClient,
walletClient,
account: signer.account,
address: '0x1111111111111111111111111111111111111111',
abi: registryAbi,
functionName: 'register',
value: 1000000000000000n,
dryRun: true,
});
if (dryRun.status === 'dry-run') {
console.log(dryRun.estimatedGas.toString());
console.log(dryRun.privyPolicy?.status); // 'allowed' | 'blocked'
}
const live = await executeTx({
publicClient,
walletClient,
account: signer.account,
address: '0x1111111111111111111111111111111111111111',
abi: registryAbi,
functionName: 'register',
value: 1000000000000000n,
});
console.log(live.hash, live.status);Privy signing flow example
const signer = await resolveSigner({
privy: true,
privyAppId: process.env.PRIVY_APP_ID,
privyWalletId: process.env.PRIVY_WALLET_ID,
privyAuthorizationKey: process.env.PRIVY_AUTHORIZATION_KEY,
privyApiUrl: process.env.PRIVY_API_URL,
});
if (signer.provider !== 'privy') throw new Error('Expected Privy signer');
const messageSig = await signer.account.signMessage({ message: 'hello from tx-shared' });
const typedDataSig = await signer.account.signTypedData({
domain: { name: 'SpectraTools', version: '1', chainId: 2741 },
primaryType: 'Ping',
types: {
Ping: [{ name: 'message', type: 'string' }],
},
message: { message: 'privy typed data' },
});
const signedTxHex = await signer.account.signTransaction({
to: '0x1111111111111111111111111111111111111111',
chainId: 2741,
nonce: 1,
gas: 21000n,
maxFeePerGas: 1000000000n,
maxPriorityFeePerGas: 1000000000n,
value: 0n,
data: '0x',
});
console.log(messageSig, typedDataSig, signedTxHex);Structured errors
executeTx() and signer helpers throw TxError with stable code values:
INSUFFICIENT_FUNDSNONCE_CONFLICTGAS_ESTIMATION_FAILEDTX_REVERTEDSIGNER_NOT_CONFIGUREDKEYSTORE_DECRYPT_FAILEDPRIVY_AUTH_FAILEDPRIVY_TRANSPORT_FAILEDPRIVY_POLICY_BLOCKED
import { TxError } from '@spectratools/tx-shared';
try {
// resolveSigner(...) + executeTx(...)
} catch (error) {
if (error instanceof TxError) {
switch (error.code) {
case 'INSUFFICIENT_FUNDS':
case 'NONCE_CONFLICT':
case 'GAS_ESTIMATION_FAILED':
case 'TX_REVERTED':
case 'SIGNER_NOT_CONFIGURED':
case 'KEYSTORE_DECRYPT_FAILED':
case 'PRIVY_AUTH_FAILED':
case 'PRIVY_TRANSPORT_FAILED':
case 'PRIVY_POLICY_BLOCKED':
throw error;
}
}
throw error;
}Troubleshooting
SIGNER_NOT_CONFIGURED- check signer input precedence
- confirm at least one provider is configured
KEYSTORE_DECRYPT_FAILED- verify file path and password
- ensure keystore is valid V3 JSON
PRIVY_AUTH_FAILED- verify required env:
PRIVY_APP_ID,PRIVY_WALLET_ID,PRIVY_AUTHORIZATION_KEY - validate
PRIVY_AUTHORIZATION_KEYformat:wallet-auth:<base64-pkcs8-p256-private-key> - verify
PRIVY_API_URLoverride points at a valid Privy API host
- verify required env:
PRIVY_TRANSPORT_FAILED- check Privy API availability and credentials
- inspect upstream intent failure payloads for status + reason
PRIVY_POLICY_BLOCKED- review active wallet policy allowlists and native value caps
- use
dryRun: trueand inspectresult.privyPolicybefore broadcasting
GAS_ESTIMATION_FAILED/TX_REVERTED- validate function args and
value - run with
dryRun: truefirst
- validate function args and
NONCE_CONFLICT- refresh nonce and retry once with an explicit override if needed
Consumer integration examples
- tx-shared assembly-style example:
- assembly consumer reference wiring:
