@livo-build/runtime 0.2.4 → 0.2.6

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.
@@ -0,0 +1,45 @@
1
+ import { type Bytes, type Hex } from "./hex.js";
2
+ export interface TypedDataField {
3
+ name: string;
4
+ type: string;
5
+ }
6
+ export interface TypedDataDomain {
7
+ name?: string;
8
+ version?: string;
9
+ chainId?: number | bigint;
10
+ verifyingContract?: string;
11
+ salt?: string;
12
+ }
13
+ export type TypedDataTypes = Record<string, readonly TypedDataField[]>;
14
+ export interface TypedDataDefinition {
15
+ domain: TypedDataDomain;
16
+ types: TypedDataTypes;
17
+ primaryType: string;
18
+ message: Record<string, unknown>;
19
+ }
20
+ /** The 32-byte EIP-712 digest (`keccak256(0x1901 ‖ domainSeparator ‖ hashStruct)`). */
21
+ export declare function hashTypedData(def: TypedDataDefinition): Bytes;
22
+ /**
23
+ * Sign EIP-712 typed data with a raw private key. Returns the 65-byte
24
+ * `r ‖ s ‖ v` signature (v = 27/28), the form Ethereum verifiers expect.
25
+ */
26
+ export declare function signTypedData(def: TypedDataDefinition & {
27
+ privateKey: string;
28
+ }): Hex;
29
+ /** Viem-style typed-data params (what a viem local account's signTypedData receives). */
30
+ export interface ViemTypedDataParams {
31
+ domain: TypedDataDomain;
32
+ types: TypedDataTypes;
33
+ primaryType: string;
34
+ message: Record<string, unknown>;
35
+ }
36
+ export interface LocalAccount {
37
+ address: Hex;
38
+ signTypedData(params: ViemTypedDataParams): Promise<Hex>;
39
+ }
40
+ /**
41
+ * A viem-"local account"-shaped signer backed by a raw private key — `{ address,
42
+ * signTypedData }`. Drops into SDKs that accept a viem account (e.g.
43
+ * @nktkas/hyperliquid's ExchangeClient) without depending on viem itself.
44
+ */
45
+ export declare function localAccount(privateKey: string): LocalAccount;
package/dist/eip712.js ADDED
@@ -0,0 +1,160 @@
1
+ // EIP-712 typed-data hashing + signing — the missing primitive both the
2
+ // Hyperliquid and Polymarket helpers need (their auth/order signatures are all
3
+ // EIP-712). Implemented on the same audited @noble crypto as tx.ts/sig.ts and
4
+ // validated byte-for-byte against viem in the test suite. Worker-safe, no deps
5
+ // beyond noble.
6
+ //
7
+ // `localAccount(privateKey)` returns a viem-"local account"-shaped object
8
+ // ({ address, signTypedData(params) }) so it drops straight into SDKs that accept
9
+ // a viem account (e.g. @nktkas/hyperliquid's ExchangeClient) without pulling viem.
10
+ import { secp256k1 } from "@noble/curves/secp256k1";
11
+ import { keccak_256 } from "@noble/hashes/sha3";
12
+ import { bytesToHex, concatBytes, hexToBytes, toBytes, toBytesSigned, } from "./hex.js";
13
+ import { toChecksumAddress } from "./tx.js";
14
+ // The canonical field order for the EIP712Domain struct — only the present fields
15
+ // are included, matching what viem/ethers produce.
16
+ const DOMAIN_FIELDS = [
17
+ { name: "name", type: "string" },
18
+ { name: "version", type: "string" },
19
+ { name: "chainId", type: "uint256" },
20
+ { name: "verifyingContract", type: "address" },
21
+ { name: "salt", type: "bytes32" },
22
+ ];
23
+ function domainTypes(domain) {
24
+ return DOMAIN_FIELDS.filter((f) => domain[f.name] !== undefined && domain[f.name] !== null).map((f) => ({ name: f.name, type: f.type }));
25
+ }
26
+ // Collect the struct types `primaryType` depends on (transitively), excluding the
27
+ // well-known EIP712Domain. Used to build the canonical `encodeType` string.
28
+ function dependencies(primaryType, types, found = new Set()) {
29
+ const base = primaryType.replace(/\[.*\]$/, "");
30
+ if (found.has(base) || !types[base] || base === "EIP712Domain")
31
+ return [...found];
32
+ found.add(base);
33
+ for (const field of types[base]) {
34
+ for (const dep of dependencies(field.type, types, found))
35
+ found.add(dep);
36
+ }
37
+ return [...found];
38
+ }
39
+ function encodeType(primaryType, types) {
40
+ const deps = dependencies(primaryType, types).filter((d) => d !== primaryType);
41
+ deps.sort();
42
+ const ordered = [primaryType, ...deps];
43
+ return ordered
44
+ .map((t) => `${t}(${(types[t] ?? []).map((f) => `${f.type} ${f.name}`).join(",")})`)
45
+ .join("");
46
+ }
47
+ function typeHash(primaryType, types) {
48
+ return keccak_256(new TextEncoder().encode(encodeType(primaryType, types)));
49
+ }
50
+ function isArrayType(type) {
51
+ return type.endsWith("]");
52
+ }
53
+ // Encode one field to its 32-byte (or, for arrays/structs, hashed-to-32) chunk.
54
+ function encodeField(types, type, value) {
55
+ if (types[type]) {
56
+ // nested struct → hash of its encodeData
57
+ return keccak_256(encodeData(type, value, types));
58
+ }
59
+ if (isArrayType(type)) {
60
+ const baseType = type.slice(0, type.lastIndexOf("["));
61
+ const arr = value;
62
+ return keccak_256(concatBytes(...arr.map((item) => encodeField(types, baseType, item))));
63
+ }
64
+ if (type === "string") {
65
+ return keccak_256(new TextEncoder().encode(String(value)));
66
+ }
67
+ if (type === "bytes") {
68
+ return keccak_256(hexToBytes(String(value)));
69
+ }
70
+ if (type === "bool") {
71
+ return toBytes(value ? 1n : 0n, 32);
72
+ }
73
+ if (type === "address") {
74
+ const addr = hexToBytes(String(value));
75
+ if (addr.length !== 20)
76
+ throw new Error(`address must be 20 bytes: ${String(value)}`);
77
+ return concatBytes(new Uint8Array(12), addr);
78
+ }
79
+ const bytesN = type.match(/^bytes(\d+)$/);
80
+ if (bytesN) {
81
+ const raw = hexToBytes(String(value));
82
+ const out = new Uint8Array(32);
83
+ out.set(raw.slice(0, Number(bytesN[1])), 0); // right-padded
84
+ return out;
85
+ }
86
+ const uint = type.match(/^uint(\d+)?$/);
87
+ if (uint)
88
+ return toBytes(toBigIntValue(value), 32);
89
+ const int = type.match(/^int(\d+)?$/);
90
+ if (int)
91
+ return toBytesSigned(toBigIntValue(value), 32);
92
+ throw new Error(`unsupported EIP-712 type: ${type}`);
93
+ }
94
+ function toBigIntValue(value) {
95
+ if (typeof value === "bigint")
96
+ return value;
97
+ if (typeof value === "number")
98
+ return BigInt(value);
99
+ return BigInt(String(value)); // decimal or 0x-hex string
100
+ }
101
+ function encodeData(primaryType, data, types) {
102
+ const parts = [typeHash(primaryType, types)];
103
+ for (const field of types[primaryType]) {
104
+ parts.push(encodeField(types, field.type, data[field.name]));
105
+ }
106
+ return concatBytes(...parts);
107
+ }
108
+ function hashStruct(primaryType, data, types) {
109
+ return keccak_256(encodeData(primaryType, data, types));
110
+ }
111
+ function domainSeparator(domain) {
112
+ const types = { EIP712Domain: domainTypes(domain) };
113
+ const data = {};
114
+ for (const f of domainTypes(domain)) {
115
+ const key = f.name;
116
+ data[f.name] = f.type === "uint256" ? toBigIntValue(domain[key]) : domain[key];
117
+ }
118
+ return hashStruct("EIP712Domain", data, types);
119
+ }
120
+ /** The 32-byte EIP-712 digest (`keccak256(0x1901 ‖ domainSeparator ‖ hashStruct)`). */
121
+ export function hashTypedData(def) {
122
+ // `types` may carry an EIP712Domain entry (viem-style) — ignore it; the domain
123
+ // separator is derived from the present domain fields.
124
+ const sep = domainSeparator(def.domain);
125
+ const struct = hashStruct(def.primaryType, def.message, def.types);
126
+ return keccak_256(concatBytes(new Uint8Array([0x19, 0x01]), sep, struct));
127
+ }
128
+ function normalizeKey(privateKey) {
129
+ const k = privateKey.trim().replace(/^0x/, "");
130
+ if (!/^[0-9a-fA-F]{64}$/.test(k))
131
+ throw new Error("private key must be 32 bytes (64 hex chars)");
132
+ return hexToBytes(k);
133
+ }
134
+ /**
135
+ * Sign EIP-712 typed data with a raw private key. Returns the 65-byte
136
+ * `r ‖ s ‖ v` signature (v = 27/28), the form Ethereum verifiers expect.
137
+ */
138
+ export function signTypedData(def) {
139
+ const digest = hashTypedData(def);
140
+ const sig = secp256k1.sign(digest, normalizeKey(def.privateKey), { lowS: true });
141
+ const r = toBytes(sig.r, 32);
142
+ const s = toBytes(sig.s, 32);
143
+ const v = new Uint8Array([27 + sig.recovery]);
144
+ return bytesToHex(concatBytes(r, s, v));
145
+ }
146
+ /**
147
+ * A viem-"local account"-shaped signer backed by a raw private key — `{ address,
148
+ * signTypedData }`. Drops into SDKs that accept a viem account (e.g.
149
+ * @nktkas/hyperliquid's ExchangeClient) without depending on viem itself.
150
+ */
151
+ export function localAccount(privateKey) {
152
+ const pub = secp256k1.getPublicKey(normalizeKey(privateKey), false).slice(1);
153
+ const address = toChecksumAddress(bytesToHex(keccak_256(pub).slice(-20)));
154
+ return {
155
+ address,
156
+ async signTypedData(params) {
157
+ return signTypedData({ ...params, privateKey });
158
+ },
159
+ };
160
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ // EIP-712 correctness: our hashTypedData/signTypedData must match viem byte-for-byte
2
+ // (these signatures move real money on Hyperliquid/Polymarket). viem is the oracle.
3
+ import { describe, expect, it } from "vitest";
4
+ import { hashTypedData as _viemHash } from "viem";
5
+ import { privateKeyToAccount as _pkToAccount } from "viem/accounts";
6
+ import { hashTypedData, signTypedData, localAccount } from "./eip712.js";
7
+ import { bytesToHex } from "./hex.js";
8
+ const PK = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d";
9
+ // A representative spread: domain with all fields, nested struct, array, bytes,
10
+ // uint, address, bool, string.
11
+ const PERSON_DEF = {
12
+ domain: {
13
+ name: "Ether Mail",
14
+ version: "1",
15
+ chainId: 1,
16
+ verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
17
+ },
18
+ types: {
19
+ Person: [
20
+ { name: "name", type: "string" },
21
+ { name: "wallet", type: "address" },
22
+ ],
23
+ Mail: [
24
+ { name: "from", type: "Person" },
25
+ { name: "to", type: "Person" },
26
+ { name: "contents", type: "string" },
27
+ ],
28
+ },
29
+ primaryType: "Mail",
30
+ message: {
31
+ from: { name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" },
32
+ to: { name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" },
33
+ contents: "Hello, Bob!",
34
+ },
35
+ };
36
+ // Hyperliquid's Agent typed data (chainId 1337, the "Exchange" domain).
37
+ const HL_AGENT_DEF = {
38
+ domain: { name: "Exchange", version: "1", chainId: 1337, verifyingContract: "0x0000000000000000000000000000000000000000" },
39
+ types: { Agent: [{ name: "source", type: "string" }, { name: "connectionId", type: "bytes32" }] },
40
+ primaryType: "Agent",
41
+ message: { source: "a", connectionId: "0x" + "ab".repeat(32) },
42
+ };
43
+ // Polymarket CTF Exchange Order — the exact struct we post.
44
+ const PM_ORDER_DEF = {
45
+ domain: { name: "Polymarket CTF Exchange", version: "1", chainId: 137, verifyingContract: "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E" },
46
+ types: {
47
+ Order: [
48
+ { name: "salt", type: "uint256" },
49
+ { name: "maker", type: "address" },
50
+ { name: "signer", type: "address" },
51
+ { name: "taker", type: "address" },
52
+ { name: "tokenId", type: "uint256" },
53
+ { name: "makerAmount", type: "uint256" },
54
+ { name: "takerAmount", type: "uint256" },
55
+ { name: "expiration", type: "uint256" },
56
+ { name: "nonce", type: "uint256" },
57
+ { name: "feeRateBps", type: "uint256" },
58
+ { name: "side", type: "uint8" },
59
+ { name: "signatureType", type: "uint8" },
60
+ ],
61
+ },
62
+ primaryType: "Order",
63
+ message: {
64
+ salt: "123456789",
65
+ maker: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
66
+ signer: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
67
+ taker: "0x0000000000000000000000000000000000000000",
68
+ tokenId: "71321045679252212594626385532706912750332728571942532289631379312455583992563",
69
+ makerAmount: "1000000",
70
+ takerAmount: "2000000",
71
+ expiration: "0",
72
+ nonce: "0",
73
+ feeRateBps: "0",
74
+ side: 0,
75
+ signatureType: 0,
76
+ },
77
+ };
78
+ describe("hashTypedData matches viem", () => {
79
+ for (const [label, def] of [
80
+ ["EIP-712 example (nested structs)", PERSON_DEF],
81
+ ["Hyperliquid Agent", HL_AGENT_DEF],
82
+ ["Polymarket Order", PM_ORDER_DEF],
83
+ ]) {
84
+ it(label, () => {
85
+ const ours = bytesToHex(hashTypedData(def));
86
+ const theirs = _viemHash(def);
87
+ expect(ours).toBe(theirs);
88
+ });
89
+ }
90
+ });
91
+ describe("signTypedData matches viem", () => {
92
+ it("produces the same 65-byte signature as a viem local account", async () => {
93
+ const ours = signTypedData({ ...PM_ORDER_DEF, privateKey: PK });
94
+ const account = _pkToAccount(PK);
95
+ const theirs = await account.signTypedData(PM_ORDER_DEF);
96
+ expect(ours).toBe(theirs);
97
+ });
98
+ it("localAccount address + signature match viem", async () => {
99
+ const acct = localAccount(PK);
100
+ const viemAcct = _pkToAccount(PK);
101
+ expect(acct.address.toLowerCase()).toBe(viemAcct.address.toLowerCase());
102
+ const ours = await acct.signTypedData(HL_AGENT_DEF);
103
+ const theirs = await viemAcct.signTypedData(HL_AGENT_DEF);
104
+ expect(ours).toBe(theirs);
105
+ });
106
+ });
package/dist/gate.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Chain } from "./chain.js";
2
+ export declare function tokenBalanceOf(chain: Chain, token: string, owner: string): Promise<bigint>;
3
+ export interface TokenGate {
4
+ /** ERC-20 / ERC-721 token address. */
5
+ token: string;
6
+ /** Minimum balance (base units for ERC-20; token count for ERC-721). */
7
+ minBalance: bigint;
8
+ }
9
+ /** Whether an owner currently meets a token gate. */
10
+ export declare function meetsGate(chain: Chain, owner: string, gate: TokenGate): Promise<boolean>;
package/dist/gate.js ADDED
@@ -0,0 +1,18 @@
1
+ // Token-gating helpers — does a wallet hold enough of a token to enter / stay in a
2
+ // gated room? Works for ERC-20 (balance) and ERC-721 (count) since both expose
3
+ // balanceOf(address). Read-only — a bot/keeper Chain needs only an RPC_URL.
4
+ // The balanceOf(address)→uint256 of a token for an owner. Returns 0n on a failed
5
+ // read (treat as "doesn't hold") rather than throwing the whole keeper sweep.
6
+ export async function tokenBalanceOf(chain, token, owner) {
7
+ try {
8
+ const r = await chain.call(token, "balanceOf(address)(uint256)", [owner]);
9
+ return BigInt(r);
10
+ }
11
+ catch {
12
+ return 0n;
13
+ }
14
+ }
15
+ /** Whether an owner currently meets a token gate. */
16
+ export async function meetsGate(chain, owner, gate) {
17
+ return (await tokenBalanceOf(chain, gate.token, owner)) >= gate.minBalance;
18
+ }