Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ test-results/
.superpowers/
.kilo/
kilo.json
.local-sdk/
2 changes: 1 addition & 1 deletion app/api/register/[name]/fees/[coin]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import type { BYOCSymbol } from "@metanames/sdk/dist/providers/config";
import type { BYOCSymbol } from "@metanames/sdk/providers/config";
import { getServerSdk } from "@/lib/sdk";
import { handleError, jsonError } from "@/lib/server-error";
import { validateDomainName, normalizeDomain } from "@/lib/domain-validator";
Expand Down
2 changes: 1 addition & 1 deletion app/tld/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getServerSdk } from "@/lib/sdk";
import { TldPageClient } from "./TldPageClient";
import Loading from "./loading";
import type { Domain } from "@/lib/types";
import type { Domain as SdkDomain } from "@metanames/sdk/dist/models/domain";
import type { Domain as SdkDomain } from "@metanames/sdk/models/domain";

export const metadata = {
title: "TLD Information",
Expand Down
2 changes: 1 addition & 1 deletion components/domain-payment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { RequireWalletConnection } from "@/components/require-wallet-connection"
import { useDomainPayment } from "@/lib/hooks/use-domain-payment";
import { Minus, Plus, Loader2, Check } from "lucide-react";
import { cn } from "@/lib/utils";
import type { BYOCSymbol as SdkBYOCSymbol } from "@metanames/sdk/dist/providers/config";
import type { BYOCSymbol as SdkBYOCSymbol } from "@metanames/sdk/providers/config";

interface DomainPaymentProps {
domain: string;
Expand Down
2 changes: 1 addition & 1 deletion components/subdomain-registration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useWalletStore } from "@/lib/stores/wallet-store";
import { useSdkStore } from "@/lib/stores/sdk-store";
import { explorerTransactionUrl } from "@/lib/url";
import { toast } from "sonner";
import type { BYOCSymbol as SdkBYOCSymbol } from "@metanames/sdk/dist/providers/config";
import type { BYOCSymbol as SdkBYOCSymbol } from "@metanames/sdk/providers/config";

interface SubdomainRegistrationProps {
domain: string;
Expand Down
62 changes: 56 additions & 6 deletions lib/__tests__/wallet-connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ vi.mock("@ledgerhq/hw-transport-webusb", () => ({
}));

const ledgerGetAddress = vi.fn();
vi.mock("@metanames/sdk/dist/transactions/ledger", () => ({
vi.mock("@metanames/sdk/transactions/ledger", () => ({
PartisiaLedgerClient: class {
constructor(public transport: unknown) {}
getAddress = ledgerGetAddress;
Expand Down Expand Up @@ -56,17 +56,20 @@ function mockSdk() {
};
}

/** Install (or remove) the MetaMask-shaped globals the connector reads. */
/** Install (or remove) the injected provider the connector reads. */
function setEthereum(
value: { isMetaMask?: boolean; request?: unknown } | null,
value: {
isMetaMask?: boolean;
request?: unknown;
providers?: unknown[];
} | null,
) {
const w = window as unknown as Record<string, unknown>;
if (value === null) {
delete w.isMetaMask;
delete w.request;
delete w.ethereum;
return;
}
Object.assign(w, value);
w.ethereum = value;
}

describe("connectMetaMask", () => {
Expand Down Expand Up @@ -99,6 +102,53 @@ describe("connectMetaMask", () => {
expect(sdk.setSigningStrategy).not.toHaveBeenCalled();
});

// Extensions inject on `window.ethereum`, never on `window` itself: a
// connector reading the flag off the window finds nothing in a real browser.
it("refuses when the flag is on the window instead of the provider", async () => {
setEthereum(null);
const w = window as unknown as Record<string, unknown>;
w.isMetaMask = true;
w.request = vi.fn();
const sdk = mockSdk();

await expect(connectMetaMask(sdk)).rejects.toThrow("MetaMask not found");

delete w.isMetaMask;
delete w.request;
});

// Several wallets installed at once: the last one to load owns
// `window.ethereum` and lists the others under `providers`.
it("finds MetaMask among several injected providers", async () => {
const request = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce("mm-address");
setEthereum({
isMetaMask: false,
request: vi.fn(),
providers: [
{ isMetaMask: false, request: vi.fn() },
{ isMetaMask: true, request },
],
});
const sdk = mockSdk();

await expect(connectMetaMask(sdk)).resolves.toBe("mm-address");
});

// The snap answers with the address itself; older builds wrapped it.
it("accepts the address as a bare string", async () => {
const request = vi
.fn()
.mockResolvedValueOnce(undefined)
.mockResolvedValueOnce("bare-address");
setEthereum({ isMetaMask: true, request });
const sdk = mockSdk();

await expect(connectMetaMask(sdk)).resolves.toBe("bare-address");
});

// A snap that resolves without an address must not leave the app "connected"
// to an account it cannot name.
it("refuses when the snap returns no address", async () => {
Expand Down
2 changes: 1 addition & 1 deletion lib/hooks/use-domain-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "@/lib/error";
import { bridgeUrl, explorerTransactionUrl } from "@/lib/url";
import type { FeesResponse } from "@/lib/types";
import type { BYOCSymbol } from "@metanames/sdk/dist/providers/config";
import type { BYOCSymbol } from "@metanames/sdk/providers/config";
import { toast } from "sonner";
import { track } from "@vercel/analytics";

Expand Down
2 changes: 1 addition & 1 deletion lib/stores/sdk-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { create } from "zustand";
import type { MetaNamesSdk } from "@metanames/sdk";
import type { BYOCSymbol } from "@metanames/sdk/dist/providers/config";
import type { BYOCSymbol } from "@metanames/sdk/providers/config";

interface SdkStore {
metaNamesSdk: MetaNamesSdk | null;
Expand Down
2 changes: 1 addition & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface AlertMessage {
message: string;
action?: { label: string; onClick: () => void };
}
import type { BYOCSymbol as SdkBYOCSymbol } from "@metanames/sdk/dist/providers/config";
import type { BYOCSymbol as SdkBYOCSymbol } from "@metanames/sdk/providers/config";

export type BYOCSymbol = SdkBYOCSymbol;
// Testnet coins - for mainnet coins use sdk.config.byoc at runtime
Expand Down
36 changes: 29 additions & 7 deletions lib/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
import type { MetaNamesSdk } from "@metanames/sdk";
import type { MetaMaskSdk } from "@metanames/sdk/dist/interface";
import type { MetaMaskSdk } from "@metanames/sdk/interface";
import type { PermissionTypes } from "partisia-blockchain-applications-sdk/lib/sdk-listeners";
import { config } from "./config";

interface EthereumProvider extends MetaMaskSdk {
isMetaMask?: boolean;
/** Set when several wallet extensions are installed side by side. */
providers?: EthereumProvider[];
}

/**
* The MetaMask provider, or nothing when it is not installed.
*
* Extensions inject themselves on `window.ethereum`, not on `window`. When more
* than one is installed they share that slot: whichever loaded last owns it and
* the rest are listed under `providers`, so the flag has to be checked on each
* entry rather than on the slot itself.
*/
function metaMaskProvider(): EthereumProvider | undefined {
const injected = (window as { ethereum?: EthereumProvider }).ethereum;
if (!injected) return undefined;
if (injected.providers?.length)
return injected.providers.find((provider) => provider.isMetaMask);

return injected.isMetaMask ? injected : undefined;
}

export async function connectMetaMask(sdk: MetaNamesSdk): Promise<string> {
const eth = window as unknown as EthereumProvider;
if (!eth?.isMetaMask) throw new Error("MetaMask not found");
const eth = metaMaskProvider();
if (!eth) throw new Error("MetaMask not found");
await eth.request({
method: "wallet_requestSnaps",
params: { "npm:@partisiablockchain/snap": {} },
});
// The snap answers `get_address` with the address itself. Older builds
// wrapped it in an object, so both shapes are read.
const res = (await eth.request({
method: "wallet_invokeSnap",
params: {
snapId: "npm:@partisiablockchain/snap",
request: { method: "get_address" },
},
})) as { address?: string };
if (!res?.address) throw new Error("No address from MetaMask");
})) as string | { address?: string } | undefined;
const address = typeof res === "string" ? res : res?.address;
if (!address) throw new Error("No address from MetaMask");
sdk.setSigningStrategy("MetaMask", eth);
return res.address;
return address;
}
export async function connectPartisiaWallet(
sdk: MetaNamesSdk,
Expand All @@ -45,7 +67,7 @@ export async function connectLedger(sdk: MetaNamesSdk): Promise<string> {
const { default: TransportWebUSB } =
await import("@ledgerhq/hw-transport-webusb");
const { PartisiaLedgerClient } =
await import("@metanames/sdk/dist/transactions/ledger");
await import("@metanames/sdk/transactions/ledger");
const transport = await TransportWebUSB.create();
const client = new PartisiaLedgerClient(transport);
const address = await client.getAddress();
Expand Down
Loading
Loading