Skip to content

Commit

Permalink
feat(entrykit): initial release (#3419)
Browse files Browse the repository at this point in the history
Co-authored-by: alvarius <[email protected]>
  • Loading branch information
holic and alvrs authored Jan 9, 2025
1 parent 1b477d4 commit 971ffed
Show file tree
Hide file tree
Showing 129 changed files with 9,154 additions and 1,407 deletions.
5 changes: 5 additions & 0 deletions .changeset/dirty-buttons-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/entrykit": patch
---

Initial, experimental release of EntryKit.
6 changes: 4 additions & 2 deletions packages/common/src/deploy/ensureContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Contract = {
bytecode: Hex;
deployedBytecodeSize?: number;
debugLabel?: string;
salt?: Hex;
};

export async function ensureContract({
Expand All @@ -16,6 +17,7 @@ export async function ensureContract({
bytecode,
deployedBytecodeSize,
debugLabel = "contract",
salt = singletonSalt,
}: {
readonly client: Client<Transport, Chain | undefined, Account>;
readonly deployerAddress: Hex;
Expand All @@ -24,7 +26,7 @@ export async function ensureContract({
throw new Error(`Found unlinked public library in ${debugLabel} bytecode`);
}

const address = getCreate2Address({ from: deployerAddress, salt: singletonSalt, bytecode });
const address = getCreate2Address({ from: deployerAddress, salt, bytecode });

const contractCode = await getCode(client, { address, blockTag: "pending" });
if (contractCode) {
Expand All @@ -50,7 +52,7 @@ export async function ensureContract({
await sendTransaction(client, {
chain: client.chain ?? null,
to: deployerAddress,
data: concatHex([singletonSalt, bytecode]),
data: concatHex([salt, bytecode]),
}),
];
}
4 changes: 3 additions & 1 deletion packages/common/src/deploy/getContractAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { singletonSalt } from "./common";
export function getContractAddress({
deployerAddress,
bytecode,
salt = singletonSalt,
}: {
readonly deployerAddress: Hex;
readonly bytecode: Hex;
readonly salt?: Hex;
}): Hex {
return getCreate2Address({ from: deployerAddress, bytecode, salt: singletonSalt });
return getCreate2Address({ from: deployerAddress, bytecode, salt });
}
5 changes: 4 additions & 1 deletion packages/common/src/utils/uniqueBy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export function uniqueBy<value, key>(values: readonly value[], getKey: (value: value) => key): readonly value[] {
const map = new Map<key, value>();
for (const value of values) {
map.set(getKey(value), value);
const key = getKey(value);
if (!map.has(key)) {
map.set(key, value);
}
}
return Array.from(map.values());
}
2 changes: 1 addition & 1 deletion packages/common/src/waitForTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export async function waitForTransactions({
}: {
readonly client: Client<Transport, Chain | undefined, Account>;
readonly hashes: readonly Hex[];
readonly debugLabel: string;
readonly debugLabel?: string;
}): Promise<void> {
if (!hashes.length) return;

Expand Down
7 changes: 7 additions & 0 deletions packages/entrykit/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": ["../../.eslintrc", "plugin:react/recommended", "plugin:react-hooks/recommended"],
"plugins": ["react", "react-hooks"],
"rules": {
"react/react-in-jsx-scope": "off"
}
}
13 changes: 13 additions & 0 deletions packages/entrykit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# EntryKit

UI kit to streamline signing in to MUD apps.

## Installation

```
npm install @latticexyz/entrykit
```

## Usage

TODO
3 changes: 3 additions & 0 deletions packages/entrykit/bin/deploy-local-prereqs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env node
// workaround for https://github.com/pnpm/pnpm/issues/1801
import "../dist/tsup/bin/deploy-local-prereqs.js";
4 changes: 4 additions & 0 deletions packages/entrykit/inline-import.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module "*?inline" {
const content: string;
export default content;
}
29 changes: 29 additions & 0 deletions packages/entrykit/mprocs.deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
scrollback: 10000
procs:
anvil:
shell: anvil --dump-state playground/anvil-state.json

deploy-prereqs:
shell: ./bin/deploy-local-prereqs.js
env:
DEBUG: "mud:*"
# Anvil default account (0x70997970C51812dc3A010C7d01b50e0d17dc79C8)
PRIVATE_KEY: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"

# deploy-quarry-paymaster:
# cwd: ../../../quarry-paymaster/packages/contracts
# shell: pnpm mud deploy --salt 0x && pnpm fund:paymaster:local && pnpm fund:issuer:local
# env:
# DEBUG: "mud:*"

deploy-game:
cwd: ../../test/mock-game-contracts
shell: pnpm deploy:local --salt 0x
env:
DEBUG: "mud:*"
# Anvil default account (0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc)
PRIVATE_KEY: "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba"

# deploy-bundler:
# cwd: ../../../alto
# shell: pnpm ts-node scripts/localDeployer/index.ts
18 changes: 18 additions & 0 deletions packages/entrykit/mprocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
scrollback: 10000
procs:
client:
shell: pnpm vite dev playground
anvil:
shell: anvil --block-time 2 --load-state playground/anvil-state.json

# mud:
# cwd: ../..
# shell: pnpm run dev --filter=!@latticexyz/entrykit

# quarry-issuer:
# cwd: ../../../quarry-paymaster/packages/issuer
# shell: pnpm start

# bundler:
# cwd: ../../../alto
# shell: pnpm ts-node src --config ../mud/packages/entrykit/playground/alto.config.json
98 changes: 98 additions & 0 deletions packages/entrykit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"name": "@latticexyz/entrykit",
"version": "2.2.14",
"description": "User onboarding flows for MUD apps",
"repository": {
"type": "git",
"url": "https://github.com/latticexyz/mud.git",
"directory": "packages/entrykit"
},
"license": "MIT",
"type": "module",
"exports": {
".": "./dist/tsup/exports/index.js",
"./internal": "./dist/tsup/exports/internal.js"
},
"typesVersions": {
"*": {
"index": [
"./dist/tsup/exports/index.d.ts"
],
"internal": [
"./dist/tsup/exports/internal.d.ts"
]
}
},
"bin": {
"deploy-local-prereqs": "./bin/deploy-local-prereqs.js"
},
"files": [
"dist"
],
"scripts": {
"build": "pnpm run build:js",
"build:js": "tsup",
"clean": "pnpm run clean:js",
"clean:js": "shx rm -rf dist",
"data:relay-chains": "tsx src/scripts/get-relay-chains.ts",
"dev": "tsup --watch",
"playground": "mprocs",
"playground:deploy": "mprocs --config mprocs.deploy.yaml",
"test": "tsc --noEmit && vitest",
"test:ci": "tsc --noEmit && vitest --run"
},
"dependencies": {
"@account-abstraction/contracts": "^0.7.0",
"@ark/util": "0.2.2",
"@latticexyz/common": "workspace:*",
"@latticexyz/config": "workspace:*",
"@latticexyz/paymaster": "workspace:*",
"@latticexyz/protocol-parser": "workspace:*",
"@latticexyz/store": "workspace:*",
"@latticexyz/world": "workspace:*",
"@latticexyz/world-modules": "workspace:*",
"@radix-ui/react-dialog": "^1.0.5",
"@rainbow-me/rainbowkit": "2.1.7",
"debug": "^4.3.4",
"dotenv": "^16.0.3",
"permissionless": "^0.2.3",
"react-error-boundary": "^4.0.13",
"react-merge-refs": "^2.1.1",
"tailwind-merge": "^1.12.0",
"usehooks-ts": "^3.1.0",
"zustand": "^4.5.2"
},
"devDependencies": {
"@tanstack/react-query": "^5.56.2",
"@types/debug": "^4.1.7",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
"@types/ws": "^8.5.4",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0",
"mprocs": "^0.7.1",
"postcss": "^8.4.47",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "^3.4.13",
"tsup": "^6.7.0",
"viem": "2.21.19",
"vite": "^5.4.1",
"vite-plugin-dts": "^4.2.4",
"vite-plugin-externalize-deps": "^0.8.0",
"vitest": "0.34.6",
"wagmi": "2.12.11"
},
"peerDependencies": {
"@tanstack/react-query": "5.x",
"react": "18.x",
"react-dom": "18.x",
"viem": "2.x",
"wagmi": "2.x"
},
"publishConfig": {
"access": "public"
}
}
42 changes: 42 additions & 0 deletions packages/entrykit/playground/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffect } from "react";
import { useLocalStorage } from "usehooks-ts";
import { UserWrite } from "./UserWrite";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { SessionWrite } from "./SessionWrite";
import { useAccountModal } from "../src/useAccountModal";
import { AccountButton } from "../src/AccountButton";

export function App() {
const { openAccountModal } = useAccountModal();

const [openModal, setOpenModal] = useLocalStorage<boolean>("mud:entryKitPlayground:openModalOnMount", false);

useEffect(() => {
if (openModal) {
openAccountModal();
}
}, [openAccountModal, openModal]);

return (
<div style={{ display: "flex", flexDirection: "column", gap: "0.5em" }}>
<div>
<AccountButton />
</div>
<div>
<label style={{ display: "flex", gap: "0.25em" }}>
<input type="checkbox" checked={openModal} onChange={(event) => setOpenModal(event.currentTarget.checked)} />
Open modal on mount
</label>
</div>
<div>
<ConnectButton />
</div>
<div>
<UserWrite />
</div>
<div>
<SessionWrite />
</div>
</div>
);
}
48 changes: 48 additions & 0 deletions packages/entrykit/playground/SessionWrite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useEntryKitConfig } from "../src/EntryKitConfigProvider";
import { useSessionClientReady } from "../src/useSessionClientReady";
import { getContract } from "viem";
import { mockGameAbi } from "./mockGame";
import { useMemo } from "react";
import { waitForTransactionReceipt } from "viem/actions";
import { getAction } from "viem/utils";
import { useClient } from "wagmi";

export function SessionWrite() {
const { chainId, worldAddress } = useEntryKitConfig();
const client = useClient({ chainId });
const { data: sessionClient } = useSessionClientReady();

const worldContract = useMemo(
() =>
sessionClient
? getContract({
client: sessionClient,
address: worldAddress,
abi: mockGameAbi,
})
: null,
[sessionClient, worldAddress],
);

return (
<div>
<button
disabled={!client || !worldContract}
onClick={async () => {
if (!client) throw new Error("Client not ready.");
if (!worldContract) throw new Error("World contract not ready");

console.log("writing from session account");
const hash = await worldContract.write.move([2, 2]);
console.log("got tx", hash);
const receipt = await getAction(client, waitForTransactionReceipt, "waitForTransactionReceipt")({ hash });
console.log("got receipt", receipt);
}}
>
Session write
</button>
<p>world: {worldAddress}</p>
<p>session: {sessionClient?.account.address}</p>
</div>
);
}
52 changes: 52 additions & 0 deletions packages/entrykit/playground/UserWrite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { useEntryKitConfig } from "../src/EntryKitConfigProvider";

export function UserWrite() {
const { chainId, worldAddress } = useEntryKitConfig();

const { writeContractAsync, data: hash } = useWriteContract();
const { data: receipt } = useWaitForTransactionReceipt({ hash });

return (
<div>
<button
onClick={async () => {
console.log("writing from user");

const hash = await writeContractAsync({
chainId,
address: worldAddress,
abi: [
{
type: "function",
name: "move",
inputs: [
{
name: "x",
type: "int32",
internalType: "int32",
},
{
name: "y",
type: "int32",
internalType: "int32",
},
],
outputs: [],
stateMutability: "nonpayable",
},
],
functionName: "move",
args: [1, 1],
});

console.log("got tx", hash);
}}
>
User write
</button>
<br />
tx: {hash ?? "??"} ({receipt?.status ?? "??"})
</div>
);
}
Loading

0 comments on commit 971ffed

Please sign in to comment.