Skip to content

Commit 783cd5e

Browse files
lljxx1georgeroman
andauthored
feat: add deposit nonce binding (#79)
* feat: add hypeliquid nonce mapping * feat: cleanup logs and handle nonce duplication via primary key constraint * feat: include chainId in saving * feat: add version * feat: use signatureChainId for signature validation * feat: use signatureChainId for signature validation * feat: add new test case and wallet normalize * refactor: minor tweaks --------- Co-authored-by: George Roman <[email protected]>
1 parent 04d6945 commit 783cd5e

File tree

10 files changed

+1730
-1752
lines changed

10 files changed

+1730
-1752
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Up migration
2+
3+
CREATE TABLE "nonce_mappings" (
4+
"wallet_chain_id" TEXT NOT NULL,
5+
"wallet" TEXT NOT NULL,
6+
"nonce" TEXT NOT NULL,
7+
"id" TEXT NOT NULL,
8+
"signature_chain_id" TEXT NOT NULL,
9+
"signature" TEXT NOT NULL,
10+
"created_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
11+
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT now()
12+
);
13+
14+
ALTER TABLE "nonce_mappings"
15+
ADD CONSTRAINT "nonce_mappings_pk"
16+
PRIMARY KEY ("wallet_chain_id", "wallet", "nonce");
17+
18+
-- Down migration
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Type } from "@fastify/type-provider-typebox";
2+
import { verifyTypedData, zeroAddress } from "viem";
3+
4+
import {
5+
Endpoint,
6+
ErrorResponse,
7+
FastifyReplyTypeBox,
8+
FastifyRequestTypeBox,
9+
} from "../../utils";
10+
import { ChainMetadataEthereumVm, getChain } from "../../../common/chains";
11+
import { externalError } from "../../../common/error";
12+
import { saveNonceMapping } from "../../../models/nonce-mappings";
13+
14+
const Schema = {
15+
body: Type.Object({
16+
walletChainId: Type.String({
17+
description: "The chain id of the wallet",
18+
}),
19+
wallet: Type.String({
20+
description: "The wallet address",
21+
}),
22+
nonce: Type.String({
23+
description: "The nonce to associate the id to",
24+
}),
25+
id: Type.String({
26+
description: "The id to associate the nonce to",
27+
}),
28+
signatureChainId: Type.String({
29+
description: "The chain id of the signature",
30+
}),
31+
signature: Type.String({
32+
description: "The signature for the mapping",
33+
}),
34+
}),
35+
response: {
36+
200: Type.Object({
37+
message: Type.String({ description: "Success message" }),
38+
}),
39+
...ErrorResponse,
40+
},
41+
};
42+
43+
export default {
44+
method: "POST",
45+
url: "/actions/nonce-mappings/v1",
46+
schema: Schema,
47+
handler: async (
48+
req: FastifyRequestTypeBox<typeof Schema>,
49+
reply: FastifyReplyTypeBox<typeof Schema>
50+
) => {
51+
const { walletChainId, wallet, nonce, id, signatureChainId, signature } =
52+
req.body;
53+
54+
const NONCE_MAPPING_DOMAIN = (chainId: number) => ({
55+
name: "RelayNonceMapping",
56+
version: "1",
57+
chainId,
58+
verifyingContract: zeroAddress,
59+
});
60+
61+
const NONCE_MAPPING_TYPES = {
62+
NonceMapping: [
63+
{ name: "wallet", type: "address" },
64+
{ name: "id", type: "bytes32" },
65+
{ name: "nonce", type: "uint256" },
66+
],
67+
};
68+
69+
const message = {
70+
wallet: wallet as `0x${string}`,
71+
id: id as `0x${string}`,
72+
nonce: BigInt(nonce),
73+
};
74+
75+
const signatureChain = await getChain(signatureChainId);
76+
if (signatureChain.vmType !== "ethereum-vm") {
77+
throw externalError("Unsupported signature chain", "INVALID_SIGNATURE");
78+
}
79+
80+
const isValidSignature = await verifyTypedData({
81+
address: wallet as `0x${string}`,
82+
domain: NONCE_MAPPING_DOMAIN(
83+
(signatureChain.metadata as ChainMetadataEthereumVm).chainId
84+
),
85+
types: NONCE_MAPPING_TYPES,
86+
primaryType: "NonceMapping",
87+
message,
88+
signature: signature as `0x${string}`,
89+
});
90+
if (!isValidSignature) {
91+
throw externalError("Invalid signature", "INVALID_SIGNATURE");
92+
}
93+
94+
const saveResult = await saveNonceMapping({
95+
walletChainId,
96+
wallet,
97+
nonce,
98+
id,
99+
signatureChainId,
100+
signature,
101+
});
102+
if (!saveResult) {
103+
throw externalError(
104+
"Nonce mapping already exists",
105+
"NONCE_MAPPING_ALREADY_EXISTS"
106+
);
107+
}
108+
109+
return reply.status(200).send({ message: "Success" });
110+
},
111+
} as Endpoint;

