Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/noter/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
ARG NEXT_PUBLIC_ENOKI_API_KEY
ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID
ARG NEXT_PUBLIC_SUI_NETWORK
ARG NEXT_PUBLIC_SUI_GRPC_URL
ARG NEXT_PUBLIC_MEMWAL_PACKAGE_ID
ARG NEXT_PUBLIC_MEMWAL_REGISTRY_ID
ARG NEXT_PUBLIC_MEMWAL_SERVER_URL

ENV NEXT_PUBLIC_ENOKI_API_KEY=$NEXT_PUBLIC_ENOKI_API_KEY
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID
ENV NEXT_PUBLIC_SUI_NETWORK=$NEXT_PUBLIC_SUI_NETWORK
ENV NEXT_PUBLIC_SUI_GRPC_URL=$NEXT_PUBLIC_SUI_GRPC_URL
ENV NEXT_PUBLIC_MEMWAL_PACKAGE_ID=$NEXT_PUBLIC_MEMWAL_PACKAGE_ID
ENV NEXT_PUBLIC_MEMWAL_REGISTRY_ID=$NEXT_PUBLIC_MEMWAL_REGISTRY_ID
ENV NEXT_PUBLIC_MEMWAL_SERVER_URL=$NEXT_PUBLIC_MEMWAL_SERVER_URL
Expand Down
49 changes: 13 additions & 36 deletions apps/noter/app/components/enoki-login-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import { createSponsorAuthorization } from "@mysten-incubation/memwal";
import { Loader2 } from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import { enokiConfig } from "@/lib/enoki/config";
import {
fetchAccountIdForOwner,
findCreatedObjectByType,
} from "@/lib/sui-client-compat";
import { useAuth } from "@/feature/auth";
import { trpc } from "@/shared/lib/trpc/client";

