first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# tx-decoder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides naive transaction-hash classification and 4-byte function-selector decoding for EVM calldata. `decodeTxHash` classifies a transaction hash into one of `"evm" | "utxo" | "solana" | "unknown"` and returns the set of chains the hash could plausibly belong to. `extractSelector`, `lookupSelector`, and `decodeCalldata` work together to identify a function call from raw calldata when the called function is in the curated `KNOWN_SELECTORS` table (eight well-known ERC-20 / ERC-721 functions).
This module is a deliberate MVP: it does not decode argument bytes (ABI-aware decoding is scoped out per `enterprise/cryptocurrency.md` §3) and does not consult an online selector database (`4byte.directory` / `openchain.xyz`). Both gaps land in the `add-cryptocurrency-abi` follow-up change.
## Features
- `decodeTxHash(hash)` — classifies EVM (0x + 64 hex), UTXO (64 hex), Solana (base58 87-88 char) hashes and returns `TxMetadata { hash, chains, valid, hashType, hashHex? }`.
- `extractSelector(calldata)` — extracts the first 10 characters of the trimmed lowercased calldata when it looks like a function call; returns `undefined` otherwise.
- `lookupSelector(selector)` — case-insensitive lookup against the `KNOWN_SELECTORS` table.
- `decodeCalldata(calldata)` — composition of `extractSelector` then `lookupSelector`.
- `KNOWN_SELECTORS` — curated list of 8 well-known ERC-20 / ERC-721 function signatures.
- All inputs are trimmed; all matching is case-insensitive on hex.
## Usage: Basic
```ts
import {
decodeCalldata,
decodeTxHash,
extractSelector,
} from "@suaveplan/cryptocurrency";
const tx = decodeTxHash(`0x${"a".repeat(64)}`);
// { hash: "0xaa...", chains: ["ethereum", ...], valid: true, hashType: "evm", hashHex: "aa..." }
const sel = extractSelector("0xa9059cbb000000000000000000000000...");
// "0xa9059cbb"
const fn = decodeCalldata("0xa9059cbb000000000000000000000000...");
// { selector: "0xa9059cbb", name: "transfer", signature: "transfer(address,uint256)" }
```
## Usage: Advanced
```ts
import {
decodeCalldata,
type FunctionSignature,
KNOWN_SELECTORS,
lookupSelector,
} from "@suaveplan/cryptocurrency";
// Extend the curated table with project-specific signatures via in-memory composition
function makeSelectorResolver(extras: FunctionSignature[]) {
const merged = new Map<string, FunctionSignature>();
for (const s of [...KNOWN_SELECTORS, ...extras]) {
merged.set(s.selector.toLowerCase(), s);
}
return (selectorOrCalldata: string): FunctionSignature \| undefined => {
const baseline = decodeCalldata(selectorOrCalldata) ??
lookupSelector(selectorOrCalldata);
if (baseline) return baseline;
// Try the merged table as fallback for the project-specific extras
const normalised = selectorOrCalldata.toLowerCase().slice(0, 10);
return merged.get(normalised);
};
}
const resolve = makeSelectorResolver([
{
selector: "0xdeadbeef",
name: "myCustom",
signature: "myCustom(bytes32,address)",
},
]);
```
This pattern keeps the package's curated `KNOWN_SELECTORS` immutable while letting projects layer their own signatures on top — no need for a mutable global registry inside the package.
## API Reference
| Export | Signature | Notes |
|---|---|---|
| `KNOWN_SELECTORS` | `FunctionSignature[]` | 8 well-known selectors (transfer, approve, transferFrom, balanceOf, allowance, totalSupply, safeTransferFrom, setApprovalForAll) |
| `decodeTxHash` | `(hash: string) => TxMetadata` | classifies EVM / UTXO / Solana hashes |
| `lookupSelector` | `(selector: string) => FunctionSignature \| undefined` | case-insensitive on selector |
| `extractSelector` | `(calldata: string) => string \| undefined` | first 10 chars of `0x` + ≥ 8 hex chars |
| `decodeCalldata` | `(calldata: string) => FunctionSignature \| undefined` | composition of extract + lookup |
### `TxMetadata` shape
```ts
interface TxMetadata {
hash: string;
chains: ChainId[];
valid: boolean;
hashType: "evm" | "utxo" | "solana" | "unknown";
hashHex?: string; // populated for evm / utxo paths
}
```
## Implementation Notes
- The hash matchers are anchored regexes (`^...$`) to prevent partial matches. EVM (`0x` + 64 hex) is checked first because it would also match the UTXO pattern if the `0x` prefix were optional. The Solana base58 pattern excludes ambiguous characters (`0`, `O`, `I`, `l`) so it cannot collide with the hex patterns.
- `hashHex` is omitted for the Solana path (base58 is not hex). For the EVM path, the prefix is stripped; for the UTXO path, the whole input is lowercased.
- `extractSelector` requires `0x` + at least 8 hex characters — calldata containing only a selector (`"0xa9059cbb"`, 10 chars) qualifies; anything shorter is rejected. The slice always takes 10 chars (`"0x"` + 8 hex), even if more are present.
- `KNOWN_SELECTORS` is a mutable array (not `as const`) so consumers can spread it into custom collections. Sharing the array reference with callers that mutate it would corrupt the package's behaviour, but that is a misuse not worth defending against — `Object.freeze` would lose the spread-friendly mutability without preventing reassignment of indexed entries' fields anyway.
- The curated selector list is deliberately small. A comprehensive registry (4byte.directory export, openchain.xyz dump) belongs in a downstream `@suaveplan/cryptocurrency-selectors` package; keeping the core curated avoids shipping a ~50MB JSON blob.
- ABI-aware argument decoding (dynamic types, tuples, nested arrays) is **not** in this module. It lands in `add-cryptocurrency-abi` along with the pluggable online resolver interface — see `enterprise/cryptocurrency.md` §3.
- The `hashType: "unknown"` branch returns `chains: []` and `valid: false` — callers should dispatch on `valid` first, then on `hashType` for the success cases.