src/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Endpoint, errorWrapper } from "./utils";
55
// Actions
66
import actionsDepositoryDepositsV1 from "./actions/depository-deposits/v1";
77
import actionsDepositoryWithdrawalsV1 from "./actions/depository-withdrawals/v1";
8+
import actionsNonceMappingsV1 from "./actions/nonce-mappings/v1";
89
import actionsSolverFillsV1 from "./actions/solver-fills/v1";
910
import actionsSolverRefundsV1 from "./actions/solver-refunds/v1";
1011

@@ -13,6 +14,7 @@ import queriesBalanceLocksV1 from "./queries/balance-locks/v1";
1314
import queriesBalancesV1 from "./queries/balances/v1";
1415
import queriesChainsV1 from "./queries/chains/v1";
1516
import queriesConfigsV1 from "./queries/configs/v1";
17+
import queriesNonceMappingsV1 from "./queries/nonce-mappings/v1";
1618
import queriesWithdrawalRequestsV1 from "./queries/withdrawal-requests/v1";
1719

1820
// Requests
@@ -23,11 +25,13 @@ import requestsWithdrawalsSignaturesV1 from "./requests/withdrawals-signatures/v
2325
const endpoints = [
2426
actionsDepositoryDepositsV1,
2527
actionsDepositoryWithdrawalsV1,
28+
actionsNonceMappingsV1,
2629
actionsSolverFillsV1,
2730
actionsSolverRefundsV1,
2831
queriesBalanceLocksV1,
2932
queriesBalancesV1,
3033
queriesChainsV1,
34+
queriesNonceMappingsV1,
3135
queriesConfigsV1,
3236
queriesWithdrawalRequestsV1,
3337
requestsUnlocksV1,
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Type } from "@fastify/type-provider-typebox";
2+
3+
import {
4+
Endpoint,
5+
ErrorResponse,
6+
FastifyReplyTypeBox,
7+
FastifyRequestTypeBox,
8+
} from "../../utils";
9+
import { externalError } from "../../../common/error";
10+
import { getNonceMapping } from "../../../models/nonce-mappings";
11+
12+
const Schema = {
13+
params: Type.Object({
14+
walletChainId: Type.String({
15+
description: "The chain id of the wallet",
16+
}),
17+
wallet: Type.String({
18+
description: "The wallet address",
19+
}),
20+
nonce: Type.String({
21+
description: "The nonce to lookup",
22+
}),
23+
}),
24+
response: {
25+
200: Type.Object({
26+
walletChainId: Type.String(),
27+
wallet: Type.String(),
28+
nonce: Type.String(),
29+
id: Type.String(),
30+
signatureChainId: Type.String(),
31+
signature: Type.String(),
32+
}),
33+
...ErrorResponse,
34+
},
35+
};
36+
37+
export default {
38+
method: "GET",
39+
url: "/queries/nonce-mappings/:walletChainId/:wallet/:nonce/v1",
40+
schema: Schema,
41+
handler: async (
42+
req: FastifyRequestTypeBox<typeof Schema>,
43+
reply: FastifyReplyTypeBox<typeof Schema>
44+
) => {
45+
const { walletChainId, wallet, nonce } = req.params;
46+
47+
const nonceMapping = await getNonceMapping(walletChainId, wallet, nonce);
48+
if (!nonceMapping) {
49+
throw externalError("Nonce mapping not found", "NONCE_MAPPING_NOT_FOUND");
50+
}
51+
52+
return reply.status(200).send({
53+
walletChainId: nonceMapping.walletChainId,
54+
wallet: nonceMapping.wallet,
55+
nonce: nonceMapping.nonce,
56+
id: nonceMapping.id,
57+
signatureChainId: nonceMapping.signatureChainId,
58+
signature: nonceMapping.signature,
59+
});
60+
},
61+
} as Endpoint;

src/common/error.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ type ErrorCode =
33
| "INVALID_SIGNATURE"
44
| "INSUFFICIENT_SIGNATURES"
55
| "UNAUTHORIZED_ORACLE"
6-
| "UNSUPPORTED_SIGNATURE";
6+
| "UNSUPPORTED_SIGNATURE"
7+
| "NONCE_MAPPING_ALREADY_EXISTS"
8+
| "NONCE_MAPPING_NOT_FOUND";
79

