Skip to content

Commit 5f6c71d

Browse files
karooolisfrolic
andauthored
feat(entrykit): quarry paymaster relay.link top-up (#3688)
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
1 parent f7aa4c5 commit 5f6c71d

36 files changed

Lines changed: 2825 additions & 60 deletions

.changeset/ninety-keys-fold.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/entrykit": patch
3+
---
4+
5+
Added support for Quarry paymaster top-up via relay.link deposit form.

packages/entrykit/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
"@latticexyz/world": "workspace:*",
5454
"@latticexyz/world-module-callwithsignature": "workspace:*",
5555
"@radix-ui/react-dialog": "^1.0.5",
56+
"@radix-ui/react-select": "^1.0.5",
5657
"@rainbow-me/rainbowkit": "2.1.7",
58+
"@reservoir0x/relay-sdk": "^1.7.0",
5759
"debug": "^4.3.4",
5860
"dotenv": "^16.0.3",
5961
"permissionless": "0.2.30",

packages/entrykit/src/formatGas.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { formatGwei } from "viem";
2+
3+
export function formatGas(wei: bigint) {
4+
// TODO: should this support non-ether decimals?
5+
const formatted = formatGwei(wei);
6+
const magnitude = Math.floor(parseFloat(formatted)).toString().length;
7+
return parseFloat(formatted).toLocaleString("en-US", { maximumFractionDigits: Math.max(0, 6 - magnitude) });
8+
}

packages/entrykit/src/getPaymaster.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Chain, Hex } from "viem";
33
export type Paymaster = {
44
readonly type: "simple" | "quarry";
55
readonly address: Hex;
6+
readonly isGasPass?: boolean;
67
};
78

89
export function getPaymaster(chain: Chain): Paymaster | undefined {
@@ -13,6 +14,7 @@ export function getPaymaster(chain: Chain): Paymaster | undefined {
1314
return {
1415
type: "quarry",
1516
address: contracts.quarryPaymaster.address,
17+
isGasPass: !!chain.rpcUrls.quarryPassIssuer?.http?.[0],
1618
};
1719
}
1820
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { IconSVG, Props } from "./IconSVG";
2+
3+
export function ArrowLeftIcon(props: Props) {
4+
return (
5+
<IconSVG {...props}>
6+
<path
7+
d="M19 12H5M12 19l-7-7 7-7"
8+
fill="none"
9+
stroke="currentColor"
10+
strokeWidth="2"
11+
strokeLinecap="round"
12+
strokeLinejoin="round"
13+
/>
14+
</IconSVG>
15+
);
16+
}

packages/entrykit/src/onboarding/ConnectedSteps.tsx

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { useEffect, useMemo, useRef, useState } from "react";
2-
import { ConnectedClient } from "../common";
2+
import { Address } from "viem";
33
import { twMerge } from "tailwind-merge";
4+
import { ConnectedClient } from "../common";
45
import { usePrerequisites } from "./usePrerequisites";
56
import { Wallet } from "./Wallet";
67
import { Allowance } from "./quarry/Allowance";
78
import { Session } from "./Session";
89
import { Step } from "./common";
9-
import { Address } from "viem";
1010
import { useAccountModal } from "../useAccountModal";
1111
import { useEntryKitConfig } from "../EntryKitConfigProvider";
1212
import { getPaymaster } from "../getPaymaster";
1313
import { GasBalance } from "./GasBalance";
14+
import { GasBalance as GasBalanceQuarry } from "./quarry/GasBalance";
1415

1516
export type Props = {
1617
userClient: ConnectedClient;
@@ -20,6 +21,7 @@ export type Props = {
2021
export function ConnectedSteps({ userClient, initialUserAddress }: Props) {
2122
const { chain } = useEntryKitConfig();
2223
const paymaster = getPaymaster(chain);
24+
const [focusedId, setFocusedId] = useState<string | null>(null);
2325

2426
const userAddress = userClient.account.address;
2527
const { data: prerequisites, error: prerequisitesError } = usePrerequisites(userAddress);
@@ -47,7 +49,8 @@ export function ConnectedSteps({ userClient, initialUserAddress }: Props) {
4749
}
4850
}, [closeAccountModal, isNewConnection, prerequisites]);
4951

50-
const { sessionAddress, hasAllowance, isSpender, hasDelegation, hasGasBalance } = prerequisites ?? {};
52+
const { sessionAddress, hasAllowance, isSpender, hasDelegation, hasGasBalance, hasQuarryBalance } =
53+
prerequisites ?? {};
5154

5255
const steps = useMemo((): readonly Step[] => {
5356
if (!userAddress) {
@@ -77,11 +80,19 @@ export function ConnectedSteps({ userClient, initialUserAddress }: Props) {
7780
});
7881
}
7982
} else if (paymaster.type === "quarry") {
80-
steps.push({
81-
id: "allowance",
82-
isComplete: !!hasAllowance,
83-
content: (props) => <Allowance {...props} userAddress={userAddress} />,
84-
});
83+
if (paymaster.isGasPass) {
84+
steps.push({
85+
id: "allowance",
86+
isComplete: !!hasAllowance,
87+
content: (props) => <Allowance {...props} userAddress={userAddress} />,
88+
});
89+
} else {
90+
steps.push({
91+
id: "gasBalanceQuarry",
92+
isComplete: !!hasQuarryBalance,
93+
content: (props) => <GasBalanceQuarry {...props} userAddress={userAddress} />,
94+
});
95+
}
8596
}
8697

8798
steps.push({
@@ -93,7 +104,17 @@ export function ConnectedSteps({ userClient, initialUserAddress }: Props) {
93104
});
94105

95106
return steps;
96-
}, [hasAllowance, hasDelegation, hasGasBalance, isSpender, paymaster, sessionAddress, userAddress, userClient]);
107+
}, [
108+
hasAllowance,
109+
hasDelegation,
110+
hasGasBalance,
111+
hasQuarryBalance,
112+
isSpender,
113+
paymaster,
114+
sessionAddress,
115+
userAddress,
116+
userClient,
117+
]);
97118

