diff --git a/.changeset/sharp-lions-cover.md b/.changeset/sharp-lions-cover.md
new file mode 100644
index 0000000000..e94ead112b
--- /dev/null
+++ b/.changeset/sharp-lions-cover.md
@@ -0,0 +1,5 @@
+---
+"@latticexyz/store": patch
+---
+
+Added internal `getRecord` and `getStaticDataLocation` helpers.
diff --git a/packages/protocol-parser/src/getKeyTuple.ts b/packages/protocol-parser/src/getKeyTuple.ts
index 17c6ab0237..b368ae6330 100644
--- a/packages/protocol-parser/src/getKeyTuple.ts
+++ b/packages/protocol-parser/src/getKeyTuple.ts
@@ -7,7 +7,7 @@ type PartialTable = Pick
;
export type getKeyTuple = {
[i in keyof key]: Hex;
-};
+} & unknown;
export function getKeyTuple(
table: table,
diff --git a/packages/store/ts/exports/internal.ts b/packages/store/ts/exports/internal.ts
index 0765dc8d68..0ae3f3cc9a 100644
--- a/packages/store/ts/exports/internal.ts
+++ b/packages/store/ts/exports/internal.ts
@@ -2,6 +2,8 @@ export * from "../common";
export * from "../getStoreLogs";
export * from "../flattenStoreLogs";
export * from "../logToRecord";
+export * from "../getRecord";
+export * from "../getStaticDataLocation";
export * from "../config/v2/codegen";
export * from "../config/v2/defaults";
diff --git a/packages/store/ts/getRecord.ts b/packages/store/ts/getRecord.ts
new file mode 100644
index 0000000000..56b17ba3d9
--- /dev/null
+++ b/packages/store/ts/getRecord.ts
@@ -0,0 +1,78 @@
+import { Address, Client, Hex } from "viem";
+import { Table } from "@latticexyz/config";
+import {
+ decodeValueArgs,
+ getKeySchema,
+ getKeyTuple,
+ getSchemaPrimitives,
+ getSchemaTypes,
+ getValueSchema,
+} from "@latticexyz/protocol-parser/internal";
+import { readContract } from "viem/actions";
+import { getAction } from "viem/utils";
+
+export type GetRecordOptions = {
+ address: Address;
+ table: table;
+ key: getSchemaPrimitives>;
+ blockTag?: "latest" | "pending";
+};
+
+export async function getRecord(
+ client: Client,
+ { address, table, key, blockTag }: GetRecordOptions,
+): Promise> {
+ const [staticData, encodedLengths, dynamicData] = await getAction(
+ client,
+ readContract,
+ "readContract",
+ )({
+ address,
+ abi,
+ functionName: "getRecord",
+ args: [table.tableId, getKeyTuple(table, key) as readonly Hex[]],
+ blockTag,
+ });
+
+ return {
+ ...key,
+ ...decodeValueArgs(getSchemaTypes(getValueSchema(table)), { staticData, encodedLengths, dynamicData }),
+ };
+}
+
+const abi = [
+ {
+ type: "function",
+ name: "getRecord",
+ inputs: [
+ {
+ name: "tableId",
+ type: "bytes32",
+ internalType: "ResourceId",
+ },
+ {
+ name: "keyTuple",
+ type: "bytes32[]",
+ internalType: "bytes32[]",
+ },
+ ],
+ outputs: [
+ {
+ name: "staticData",
+ type: "bytes",
+ internalType: "bytes",
+ },
+ {
+ name: "encodedLengths",
+ type: "bytes32",
+ internalType: "EncodedLengths",
+ },
+ {
+ name: "dynamicData",
+ type: "bytes",
+ internalType: "bytes",
+ },
+ ],
+ stateMutability: "view",
+ },
+] as const;
diff --git a/packages/store/ts/getStaticDataLocation.ts b/packages/store/ts/getStaticDataLocation.ts
new file mode 100644
index 0000000000..ad0f14771a
--- /dev/null
+++ b/packages/store/ts/getStaticDataLocation.ts
@@ -0,0 +1,10 @@
+import { Hex, concatHex, hexToBigInt, keccak256, numberToHex, toBytes } from "viem";
+
+// TODO: move to protocol-parser?
+// equivalent of StoreCore._getStaticDataLocation
+
+const SLOT = hexToBigInt(keccak256(toBytes("mud.store")));
+
+export function getStaticDataLocation(tableId: Hex, keyTuple: readonly Hex[]): Hex {
+ return numberToHex(SLOT ^ hexToBigInt(keccak256(concatHex([tableId, ...keyTuple]))), { size: 32 });
+}