810
// Returns an error which can safely be exposed externally
911
export const externalError = (

src/models/nonce-mappings.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { getChainVmType } from "@reservoir0x/relay-protocol-sdk/dist/utils";
2+
import { ITask } from "pg-promise";
3+
4+
import { DbEntry, nvAddress } from "./utils";
5+
import { getSdkChainsConfig } from "../common/chains";
6+
import { db } from "../common/db";
7+
8+
export type NonceMapping = {
9+
walletChainId: string;
10+
wallet: string;
11+
nonce: string;
12+
id: string;
13+
signatureChainId: string;
14+
signature: string;
15+
};
16+
17+
const resultToNonceMapping = (result: any): DbEntry<NonceMapping> => ({
18+
walletChainId: result.wallet_chain_id,
19+
wallet: result.wallet,
20+
nonce: result.nonce,
21+
id: result.id,
22+
signatureChainId: result.signature_chain_id,
23+
signature: result.signature,
24+
createdAt: result.created_at,
25+
updatedAt: result.updated_at,
26+
});
27+
28+
export const getNonceMapping = async (
29+
walletChainId: string,
30+
wallet: string,
31+
nonce: string,
32+
options?: {
33+
tx?: ITask<any>;
34+
}
35+
): Promise<DbEntry<NonceMapping> | undefined> => {
36+
const result = await (options?.tx ?? db).oneOrNone(
37+
`
38+
SELECT
39+
nonce_mappings.wallet_chain_id,
40+
nonce_mappings.wallet,
41+
nonce_mappings.nonce,
42+
nonce_mappings.id,
43+
nonce_mappings.signature_chain_id,
44+
nonce_mappings.signature,
45+
nonce_mappings.created_at,
46+
nonce_mappings.updated_at
47+
FROM nonce_mappings
48+
WHERE nonce_mappings.wallet_chain_id = $/walletChainId/
49+
AND nonce_mappings.wallet = $/wallet/
50+
AND nonce_mappings.nonce = $/nonce/
51+
`,
52+
{
53+
walletChainId,
54+
wallet,
55+
nonce,
56+
}
57+
);
58+
if (!result) {
59+
return undefined;
60+
}
61+
62+
return resultToNonceMapping(result);
63+
};
64+
65+
export const saveNonceMapping = async (
66+
nonceMapping: NonceMapping,
67+
options?: {
68+
tx?: ITask<any>;
69+
}
70+
): Promise<DbEntry<NonceMapping> | undefined> => {
71+
const result = await (options?.tx ?? db).oneOrNone(
72+
`
73+
INSERT INTO nonce_mappings (
74+
wallet_chain_id,
75+
wallet,
76+
nonce,
77+
id,
78+
signature_chain_id,
79+
signature
80+
) VALUES (
81+
$/walletChainId/,
82+
$/wallet/,
83+
$/nonce/,
84+
$/id/,
85+
$/signatureChainId/,
86+
$/signature/
87+
)
88+
ON CONFLICT DO NOTHING
89+
RETURNING *
90+
`,
91+
{
92+
walletChainId: nonceMapping.walletChainId,
93+
wallet: nvAddress(
94+
nonceMapping.wallet,
95+
getChainVmType(nonceMapping.walletChainId, await getSdkChainsConfig())
96+
),
97+
nonce: nonceMapping.nonce,
98+
id: nonceMapping.id,
99+
signatureChainId: nonceMapping.signatureChainId,
100+
signature: nonceMapping.signature,
101+
}
102+
);
103+
if (!result) {
104+
return undefined;
105+
}
106+
107+
return resultToNonceMapping(result);
108+
};

test/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default async () => {
2323
id: chain.id,
2424
vmType: chain.vmType,
2525
depository: chain.depository,
26-
metadata: {},
26+
metadata: chain.metadata,
2727
}
2828
);
2929
}

test/unit/execute-depository-withdrawal.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,6 @@ describe("execute-depository-withdrawal", () => {
146146

147147
const dbBalance = await getBalance(chainId, owner, chainId, currency);
148148
expect(dbBalance).toBeTruthy();
149-
if (
150-
dbBalance?.availableAmount !==
151-
inMemoryBalances[key].availableAmount.toString()
152-
) {
153-
console.log(key, inMemoryBalances[key], dbBalance);
154-
}
155149
expect(
156150
dbBalance?.availableAmount ===
157151
inMemoryBalances[key].availableAmount.toString()

0 commit comments

Comments
 (0)