98119
const [selectedStepId] = useState<null | string>(null);
99120
const nextStep = steps.find((step) => step.content != null && !step.isComplete);
@@ -107,19 +128,35 @@ export function ConnectedSteps({ userClient, initialUserAddress }: Props) {
107128
return (
108129
<div
109130
className={twMerge(
110-
// steps.length === 2 ? "min-h-[22rem]" : "min-h-[26rem]",
111-
"px-8 flex flex-col divide-y divide-neutral-800",
131+
"px-8 flex flex-col",
132+
focusedId && "divide-y divide-neutral-800",
112133
"animate-in animate-duration-300 fade-in slide-in-from-bottom-8",
113134
)}
114135
>
115136
{steps.map((step, i) => {
116137
const isActive = step === activeStep;
117138
const isExpanded = isActive || completedSteps.length === steps.length;
118139
const isDisabled = !step.isComplete && activeStepIndex !== -1 && i > activeStepIndex;
140+
const isFocused = focusedId === step.id;
141+
142+
const content = step.content({
143+
isActive,
144+
isExpanded,
145+
isFocused,
146+
setFocused: (focused: boolean) => setFocusedId(focused ? step.id : null),
147+
});
148+
149+
if (focusedId) {
150+
if (step.id === focusedId) {
151+
return content;
152+
}
153+
return null;
154+
}
155+
119156
return (
120157
<div key={step.id} className={twMerge("py-8 flex flex-col justify-center", isActive ? "flex-grow" : null)}>
121158
<div className={twMerge("flex flex-col", isDisabled ? "opacity-30 pointer-events-none" : null)}>
122-
{step.content({ isActive, isExpanded })}
159+
{content}
123160
</div>
124161
</div>
125162
);

packages/entrykit/src/onboarding/GasBalance.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@ import { useBalance, useWatchBlockNumber } from "wagmi";
66
import { useEntryKitConfig } from "../EntryKitConfigProvider";
77
import relayChains from "../data/relayChains.json";
88
import { useSetBalance } from "./useSetBalance";
9-
import { RelayChains } from "./common";
9+
import { RelayChains, StepContentProps } from "./common";
1010
import { TruncatedHex } from "../ui/TruncatedHex";
1111
import { useShowMutationError } from "../errors/useShowMutationError";
1212
import { useShowQueryError } from "../errors/useShowQueryError";
1313

14-
export type Props = {
15-
isExpanded: boolean;
16-
isActive: boolean;
14+
export type Props = StepContentProps & {
1715
sessionAddress: Hex;
1816
};
1917

@@ -24,7 +22,6 @@ export function GasBalance({ isActive, isExpanded, sessionAddress }: Props) {
2422
useWatchBlockNumber({ onBlockNumber: () => balance.refetch() });
2523

2624
const setBalance = useShowMutationError(useSetBalance());
27-
2825
const relayChain = (relayChains as RelayChains)[chain.id];
2926

3027
return (

packages/entrykit/src/onboarding/Session.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { useEffect } from "react";
55
import { useSessionClient } from "../useSessionClient";
66
import { useShowQueryError } from "../errors/useShowQueryError";
77
import { useShowMutationError } from "../errors/useShowMutationError";
8+
import { StepContentProps } from "./common";
89

9-
export type Props = {
10-
isActive: boolean;
11-
isExpanded: boolean;
10+
export type Props = StepContentProps & {
1211
userClient: ConnectedClient;
1312
registerSpender: boolean;
1413
registerDelegation: boolean;

packages/entrykit/src/onboarding/Wallet.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import { Button } from "../ui/Button";
55
import { useAccountModal } from "../useAccountModal";
66
import { Hex } from "viem";
77
import { useShowMutationError } from "../errors/useShowMutationError";
8+
import { StepContentProps } from "./common";
89

9-
export type Props = {
10-
isActive: boolean;
11-
isExpanded: boolean;
10+
export type Props = StepContentProps & {
1211
userAddress: Hex;
1312
};
1413

@@ -18,7 +17,6 @@ export function Wallet({ isActive, isExpanded, userAddress }: Props) {
1817
const { closeAccountModal } = useAccountModal();
1918

2019
// TODO: render ENS avatar if available?
21-
2220
return (
2321
<div className="flex flex-col gap-4">
2422
<div className="flex justify-between gap-4">

packages/entrykit/src/onboarding/common.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { ReactNode } from "react";
22

3+
export type StepContentProps = {
4+
isActive: boolean;
5+
isExpanded: boolean;
6+
isFocused: boolean;
7+
setFocused: (isFocused: boolean) => void;
8+
};
9+
310
export type Step = {
411
id: string;
512
isComplete: boolean;
6-
content: (props: { isActive: boolean; isExpanded: boolean }) => ReactNode;
13+
content: (props: StepContentProps) => ReactNode;
714
};
815

916
export type RelayChain = {

0 commit comments

Comments
 (0)