Skip to content

Commit 6c91c9b

Browse files
committed
refactor(morpho-sdk): split midnight requirement helpers
1 parent e4bb79d commit 6c91c9b

8 files changed

Lines changed: 422 additions & 173 deletions
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { createMockClient, mockRead } from "@morpho-org/test/mock";
2+
import type { Chain } from "viem";
3+
import { erc20Abi } from "viem";
4+
import { describe, expect, test } from "vitest";
5+
import {
6+
midnightAddresses,
7+
midnightChainId,
8+
} from "../../../../test/fixtures/midnight.js";
9+
import { ChainIdMismatchError } from "../../../types/index.js";
10+
import { getMidnightApprovalRequirements } from "./getMidnightApprovalRequirements.js";
11+
12+
const midnightTestChain = {
13+
id: midnightChainId,
14+
name: "Midnight Test",
15+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
16+
rpcUrls: { default: { http: ["http://localhost"] } },
17+
} as const satisfies Chain;
18+
19+
const wrongChain = {
20+
...midnightTestChain,
21+
id: midnightChainId + 1,
22+
} as const satisfies Chain;
23+
24+
describe("getMidnightApprovalRequirements", () => {
25+
test("throws ChainIdMismatchError when the client chain differs", async () => {
26+
const { client } = createMockClient(wrongChain);
27+
28+
await expect(
29+
getMidnightApprovalRequirements({
30+
viemClient: client,
31+
chainId: midnightChainId,
32+
token: midnightAddresses.loanToken,
33+
owner: midnightAddresses.taker,
34+
spender: midnightAddresses.midnightBundles,
35+
amount: 1n,
36+
}),
37+
).rejects.toThrow(ChainIdMismatchError);
38+
});
39+
40+
test("returns no approval when amount is zero", async () => {
41+
const { client } = createMockClient(midnightTestChain);
42+
43+
await expect(
44+
getMidnightApprovalRequirements({
45+
viemClient: client,
46+
chainId: midnightChainId,
47+
token: midnightAddresses.loanToken,
48+
owner: midnightAddresses.taker,
49+
spender: midnightAddresses.midnightBundles,
50+
amount: 0n,
51+
}),
52+
).resolves.toEqual([]);
53+
});
54+
55+
test("returns an approval when allowance is insufficient", async () => {
56+
const handle = createMockClient(midnightTestChain);
57+
mockRead(handle, {
58+
address: midnightAddresses.loanToken,
59+
abi: erc20Abi,
60+
functionName: "allowance",
61+
result: 0n,
62+
});
63+
64+
const requirements = await getMidnightApprovalRequirements({
65+
viemClient: handle.client,
66+
chainId: midnightChainId,
67+
token: midnightAddresses.loanToken,
68+
owner: midnightAddresses.taker,
69+
spender: midnightAddresses.midnightBundles,
70+
amount: 1_000n,
71+
});
72+
73+
expect(requirements).toHaveLength(1);
74+
expect(requirements[0]?.action.type).toBe("erc20Approval");
75+
expect(requirements[0]?.action.args.spender).toBe(
76+
midnightAddresses.midnightBundles,
77+
);
78+
expect(requirements[0]?.action.args.amount).toBe(1_000n);
79+
});
80+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { Address, Client } from "viem";
2+
import { erc20Abi } from "viem";
3+
import { readContract } from "viem/actions";
4+
import { validateChainId } from "../../../helpers/index.js";
5+
import type { ERC20ApprovalAction, Transaction } from "../../../types/index.js";
6+
import { getRequirementsApproval } from "../getRequirementsApproval.js";
7+
8+
/** Parameters for {@link getMidnightApprovalRequirements}. */
9+
export interface GetMidnightApprovalRequirementsParams {
10+
readonly viemClient: Client;
11+
readonly chainId: number;
12+
readonly token: Address;
13+
readonly owner: Address;
14+
readonly spender: Address;
15+
readonly amount: bigint;
16+
}
17+
18+
/**
19+
* Resolves classic ERC20 approval requirements for a Midnight spender.
20+
*
21+
* @param params - Approval resolution parameters.
22+
* @returns Approval transactions required for `spender` to pull `amount`.
23+
* @throws {ChainIdMismatchError} when the viem client is connected to another chain.
24+
* @example
25+
* ```ts
26+
* import { getMidnightApprovalRequirements } from "@morpho-org/morpho-sdk";
27+
*
28+
* const approvals = await getMidnightApprovalRequirements({
29+
* viemClient: client,
30+
* chainId: 8453,
31+
* token: loanToken,
32+
* owner: user,
33+
* spender: midnightBundles,
34+
* amount: 1_000_000n,
35+
* });
36+
* console.log(approvals.length);
37+
* ```
38+
*/
39+
export const getMidnightApprovalRequirements = async (
40+
params: GetMidnightApprovalRequirementsParams,
41+
): Promise<readonly Readonly<Transaction<ERC20ApprovalAction>>[]> => {
42+
validateChainId(params.viemClient.chain?.id, params.chainId);
43+
44+
if (params.amount === 0n) return [];
45+
46+
const allowance = await readContract(params.viemClient, {
47+
address: params.token,
48+
abi: erc20Abi,
49+
functionName: "allowance",
50+
args: [params.owner, params.spender],
51+
});
52+
53+
return getRequirementsApproval({
54+
address: params.token,
55+
chainId: params.chainId,
56+
args: {
57+
spender: params.spender,
58+
spendAmount: params.amount,
59+
approvalAmount: params.amount,
60+
},
61+
allowances: allowance,
62+
});
63+
};
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { midnightAbi } from "@morpho-org/midnight-sdk";
2+
import { createMockClient, mockRead } from "@morpho-org/test/mock";
3+
import type { Chain } from "viem";
4+
import { describe, expect, test } from "vitest";
5+
import {
6+
midnightAddresses,
7+
midnightChainId,
8+
} from "../../../../test/fixtures/midnight.js";
9+
import { ChainIdMismatchError } from "../../../types/index.js";
10+
import { getMidnightAuthorizationRequirement } from "./getMidnightAuthorizationRequirement.js";
11+
12+
const midnightTestChain = {
13+
id: midnightChainId,
14+
name: "Midnight Test",
15+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
16+
rpcUrls: { default: { http: ["http://localhost"] } },
17+
} as const satisfies Chain;
18+
19+
const wrongChain = {
20+
...midnightTestChain,
21+
id: midnightChainId + 1,
22+
} as const satisfies Chain;
23+
24+
describe("getMidnightAuthorizationRequirement", () => {
25+
test("throws ChainIdMismatchError when the client chain differs", async () => {
26+
const { client } = createMockClient(wrongChain);
27+
28+
await expect(
29+
getMidnightAuthorizationRequirement({
30+
viemClient: client,
31+
chainId: midnightChainId,
32+
owner: midnightAddresses.taker,
33+
authorized: midnightAddresses.midnightBundles,
34+
}),
35+
).rejects.toThrow(ChainIdMismatchError);
36+
});
37+
38+
test("returns null when already authorized", async () => {
39+
const handle = createMockClient(midnightTestChain);
40+
mockRead(handle, {
41+
address: midnightAddresses.midnight,
42+
abi: midnightAbi,
43+
functionName: "isAuthorized",
44+
result: true,
45+
});
46+
47+
await expect(
48+
getMidnightAuthorizationRequirement({
49+
viemClient: handle.client,
50+
chainId: midnightChainId,
51+
owner: midnightAddresses.taker,
52+
authorized: midnightAddresses.midnightBundles,
53+
}),
54+
).resolves.toBeNull();
55+
});
56+
57+
test("builds an authorization transaction when authorization is missing", async () => {
58+
const handle = createMockClient(midnightTestChain);
59+
mockRead(handle, {
60+
address: midnightAddresses.midnight,
61+
abi: midnightAbi,
62+
functionName: "isAuthorized",
63+
result: false,
64+
});
65+
66+
const tx = await getMidnightAuthorizationRequirement({
67+
viemClient: handle.client,
68+
chainId: midnightChainId,
69+
owner: midnightAddresses.taker,
70+
authorized: midnightAddresses.midnightBundles,
71+
});
72+
73+
expect(tx?.to).toBe(midnightAddresses.midnight);
74+
expect(tx?.action.type).toBe("midnightAuthorization");
75+
expect(tx?.action.args.authorized).toBe(midnightAddresses.midnightBundles);
76+
});
77+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { midnightAbi } from "@morpho-org/midnight-sdk";
2+
import { getChainAddress } from "@morpho-org/morpho-ts";
3+
import type { Address, Client } from "viem";
4+
import { readContract } from "viem/actions";
5+
import { validateChainId } from "../../../helpers/index.js";
6+
import type {
7+
MidnightAuthorizationAction,
8+
Transaction,
9+
} from "../../../types/index.js";
10+
import { midnightAuthorization } from "../../midnight/index.js";
11+
12+
/** Parameters for {@link getMidnightAuthorizationRequirement}. */
13+
export interface GetMidnightAuthorizationRequirementParams {
14+
readonly viemClient: Client;
15+
readonly chainId: number;
16+
readonly owner: Address;
17+
readonly authorized: Address;
18+
}
19+
20+
/**
21+
* Resolves the Midnight authorization transaction for a ratifier or bundle spender.
22+
*
23+
* @param params - Authorization resolution parameters.
24+
* @returns Authorization transaction, or `null` when already authorized.
25+
* @throws {ChainIdMismatchError} when the viem client is connected to another chain.
26+
* @example
27+
* ```ts
28+
* import { getMidnightAuthorizationRequirement } from "@morpho-org/morpho-sdk";
29+
*
30+
* const tx = await getMidnightAuthorizationRequirement({
31+
* viemClient: client,
32+
* chainId: 8453,
33+
* owner: user,
34+
* authorized: midnightBundles,
35+
* });
36+
* console.log(tx?.action.type);
37+
* ```
38+
*/
39+
export const getMidnightAuthorizationRequirement = async (
40+
params: GetMidnightAuthorizationRequirementParams,
41+
): Promise<Readonly<Transaction<MidnightAuthorizationAction>> | null> => {
42+
validateChainId(params.viemClient.chain?.id, params.chainId);
43+
44+
const midnight = getChainAddress(params.chainId, "midnight");
45+
const isAuthorized = await readContract(params.viemClient, {
46+
address: midnight,
47+
abi: midnightAbi,
48+
functionName: "isAuthorized",
49+
args: [params.owner, params.authorized],
50+
});
51+
52+
if (isAuthorized) return null;
53+
54+
return midnightAuthorization({
55+
chainId: params.chainId,
56+
authorized: params.authorized,
57+
onBehalf: params.owner,
58+
});
59+
};
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { setterRatifierAbi } from "@morpho-org/midnight-sdk";
2+
import { createMockClient, mockRead } from "@morpho-org/test/mock";
3+
import type { Chain, Hex } from "viem";
4+
import { describe, expect, test } from "vitest";
5+
import {
6+
midnightAddresses,
7+
midnightChainId,
8+
} from "../../../../test/fixtures/midnight.js";
9+
import { ChainIdMismatchError } from "../../../types/index.js";
10+
import { getMidnightRatifyRootRequirement } from "./getMidnightRatifyRootRequirement.js";
11+
12+
const midnightTestChain = {
13+
id: midnightChainId,
14+
name: "Midnight Test",
15+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
16+
rpcUrls: { default: { http: ["http://localhost"] } },
17+
} as const satisfies Chain;
18+
19+
const wrongChain = {
20+
...midnightTestChain,
21+
id: midnightChainId + 1,
22+
} as const satisfies Chain;
23+
24+
const root =
25+
"0x1111111111111111111111111111111111111111111111111111111111111111" as Hex;
26+
27+
describe("getMidnightRatifyRootRequirement", () => {
28+
test("throws ChainIdMismatchError when the client chain differs", async () => {
29+
const { client } = createMockClient(wrongChain);
30+
31+
await expect(
32+
getMidnightRatifyRootRequirement({
33+
viemClient: client,
34+
chainId: midnightChainId,
35+
maker: midnightAddresses.maker,
36+
root,
37+
}),
38+
).rejects.toThrow(ChainIdMismatchError);
39+
});
40+
41+
test("returns null when the root is already ratified", async () => {
42+
const handle = createMockClient(midnightTestChain);
43+
mockRead(handle, {
44+
address: midnightAddresses.setterRatifier,
45+
abi: setterRatifierAbi,
46+
functionName: "isRootRatified",
47+
result: true,
48+
});
49+
50+
await expect(
51+
getMidnightRatifyRootRequirement({
52+
viemClient: handle.client,
53+
chainId: midnightChainId,
54+
maker: midnightAddresses.maker,
55+
root,
56+
}),
57+
).resolves.toBeNull();
58+
});
59+
60+
test("builds a ratify-root transaction when the root is not ratified", async () => {
61+
const handle = createMockClient(midnightTestChain);
62+
mockRead(handle, {
63+
address: midnightAddresses.setterRatifier,
64+
abi: setterRatifierAbi,
65+
functionName: "isRootRatified",
66+
result: false,
67+
});
68+
69+
const tx = await getMidnightRatifyRootRequirement({
70+
viemClient: handle.client,
71+
chainId: midnightChainId,
72+
maker: midnightAddresses.maker,
73+
root,
74+
});
75+
76+
expect(tx?.to).toBe(midnightAddresses.setterRatifier);
77+
expect(tx?.action.type).toBe("midnightRatifyRoot");
78+
expect(tx?.action.args.maker).toBe(midnightAddresses.maker);
79+
expect(tx?.action.args.root).toBe(root);
80+
});
81+
});

0 commit comments

Comments
 (0)