Expand Down Expand Up @@ -197,30 +201,11 @@ export function EnokiLoginCard() {
let knownAccountId: string | null = null;

try {
const registryObj = await suiClient.getObject({
id: enokiConfig.memwalRegistryId,
options: { showContent: true },
});
if (
registryObj?.data?.content &&
"fields" in registryObj.data.content
) {
const fields = registryObj.data.content.fields as any;
const tableId = fields?.accounts?.fields?.id?.id;
if (tableId) {
const dynField = await suiClient.getDynamicFieldObject({
parentId: tableId,
name: { type: "address", value: address },
});
if (
dynField?.data?.content &&
"fields" in dynField.data.content
) {
knownAccountId = (dynField.data.content.fields as any)
.value as string;
}
}
}
knownAccountId = await fetchAccountIdForOwner(
suiClient,
enokiConfig.memwalRegistryId,
address,
);
} catch {
// Dynamic field not found → no account yet
}
Expand Down Expand Up @@ -267,19 +252,11 @@ export function EnokiLoginCard() {
);
await suiClient.waitForTransaction({ digest: createResult.digest });

const txDetails = await suiClient.getTransactionBlock({
digest: createResult.digest,
options: { showObjectChanges: true },
});
const createdObj = txDetails.objectChanges?.find(
(c) =>
c.type === "created" &&
"objectType" in c &&
c.objectType.includes("MemWalAccount"),
knownAccountId = await findCreatedObjectByType(
suiClient,
createResult.digest,
"MemWalAccount",
);
if (createdObj && "objectId" in createdObj) {
knownAccountId = createdObj.objectId;
}

if (!knownAccountId) {
throw new Error("Account created but object ID not found. Please try again.");
Expand Down
18 changes: 17 additions & 1 deletion apps/noter/app/components/sui-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,29 @@ import {
useSuiClientContext,
} from "@mysten/dapp-kit";
import { isEnokiNetwork, registerEnokiWallets } from "@mysten/enoki";
import { getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc";
import { SuiGrpcClient } from "@mysten/sui/grpc";
import {
getJsonRpcFullnodeUrl,
SuiJsonRpcClient,
type SuiJsonRpcClientOptions,
} from "@mysten/sui/jsonRpc";
import { enokiConfig } from "@/lib/enoki/config";

const { networkConfig } = createNetworkConfig({
testnet: { url: getJsonRpcFullnodeUrl("testnet"), network: "testnet" },
mainnet: { url: getJsonRpcFullnodeUrl("mainnet"), network: "mainnet" },
});

function createClientForNetwork(name: string, options: SuiJsonRpcClientOptions) {
if (name === enokiConfig.suiNetwork && enokiConfig.suiGrpcUrl) {
return new SuiGrpcClient({
network: name,
baseUrl: enokiConfig.suiGrpcUrl,
}) as unknown as SuiJsonRpcClient;
}
return new SuiJsonRpcClient(options);
}

/** Registers Enoki wallets (Google OAuth) with dapp-kit on mount. No-op if env vars are missing. */
function RegisterEnokiWallets() {
const { client, network } = useSuiClientContext();
Expand Down Expand Up @@ -45,6 +60,7 @@ export function SuiProviders({ children }: { children: React.ReactNode }) {
<SuiClientProvider
networks={networkConfig}
defaultNetwork={enokiConfig.suiNetwork}
createClient={createClientForNetwork}
>
<RegisterEnokiWallets />
<WalletProvider autoConnect>{children}</WalletProvider>
Expand Down
1 change: 1 addition & 0 deletions apps/noter/lib/enoki/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const enokiConfig = {
suiNetwork: (process.env.NEXT_PUBLIC_SUI_NETWORK || "testnet") as
| "testnet"
| "mainnet",
suiGrpcUrl: process.env.NEXT_PUBLIC_SUI_GRPC_URL || "",
memwalPackageId: process.env.NEXT_PUBLIC_MEMWAL_PACKAGE_ID || "",
memwalRegistryId: process.env.NEXT_PUBLIC_MEMWAL_REGISTRY_ID || "",
memwalServerUrl:
Expand Down
127 changes: 127 additions & 0 deletions apps/noter/lib/sui-client-compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { isSuiGrpcClient, type SuiGrpcClient } from "@mysten/sui/grpc";
import { fromHex, normalizeSuiAddress, toHex } from "@mysten/sui/utils";

interface JsonRpcClientLike {
getObject(input: { id: string; options: { showContent: boolean } }): Promise<{
data?: { content?: { fields?: unknown } };
}>;
getDynamicFieldObject(input: {
parentId: string;
name: { type: string; value: string };
}): Promise<{ data?: { content?: { fields?: unknown } } }>;
getTransactionBlock(input: {
digest: string;
options: { showObjectChanges: boolean };
}): Promise<{
objectChanges?: Array<{
type: string;
objectId?: string;
objectType?: string;
}>;
}>;
}

function unwrapJsonRpcFields(value: unknown): unknown {
if (Array.isArray(value)) return value.map(unwrapJsonRpcFields);
if (value && typeof value === "object") {
const object = value as Record<string, unknown>;
const inner =
"fields" in object && object.fields && typeof object.fields === "object"
? object.fields
: object;
return Object.fromEntries(
Object.entries(inner as Record<string, unknown>).map(([key, item]) => [
key,
unwrapJsonRpcFields(item),
]),
);
}
return value;
}

async function fetchObjectJson(
client: unknown,
objectId: string,
): Promise<Record<string, unknown> | null> {
if (isSuiGrpcClient(client)) {
const response = await client.getObject({
objectId,
include: { json: true },
});
return response.object.json ?? null;
}

const response = await (client as JsonRpcClientLike).getObject({
id: objectId,
options: { showContent: true },
});
const fields = response.data?.content?.fields;
return fields
? (unwrapJsonRpcFields(fields) as Record<string, unknown>)
: null;
}

export async function fetchAccountIdForOwner(
client: unknown,
registryId: string,
ownerAddress: string,
): Promise<string | null> {
const registry = await fetchObjectJson(client, registryId);
const rawTableId = (
registry?.accounts as { id?: string | { id?: string } } | undefined
)?.id;
const tableId =
typeof rawTableId === "string" ? rawTableId : rawTableId?.id;
if (!tableId) return null;

if (isSuiGrpcClient(client)) {
const response = await client.getDynamicField({
parentId: tableId,
name: {
type: "address",
bcs: fromHex(normalizeSuiAddress(ownerAddress)),
},
});
const value = response.dynamicField?.value?.bcs;
return value?.length === 32 ? `0x${toHex(value)}` : null;
}

const response = await (client as JsonRpcClientLike).getDynamicFieldObject({
parentId: tableId,
name: { type: "address", value: ownerAddress },
});
const fields = response.data?.content?.fields;
if (!fields || typeof fields !== "object") return null;
const value = (fields as Record<string, unknown>).value;
return typeof value === "string" ? value : null;
}

export async function findCreatedObjectByType(
client: unknown,
digest: string,
objectType: string,
): Promise<string | null> {
if (isSuiGrpcClient(client)) {
const response = await (client as SuiGrpcClient).getTransaction({
digest,
include: { effects: true, objectTypes: true },
});
const transaction = response.Transaction ?? response.FailedTransaction;
const created = transaction.effects?.changedObjects.find(
(change) =>
change.idOperation === "Created" &&
transaction.objectTypes?.[change.objectId]?.includes(objectType),
);
return created?.objectId ?? null;
}

const response = await (client as JsonRpcClientLike).getTransactionBlock({
digest,
options: { showObjectChanges: true },
});
const created = response.objectChanges?.find(
(change) =>
change.type === "created" && change.objectType?.includes(objectType),
);
return created?.objectId ?? null;
}
2 changes: 2 additions & 0 deletions apps/researcher/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ ENV AUTH_SECRET="build-time-placeholder"
ARG NEXT_PUBLIC_ENOKI_API_KEY
ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID
ARG NEXT_PUBLIC_SUI_NETWORK
ARG NEXT_PUBLIC_SUI_GRPC_URL
ARG NEXT_PUBLIC_MEMWAL_PACKAGE_ID
ARG NEXT_PUBLIC_MEMWAL_REGISTRY_ID
ARG NEXT_PUBLIC_MEMWAL_SERVER_URL

ENV NEXT_PUBLIC_ENOKI_API_KEY=$NEXT_PUBLIC_ENOKI_API_KEY
ENV NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID
ENV NEXT_PUBLIC_SUI_NETWORK=$NEXT_PUBLIC_SUI_NETWORK
ENV NEXT_PUBLIC_SUI_GRPC_URL=$NEXT_PUBLIC_SUI_GRPC_URL
ENV NEXT_PUBLIC_MEMWAL_PACKAGE_ID=$NEXT_PUBLIC_MEMWAL_PACKAGE_ID
ENV NEXT_PUBLIC_MEMWAL_REGISTRY_ID=$NEXT_PUBLIC_MEMWAL_REGISTRY_ID
ENV NEXT_PUBLIC_MEMWAL_SERVER_URL=$NEXT_PUBLIC_MEMWAL_SERVER_URL
Expand Down
49 changes: 13 additions & 36 deletions apps/researcher/components/enoki-login-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { enokiConfig } from "@/lib/enoki/config";
import {
fetchAccountIdForOwner,
findCreatedObjectByType,
} from "@/lib/sui-client-compat";

type Step =
| "idle"
Expand Down Expand Up @@ -222,30 +226,11 @@ export function EnokiLoginCard() {

// Check if a Walrus Memory account already exists for this address
try {
const registryObj = await suiClient.getObject({
id: enokiConfig.memwalRegistryId,
options: { showContent: true },
});
if (
registryObj?.data?.content &&
"fields" in registryObj.data.content
) {
const fields = registryObj.data.content.fields as any;
const tableId = fields?.accounts?.fields?.id?.id;
if (tableId) {
const dynField = await suiClient.getDynamicFieldObject({
parentId: tableId,
name: { type: "address", value: address },
});
if (
dynField?.data?.content &&
"fields" in dynField.data.content
) {
knownAccountId = (dynField.data.content.fields as any)
.value as string;
}
}
}
knownAccountId = await fetchAccountIdForOwner(
suiClient,
enokiConfig.memwalRegistryId,
address,
);
} catch {
// Dynamic field not found → no account yet
}
Expand Down Expand Up @@ -298,19 +283,11 @@ export function EnokiLoginCard() {
});

// Find the created account object
const txDetails = await suiClient.getTransactionBlock({
digest: createResult.digest,
options: { showObjectChanges: true },
});
const createdObj = txDetails.objectChanges?.find(
(c) =>
c.type === "created" &&
"objectType" in c &&
c.objectType.includes("MemWalAccount"),
knownAccountId = await findCreatedObjectByType(
suiClient,
createResult.digest,
"MemWalAccount",
);
if (createdObj && "objectId" in createdObj) {
knownAccountId = createdObj.objectId;
}

if (!knownAccountId) {
throw new Error(
Expand Down
18 changes: 17 additions & 1 deletion apps/researcher/components/sui-providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
useSuiClientContext,
} from "@mysten/dapp-kit";
import { isEnokiNetwork, registerEnokiWallets } from "@mysten/enoki";
import { getJsonRpcFullnodeUrl } from "@mysten/sui/jsonRpc";
import { SuiGrpcClient } from "@mysten/sui/grpc";
import {
getJsonRpcFullnodeUrl,
SuiJsonRpcClient,
type SuiJsonRpcClientOptions,
} from "@mysten/sui/jsonRpc";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { enokiConfig } from "@/lib/enoki/config";

Expand All @@ -17,6 +22,16 @@ const { networkConfig } = createNetworkConfig({
mainnet: { url: getJsonRpcFullnodeUrl("mainnet"), network: "mainnet" },
});

function createClientForNetwork(name: string, options: SuiJsonRpcClientOptions) {
if (name === enokiConfig.suiNetwork && enokiConfig.suiGrpcUrl) {
return new SuiGrpcClient({
network: name,
baseUrl: enokiConfig.suiGrpcUrl,
}) as unknown as SuiJsonRpcClient;
}
return new SuiJsonRpcClient(options);
}

const queryClient = new QueryClient();

/** Registers Enoki wallets (Google OAuth) with dapp-kit on mount. No-op if env vars are missing. */
Expand Down Expand Up @@ -49,6 +64,7 @@ export function SuiProviders({ children }: { children: React.ReactNode }) {
<SuiClientProvider
networks={networkConfig}
defaultNetwork={enokiConfig.suiNetwork}
createClient={createClientForNetwork}
>
<RegisterEnokiWallets />
<WalletProvider autoConnect>{children}</WalletProvider>
Expand Down
1 change: 1 addition & 0 deletions apps/researcher/lib/enoki/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const enokiConfig = {
suiNetwork: (process.env.NEXT_PUBLIC_SUI_NETWORK || "testnet") as
| "testnet"
| "mainnet",
suiGrpcUrl: process.env.NEXT_PUBLIC_SUI_GRPC_URL || "",
memwalPackageId: process.env.NEXT_PUBLIC_MEMWAL_PACKAGE_ID || "",
memwalRegistryId: process.env.NEXT_PUBLIC_MEMWAL_REGISTRY_ID || "",
memwalServerUrl:
Expand Down
Loading
Loading