/
tx.ts
181 lines (165 loc) · 6.01 KB
/
tx.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/**
* Copyright 2023 Shinami Corp.
* SPDX-License-Identifier: Apache-2.0
*/
import {
SuiClient,
SuiTransactionBlockResponse,
SuiTransactionBlockResponseOptions,
} from "@mysten/sui.js/client";
import { GasStationClient } from "@shinami/clients";
import { NextApiHandler, NextApiRequest } from "next";
import { validate } from "superstruct";
import { ApiErrorBody } from "../../error.js";
import { PreparedTransactionBytes, SignedTransactionBytes } from "../../tx.js";
import { ZkLoginUser, assembleZkLoginSignature } from "../../user.js";
import { withZkLoginUserRequired } from "./session.js";
import { catchAllDispatcher, methodDispatcher } from "./utils.js";
export interface GaslessTransactionBytesWithBudget {
gaslessTxBytes: string;
gasBudget?: number; // Will use auto-budget if omitted.
}
export type GaslessTransactionBytesBuilder<TAuth = unknown> = (
req: NextApiRequest,
user: ZkLoginUser<TAuth>
) =>
| Promise<GaslessTransactionBytesWithBudget>
| GaslessTransactionBytesWithBudget;
export type TransactionBytesBuilder<TAuth = unknown> = (
req: NextApiRequest,
user: ZkLoginUser<TAuth>
) => Promise<string> | string;
export type TransactionResponseParser<TAuth = unknown, TRes = unknown> = (
req: NextApiRequest,
txRes: SuiTransactionBlockResponse,
user: ZkLoginUser<TAuth>
) => Promise<TRes> | TRes;
export class InvalidRequest extends Error {}
function txHandler<TAuth = unknown>(
buildTxBytes: TransactionBytesBuilder<TAuth>
): NextApiHandler<PreparedTransactionBytes | ApiErrorBody> {
return methodDispatcher({
POST: async (req, res) => {
const user = req.session.user! as ZkLoginUser<TAuth>;
let txBase64;
try {
txBase64 = await buildTxBytes(req, user);
} catch (e) {
if (!(e instanceof InvalidRequest)) throw e;
return res.status(400).json({ error: e.message });
}
res.json({ txBytes: txBase64 });
},
});
}
function sponsoredTxHandler<TAuth = unknown>(
gas: GasStationClient,
buildGaslessTxBytes: GaslessTransactionBytesBuilder<TAuth>
): NextApiHandler<PreparedTransactionBytes | ApiErrorBody> {
return methodDispatcher({
POST: async (req, res) => {
const user = req.session.user! as ZkLoginUser<TAuth>;
let tx;
try {
tx = await buildGaslessTxBytes(req, user);
} catch (e) {
if (!(e instanceof InvalidRequest)) throw e;
return res.status(400).json({ error: e.message });
}
const { txBytes, signature } = await gas.sponsorTransactionBlock(
tx.gaslessTxBytes,
user.wallet,
tx.gasBudget
);
res.json({ txBytes, gasSignature: signature });
},
});
}
function execHandler<TAuth = unknown, TRes = unknown>(
sui: SuiClient,
parseTxRes: TransactionResponseParser<TAuth, TRes>,
txOptions: SuiTransactionBlockResponseOptions = {}
): NextApiHandler<TRes | ApiErrorBody> {
return methodDispatcher({
POST: async (req, res) => {
const [error, body] = validate(req.body, SignedTransactionBytes, {
mask: true,
});
if (error) return res.status(400).json({ error: error.message });
const user = req.session.user! as ZkLoginUser<TAuth>;
const zkSignature = assembleZkLoginSignature(user, body.signature);
const txRes = await sui.executeTransactionBlock({
transactionBlock: body.txBytes,
signature: body.gasSignature
? [zkSignature, body.gasSignature]
: zkSignature,
options: { ...txOptions, showEffects: true },
});
if (txRes.effects?.status.status !== "success") {
console.error("Tx execution failed", txRes);
return res.status(500).json({
error: `Tx execution failed: ${txRes.effects?.status.error}`,
});
}
res.json(await parseTxRes(req, txRes, user));
},
});
}
/**
* Implements API routes for building and executing a Sui transaction block.
*
* Two routes are implemented under the hood:
* - [base_route]/tx for building the transaction block.
* - [base_route]/exec for executing the transaction block after signed by frontend, and parsing the
* transaction response.
*
* @param sui `SuiClient` for transaction building and execution.
* @param buildTxBytes Function to build a transaction block (encoded in Base64).
* @param parseTxRes Function to parse the transaction response.
* @param txOptions Transaction response options.
* @returns A Next.js API route handler.
*/
export function zkLoginTxExecHandler<TAuth = unknown, TRes = unknown>(
sui: SuiClient,
buildTxBytes: TransactionBytesBuilder<TAuth>,
parseTxRes: TransactionResponseParser<TAuth, TRes>,
txOptions: SuiTransactionBlockResponseOptions = {}
): NextApiHandler {
return withZkLoginUserRequired(
sui,
catchAllDispatcher({
tx: txHandler(buildTxBytes),
exec: execHandler(sui, parseTxRes, txOptions),
})
);
}
/**
* Implements API routes for building, sponsoring, and executing a Sui transaction block.
*
* Two routes are implemented under the hood:
* - [base_route]/tx for building and sponsoring the transaction block.
* - [base_route]/exec for executing the transaction block after signed by frontend, and parsing the
* transaction response.
*
* @param sui `SuiClient` for transaction building and execution.
* @param gas `GasStationClient` for sponsoring transaction block.
* @param buildGaslessTxBytes Function to build a gasless transaction block (encoded in Base64).
* @param parseTxRes Function to parse the transaction response.
* @param txOptions Transaction response options.
* @returns A Next.js API route handler.
*/
export function zkLoginSponsoredTxExecHandler<TAuth = unknown, TRes = unknown>(
sui: SuiClient,
gas: GasStationClient,
buildGaslessTxBytes: GaslessTransactionBytesBuilder<TAuth>,
parseTxRes: TransactionResponseParser<TAuth, TRes>,
txOptions: SuiTransactionBlockResponseOptions = {}
): NextApiHandler {
return withZkLoginUserRequired(
sui,
catchAllDispatcher({
tx: sponsoredTxHandler(gas, buildGaslessTxBytes),
exec: execHandler(sui, parseTxRes, txOptions),
})
);
}