@t402/tezos 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/dist/exact-direct/client/index.d.cts +92 -0
- package/dist/exact-direct/client/index.d.ts +92 -0
- package/dist/exact-direct/client/index.js +204 -0
- package/dist/exact-direct/client/index.js.map +1 -0
- package/dist/exact-direct/client/index.mjs +176 -0
- package/dist/exact-direct/client/index.mjs.map +1 -0
- package/dist/exact-direct/facilitator/index.d.cts +110 -0
- package/dist/exact-direct/facilitator/index.d.ts +110 -0
- package/dist/exact-direct/facilitator/index.js +331 -0
- package/dist/exact-direct/facilitator/index.js.map +1 -0
- package/dist/exact-direct/facilitator/index.mjs +303 -0
- package/dist/exact-direct/facilitator/index.mjs.map +1 -0
- package/dist/exact-direct/server/index.d.cts +109 -0
- package/dist/exact-direct/server/index.d.ts +109 -0
- package/dist/exact-direct/server/index.js +226 -0
- package/dist/exact-direct/server/index.js.map +1 -0
- package/dist/exact-direct/server/index.mjs +198 -0
- package/dist/exact-direct/server/index.mjs.map +1 -0
- package/dist/index.d.cts +124 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +228 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +170 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-DQMtUOa_.d.cts +125 -0
- package/dist/types-DQMtUOa_.d.ts +125 -0
- package/package.json +100 -0
- package/src/constants.ts +53 -0
- package/src/exact-direct/client/index.ts +13 -0
- package/src/exact-direct/client/register.ts +71 -0
- package/src/exact-direct/client/scheme.ts +177 -0
- package/src/exact-direct/facilitator/index.ts +13 -0
- package/src/exact-direct/facilitator/register.ts +74 -0
- package/src/exact-direct/facilitator/scheme.ts +311 -0
- package/src/exact-direct/server/index.ts +13 -0
- package/src/exact-direct/server/register.ts +64 -0
- package/src/exact-direct/server/scheme.ts +205 -0
- package/src/index.ts +32 -0
- package/src/tokens.ts +86 -0
- package/src/types.ts +160 -0
- package/src/utils.ts +128 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tezos Exact-Direct Facilitator Scheme
|
|
3
|
+
*
|
|
4
|
+
* Verifies FA2 transfer operations and manages replay protection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
SchemeNetworkFacilitator,
|
|
9
|
+
PaymentPayload,
|
|
10
|
+
PaymentRequirements,
|
|
11
|
+
VerifyResponse,
|
|
12
|
+
SettleResponse,
|
|
13
|
+
Network,
|
|
14
|
+
} from "@t402/core/types";
|
|
15
|
+
import { SCHEME_EXACT_DIRECT, TEZOS_CAIP2_NAMESPACE } from "../../constants.js";
|
|
16
|
+
import type {
|
|
17
|
+
FacilitatorTezosSigner,
|
|
18
|
+
ExactDirectTezosPayload,
|
|
19
|
+
} from "../../types.js";
|
|
20
|
+
import { isValidOperationHash, isTezosNetwork } from "../../types.js";
|
|
21
|
+
import {
|
|
22
|
+
compareAddresses,
|
|
23
|
+
extractFA2TransferDetails,
|
|
24
|
+
} from "../../utils.js";
|
|
25
|
+
import { getDefaultToken } from "../../tokens.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configuration for ExactDirectTezosFacilitator
|
|
29
|
+
*/
|
|
30
|
+
export interface ExactDirectTezosFacilitatorConfig {
|
|
31
|
+
/**
|
|
32
|
+
* Maximum age of operation in seconds (default: 3600 = 1 hour)
|
|
33
|
+
*/
|
|
34
|
+
maxOperationAge?: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Duration to cache used operation hashes (in milliseconds)
|
|
38
|
+
*/
|
|
39
|
+
usedOpCacheDuration?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Tezos Exact-Direct Facilitator
|
|
44
|
+
*
|
|
45
|
+
* Implements the facilitator-side verification and settlement.
|
|
46
|
+
* For exact-direct, settlement is a no-op since client already executed.
|
|
47
|
+
*/
|
|
48
|
+
export class ExactDirectTezosFacilitator implements SchemeNetworkFacilitator {
|
|
49
|
+
readonly scheme = SCHEME_EXACT_DIRECT;
|
|
50
|
+
readonly caipFamily = `${TEZOS_CAIP2_NAMESPACE}:*`;
|
|
51
|
+
|
|
52
|
+
private readonly config: Required<ExactDirectTezosFacilitatorConfig>;
|
|
53
|
+
private usedOps: Map<string, number> = new Map();
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private readonly signer: FacilitatorTezosSigner,
|
|
57
|
+
config?: ExactDirectTezosFacilitatorConfig,
|
|
58
|
+
) {
|
|
59
|
+
this.config = {
|
|
60
|
+
maxOperationAge: config?.maxOperationAge ?? 3600,
|
|
61
|
+
usedOpCacheDuration: config?.usedOpCacheDuration ?? 24 * 60 * 60 * 1000, // 24 hours
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Start cleanup interval
|
|
65
|
+
this.startCleanupInterval();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get extra data for a supported kind
|
|
70
|
+
*/
|
|
71
|
+
getExtra(network: Network): Record<string, unknown> | undefined {
|
|
72
|
+
const token = getDefaultToken(network);
|
|
73
|
+
if (!token) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
assetSymbol: token.symbol,
|
|
78
|
+
assetDecimals: token.decimals,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get facilitator signer addresses for a network
|
|
84
|
+
*/
|
|
85
|
+
getSigners(network: Network): string[] {
|
|
86
|
+
return this.signer.getAddresses(network);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Verify a payment payload
|
|
91
|
+
*/
|
|
92
|
+
async verify(
|
|
93
|
+
payload: PaymentPayload,
|
|
94
|
+
requirements: PaymentRequirements,
|
|
95
|
+
): Promise<VerifyResponse> {
|
|
96
|
+
// Validate scheme
|
|
97
|
+
if (payload.accepted.scheme !== SCHEME_EXACT_DIRECT) {
|
|
98
|
+
return {
|
|
99
|
+
isValid: false,
|
|
100
|
+
invalidReason: "invalid_scheme",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate network
|
|
105
|
+
if (!isTezosNetwork(payload.accepted.network)) {
|
|
106
|
+
return {
|
|
107
|
+
isValid: false,
|
|
108
|
+
invalidReason: "invalid_network",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract Tezos-specific payload
|
|
113
|
+
const tezosPayload = payload.payload as ExactDirectTezosPayload;
|
|
114
|
+
|
|
115
|
+
// Validate operation hash format
|
|
116
|
+
if (!isValidOperationHash(tezosPayload.opHash)) {
|
|
117
|
+
return {
|
|
118
|
+
isValid: false,
|
|
119
|
+
invalidReason: "invalid_operation_hash_format",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for replay attack
|
|
124
|
+
if (this.isOpUsed(tezosPayload.opHash)) {
|
|
125
|
+
return {
|
|
126
|
+
isValid: false,
|
|
127
|
+
invalidReason: "operation_already_used",
|
|
128
|
+
payer: tezosPayload.from,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// Query operation
|
|
134
|
+
const op = await this.signer.queryOperation(tezosPayload.opHash);
|
|
135
|
+
if (!op) {
|
|
136
|
+
return {
|
|
137
|
+
isValid: false,
|
|
138
|
+
invalidReason: "operation_not_found",
|
|
139
|
+
payer: tezosPayload.from,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Verify operation was successful
|
|
144
|
+
if (op.status !== "applied") {
|
|
145
|
+
return {
|
|
146
|
+
isValid: false,
|
|
147
|
+
invalidReason: `operation_not_applied: status is ${op.status}`,
|
|
148
|
+
payer: tezosPayload.from,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check operation age
|
|
153
|
+
if (this.config.maxOperationAge > 0) {
|
|
154
|
+
const opTimestamp = new Date(op.timestamp).getTime() / 1000;
|
|
155
|
+
const now = Date.now() / 1000;
|
|
156
|
+
const age = now - opTimestamp;
|
|
157
|
+
if (age > this.config.maxOperationAge) {
|
|
158
|
+
return {
|
|
159
|
+
isValid: false,
|
|
160
|
+
invalidReason: `operation_too_old: ${Math.round(age)} seconds`,
|
|
161
|
+
payer: tezosPayload.from,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Verify it's a transfer to the correct contract
|
|
167
|
+
if (op.target?.address !== tezosPayload.contractAddress) {
|
|
168
|
+
return {
|
|
169
|
+
isValid: false,
|
|
170
|
+
invalidReason: `contract_mismatch: expected ${tezosPayload.contractAddress}, got ${op.target?.address}`,
|
|
171
|
+
payer: tezosPayload.from,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Verify entrypoint
|
|
176
|
+
if (op.entrypoint !== "transfer") {
|
|
177
|
+
return {
|
|
178
|
+
isValid: false,
|
|
179
|
+
invalidReason: `entrypoint_mismatch: expected transfer, got ${op.entrypoint}`,
|
|
180
|
+
payer: tezosPayload.from,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Extract transfer details from parameter
|
|
185
|
+
const transferDetails = extractFA2TransferDetails(op.parameter);
|
|
186
|
+
if (!transferDetails) {
|
|
187
|
+
return {
|
|
188
|
+
isValid: false,
|
|
189
|
+
invalidReason: "could_not_extract_transfer_details",
|
|
190
|
+
payer: tezosPayload.from,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Verify sender
|
|
195
|
+
if (!compareAddresses(transferDetails.from, op.sender.address)) {
|
|
196
|
+
return {
|
|
197
|
+
isValid: false,
|
|
198
|
+
invalidReason: `sender_mismatch: parameter says ${transferDetails.from}, but sender is ${op.sender.address}`,
|
|
199
|
+
payer: tezosPayload.from,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Verify recipient
|
|
204
|
+
if (!compareAddresses(transferDetails.to, requirements.payTo)) {
|
|
205
|
+
return {
|
|
206
|
+
isValid: false,
|
|
207
|
+
invalidReason: `recipient_mismatch: expected ${requirements.payTo}, got ${transferDetails.to}`,
|
|
208
|
+
payer: tezosPayload.from,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Verify token ID
|
|
213
|
+
if (transferDetails.tokenId !== tezosPayload.tokenId) {
|
|
214
|
+
return {
|
|
215
|
+
isValid: false,
|
|
216
|
+
invalidReason: `token_id_mismatch: expected ${tezosPayload.tokenId}, got ${transferDetails.tokenId}`,
|
|
217
|
+
payer: tezosPayload.from,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Verify amount
|
|
222
|
+
const expectedAmount = BigInt(requirements.amount);
|
|
223
|
+
const actualAmount = BigInt(transferDetails.amount);
|
|
224
|
+
if (actualAmount < expectedAmount) {
|
|
225
|
+
return {
|
|
226
|
+
isValid: false,
|
|
227
|
+
invalidReason: `insufficient_amount: expected ${expectedAmount}, got ${actualAmount}`,
|
|
228
|
+
payer: tezosPayload.from,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Mark operation as used
|
|
233
|
+
this.markOpUsed(tezosPayload.opHash);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
isValid: true,
|
|
237
|
+
payer: transferDetails.from,
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
isValid: false,
|
|
242
|
+
invalidReason: `verification_error: ${error instanceof Error ? error.message : String(error)}`,
|
|
243
|
+
payer: tezosPayload.from,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Settle a payment (no-op for exact-direct since client already executed)
|
|
250
|
+
*/
|
|
251
|
+
async settle(
|
|
252
|
+
payload: PaymentPayload,
|
|
253
|
+
requirements: PaymentRequirements,
|
|
254
|
+
): Promise<SettleResponse> {
|
|
255
|
+
// Verify first
|
|
256
|
+
const verifyResult = await this.verify(payload, requirements);
|
|
257
|
+
|
|
258
|
+
if (!verifyResult.isValid) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
errorReason: verifyResult.invalidReason || "verification_failed",
|
|
262
|
+
payer: verifyResult.payer,
|
|
263
|
+
transaction: "",
|
|
264
|
+
network: requirements.network,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const tezosPayload = payload.payload as ExactDirectTezosPayload;
|
|
269
|
+
|
|
270
|
+
// For exact-direct, settlement is already complete
|
|
271
|
+
return {
|
|
272
|
+
success: true,
|
|
273
|
+
transaction: tezosPayload.opHash,
|
|
274
|
+
network: requirements.network,
|
|
275
|
+
payer: tezosPayload.from,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if an operation has been used
|
|
281
|
+
*/
|
|
282
|
+
private isOpUsed(opHash: string): boolean {
|
|
283
|
+
return this.usedOps.has(opHash);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Mark an operation as used
|
|
288
|
+
*/
|
|
289
|
+
private markOpUsed(opHash: string): void {
|
|
290
|
+
this.usedOps.set(opHash, Date.now());
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Start the cleanup interval for used operations
|
|
295
|
+
*/
|
|
296
|
+
private startCleanupInterval(): void {
|
|
297
|
+
setInterval(
|
|
298
|
+
() => {
|
|
299
|
+
const cutoff = Date.now() - this.config.usedOpCacheDuration;
|
|
300
|
+
for (const [opHash, usedAt] of this.usedOps.entries()) {
|
|
301
|
+
if (usedAt < cutoff) {
|
|
302
|
+
this.usedOps.delete(opHash);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
60 * 60 * 1000,
|
|
307
|
+
); // Cleanup every hour
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export default ExactDirectTezosFacilitator;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration function for Tezos Exact-Direct server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { t402ResourceServer } from "@t402/core/server";
|
|
6
|
+
import type { Network } from "@t402/core/types";
|
|
7
|
+
import {
|
|
8
|
+
ExactDirectTezosServer,
|
|
9
|
+
type ExactDirectTezosServerConfig,
|
|
10
|
+
} from "./scheme.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration options for registering Tezos schemes to a t402ResourceServer
|
|
14
|
+
*/
|
|
15
|
+
export interface TezosServerConfig {
|
|
16
|
+
/**
|
|
17
|
+
* Optional specific networks to register
|
|
18
|
+
* If not provided, registers wildcard support (tezos:*)
|
|
19
|
+
*/
|
|
20
|
+
networks?: Network[];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional scheme configuration
|
|
24
|
+
*/
|
|
25
|
+
schemeConfig?: ExactDirectTezosServerConfig;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Registers Tezos exact-direct payment scheme to a t402ResourceServer instance.
|
|
30
|
+
*
|
|
31
|
+
* @param server - The t402ResourceServer instance to register schemes to
|
|
32
|
+
* @param config - Configuration for Tezos server registration
|
|
33
|
+
* @returns The server instance for chaining
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { registerExactDirectTezosServer } from "@t402/tezos/exact-direct/server";
|
|
38
|
+
* import { t402ResourceServer } from "@t402/core/server";
|
|
39
|
+
*
|
|
40
|
+
* const server = new t402ResourceServer();
|
|
41
|
+
* registerExactDirectTezosServer(server, {
|
|
42
|
+
* networks: ["tezos:NetXdQprcVkpaWU"]
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function registerExactDirectTezosServer(
|
|
47
|
+
server: t402ResourceServer,
|
|
48
|
+
config: TezosServerConfig = {},
|
|
49
|
+
): t402ResourceServer {
|
|
50
|
+
const scheme = new ExactDirectTezosServer(config.schemeConfig);
|
|
51
|
+
|
|
52
|
+
// Register scheme
|
|
53
|
+
if (config.networks && config.networks.length > 0) {
|
|
54
|
+
// Register specific networks
|
|
55
|
+
config.networks.forEach((network) => {
|
|
56
|
+
server.register(network, scheme);
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
// Register wildcard for all Tezos networks
|
|
60
|
+
server.register("tezos:*", scheme);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return server;
|
|
64
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tezos Exact-Direct Server Scheme
|
|
3
|
+
*
|
|
4
|
+
* Handles price parsing and payment requirement enhancement for
|
|
5
|
+
* Tezos FA2 payments using the exact-direct scheme.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
SchemeNetworkServer,
|
|
10
|
+
PaymentRequirements,
|
|
11
|
+
Price,
|
|
12
|
+
AssetAmount,
|
|
13
|
+
Network,
|
|
14
|
+
MoneyParser,
|
|
15
|
+
} from "@t402/core/types";
|
|
16
|
+
import { SCHEME_EXACT_DIRECT } from "../../constants.js";
|
|
17
|
+
import { getTokenBySymbol, getDefaultToken, TOKEN_REGISTRY } from "../../tokens.js";
|
|
18
|
+
import { parseAmount } from "../../utils.js";
|
|
19
|
+
import { isTezosNetwork } from "../../types.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for ExactDirectTezosServer
|
|
23
|
+
*/
|
|
24
|
+
export interface ExactDirectTezosServerConfig {
|
|
25
|
+
/** Preferred token symbol (e.g., "USDt"). Defaults to network's default token. */
|
|
26
|
+
preferredToken?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tezos Exact-Direct Server
|
|
31
|
+
*
|
|
32
|
+
* Implements the server-side price parsing and payment requirements enhancement.
|
|
33
|
+
*/
|
|
34
|
+
export class ExactDirectTezosServer implements SchemeNetworkServer {
|
|
35
|
+
readonly scheme = SCHEME_EXACT_DIRECT;
|
|
36
|
+
private moneyParsers: MoneyParser[] = [];
|
|
37
|
+
private config: ExactDirectTezosServerConfig;
|
|
38
|
+
|
|
39
|
+
constructor(config: ExactDirectTezosServerConfig = {}) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a custom money parser in the parser chain.
|
|
45
|
+
*/
|
|
46
|
+
registerMoneyParser(parser: MoneyParser): ExactDirectTezosServer {
|
|
47
|
+
this.moneyParsers.push(parser);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse price into Tezos-specific amount
|
|
53
|
+
*/
|
|
54
|
+
async parsePrice(price: Price, network: Network): Promise<AssetAmount> {
|
|
55
|
+
// Validate network
|
|
56
|
+
if (!isTezosNetwork(network)) {
|
|
57
|
+
throw new Error(`Invalid Tezos network: ${network}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// If already an AssetAmount, return it directly
|
|
61
|
+
if (typeof price === "object" && price !== null && "amount" in price) {
|
|
62
|
+
if (!price.asset) {
|
|
63
|
+
throw new Error(`Asset must be specified for AssetAmount on network ${network}`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
amount: price.amount,
|
|
67
|
+
asset: price.asset,
|
|
68
|
+
extra: price.extra || {},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Parse Money to decimal number
|
|
73
|
+
const amount = this.parseMoneyToDecimal(price);
|
|
74
|
+
|
|
75
|
+
// Try each custom money parser in order
|
|
76
|
+
for (const parser of this.moneyParsers) {
|
|
77
|
+
const result = await parser(amount, network);
|
|
78
|
+
if (result !== null) {
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// All custom parsers returned null, use default conversion
|
|
84
|
+
return this.defaultMoneyConversion(amount, network);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Enhance payment requirements with Tezos-specific details
|
|
89
|
+
*/
|
|
90
|
+
async enhancePaymentRequirements(
|
|
91
|
+
paymentRequirements: PaymentRequirements,
|
|
92
|
+
supportedKind: {
|
|
93
|
+
t402Version: number;
|
|
94
|
+
scheme: string;
|
|
95
|
+
network: Network;
|
|
96
|
+
extra?: Record<string, unknown>;
|
|
97
|
+
},
|
|
98
|
+
facilitatorExtensions: string[],
|
|
99
|
+
): Promise<PaymentRequirements> {
|
|
100
|
+
// Mark unused parameters
|
|
101
|
+
void facilitatorExtensions;
|
|
102
|
+
|
|
103
|
+
// Start with existing extra fields
|
|
104
|
+
const extra = { ...paymentRequirements.extra };
|
|
105
|
+
|
|
106
|
+
// Add any facilitator-provided extra fields
|
|
107
|
+
if (supportedKind.extra?.assetSymbol) {
|
|
108
|
+
extra.assetSymbol = supportedKind.extra.assetSymbol;
|
|
109
|
+
}
|
|
110
|
+
if (supportedKind.extra?.assetDecimals) {
|
|
111
|
+
extra.assetDecimals = supportedKind.extra.assetDecimals;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
...paymentRequirements,
|
|
116
|
+
extra,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse Money (string | number) to a decimal number.
|
|
122
|
+
*/
|
|
123
|
+
private parseMoneyToDecimal(money: string | number): number {
|
|
124
|
+
if (typeof money === "number") {
|
|
125
|
+
return money;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Remove $ sign and whitespace, then parse
|
|
129
|
+
const cleanMoney = money.replace(/^\$/, "").trim();
|
|
130
|
+
const amount = parseFloat(cleanMoney);
|
|
131
|
+
|
|
132
|
+
if (isNaN(amount)) {
|
|
133
|
+
throw new Error(`Invalid money format: ${money}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return amount;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Default money conversion implementation.
|
|
141
|
+
*/
|
|
142
|
+
private defaultMoneyConversion(amount: number, network: Network): AssetAmount {
|
|
143
|
+
const token = this.getDefaultAsset(network);
|
|
144
|
+
|
|
145
|
+
// Convert decimal amount to token amount
|
|
146
|
+
const tokenAmount = parseAmount(amount.toString(), token.decimals);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
amount: tokenAmount.toString(),
|
|
150
|
+
asset: this.createAssetIdentifier(network, token.contractAddress, token.tokenId),
|
|
151
|
+
extra: {
|
|
152
|
+
symbol: token.symbol,
|
|
153
|
+
name: token.name,
|
|
154
|
+
decimals: token.decimals,
|
|
155
|
+
tokenId: token.tokenId,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create a CAIP-19 asset identifier for Tezos FA2
|
|
162
|
+
*/
|
|
163
|
+
private createAssetIdentifier(
|
|
164
|
+
network: Network,
|
|
165
|
+
contractAddress: string,
|
|
166
|
+
tokenId: number,
|
|
167
|
+
): string {
|
|
168
|
+
return `${network}/fa2:${contractAddress}/${tokenId}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the default asset info for a network.
|
|
173
|
+
*/
|
|
174
|
+
private getDefaultAsset(
|
|
175
|
+
network: Network,
|
|
176
|
+
): { contractAddress: string; tokenId: number; symbol: string; name: string; decimals: number } {
|
|
177
|
+
// If a preferred token is configured, try to use it
|
|
178
|
+
if (this.config.preferredToken) {
|
|
179
|
+
const preferred = getTokenBySymbol(network, this.config.preferredToken);
|
|
180
|
+
if (preferred) return preferred;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Use the network's default token
|
|
184
|
+
const defaultToken = getDefaultToken(network);
|
|
185
|
+
if (defaultToken) return defaultToken;
|
|
186
|
+
|
|
187
|
+
throw new Error(`No tokens configured for network ${network}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get all supported networks
|
|
192
|
+
*/
|
|
193
|
+
static getSupportedNetworks(): string[] {
|
|
194
|
+
return Object.keys(TOKEN_REGISTRY);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if a network is supported
|
|
199
|
+
*/
|
|
200
|
+
static isNetworkSupported(network: string): boolean {
|
|
201
|
+
return network in TOKEN_REGISTRY;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default ExactDirectTezosServer;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @t402/tezos - Tezos (FA2) mechanism for T402 payment protocol
|
|
3
|
+
*
|
|
4
|
+
* This package provides client, server, and facilitator implementations
|
|
5
|
+
* for processing USDT payments on Tezos using the FA2 token standard (TZIP-12).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // Client usage
|
|
10
|
+
* import { registerExactDirectTezosClient } from "@t402/tezos/exact-direct/client";
|
|
11
|
+
*
|
|
12
|
+
* // Server usage
|
|
13
|
+
* import { registerExactDirectTezosServer } from "@t402/tezos/exact-direct/server";
|
|
14
|
+
*
|
|
15
|
+
* // Facilitator usage
|
|
16
|
+
* import { registerExactDirectTezosFacilitator } from "@t402/tezos/exact-direct/facilitator";
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @packageDocumentation
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Constants
|
|
23
|
+
export * from "./constants.js";
|
|
24
|
+
|
|
25
|
+
// Types
|
|
26
|
+
export * from "./types.js";
|
|
27
|
+
|
|
28
|
+
// Tokens
|
|
29
|
+
export * from "./tokens.js";
|
|
30
|
+
|
|
31
|
+
// Utilities
|
|
32
|
+
export * from "./utils.js";
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tezos token registry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TEZOS_MAINNET_CAIP2, TEZOS_GHOSTNET_CAIP2 } from "./constants.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Token configuration for Tezos FA2 tokens
|
|
9
|
+
*/
|
|
10
|
+
export interface TokenConfig {
|
|
11
|
+
/** FA2 contract address (KT1...) */
|
|
12
|
+
contractAddress: string;
|
|
13
|
+
/** Token ID within the FA2 contract */
|
|
14
|
+
tokenId: number;
|
|
15
|
+
/** Token symbol */
|
|
16
|
+
symbol: string;
|
|
17
|
+
/** Token name */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Token decimals */
|
|
20
|
+
decimals: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* USDT on Tezos Mainnet
|
|
25
|
+
*/
|
|
26
|
+
export const USDT_MAINNET: TokenConfig = {
|
|
27
|
+
contractAddress: "KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o",
|
|
28
|
+
tokenId: 0,
|
|
29
|
+
symbol: "USDt",
|
|
30
|
+
name: "Tether USD",
|
|
31
|
+
decimals: 6,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Token registry by network
|
|
36
|
+
*/
|
|
37
|
+
export const TOKEN_REGISTRY: Record<string, TokenConfig[]> = {
|
|
38
|
+
[TEZOS_MAINNET_CAIP2]: [USDT_MAINNET],
|
|
39
|
+
[TEZOS_GHOSTNET_CAIP2]: [],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Default token for each network
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_TOKENS: Record<string, TokenConfig | undefined> = {
|
|
46
|
+
[TEZOS_MAINNET_CAIP2]: USDT_MAINNET,
|
|
47
|
+
[TEZOS_GHOSTNET_CAIP2]: undefined,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get token by symbol for a network
|
|
52
|
+
*/
|
|
53
|
+
export function getTokenBySymbol(
|
|
54
|
+
network: string,
|
|
55
|
+
symbol: string,
|
|
56
|
+
): TokenConfig | undefined {
|
|
57
|
+
const tokens = TOKEN_REGISTRY[network];
|
|
58
|
+
if (!tokens) return undefined;
|
|
59
|
+
return tokens.find(
|
|
60
|
+
(t) => t.symbol.toLowerCase() === symbol.toLowerCase(),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get token by contract address and token ID
|
|
66
|
+
*/
|
|
67
|
+
export function getTokenByContract(
|
|
68
|
+
network: string,
|
|
69
|
+
contractAddress: string,
|
|
70
|
+
tokenId: number,
|
|
71
|
+
): TokenConfig | undefined {
|
|
72
|
+
const tokens = TOKEN_REGISTRY[network];
|
|
73
|
+
if (!tokens) return undefined;
|
|
74
|
+
return tokens.find(
|
|
75
|
+
(t) =>
|
|
76
|
+
t.contractAddress.toLowerCase() === contractAddress.toLowerCase() &&
|
|
77
|
+
t.tokenId === tokenId,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get default token for a network
|
|
83
|
+
*/
|
|
84
|
+
export function getDefaultToken(network: string): TokenConfig | undefined {
|
|
85
|
+
return DEFAULT_TOKENS[network];
|
|
86
|
+
}
|