@t402/stacks 2.4.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.
Files changed (40) hide show
  1. package/README.md +178 -0
  2. package/dist/exact-direct/client/index.cjs +167 -0
  3. package/dist/exact-direct/client/index.cjs.map +1 -0
  4. package/dist/exact-direct/client/index.d.cts +39 -0
  5. package/dist/exact-direct/client/index.d.ts +39 -0
  6. package/dist/exact-direct/client/index.mjs +139 -0
  7. package/dist/exact-direct/client/index.mjs.map +1 -0
  8. package/dist/exact-direct/facilitator/index.cjs +395 -0
  9. package/dist/exact-direct/facilitator/index.cjs.map +1 -0
  10. package/dist/exact-direct/facilitator/index.d.cts +55 -0
  11. package/dist/exact-direct/facilitator/index.d.ts +55 -0
  12. package/dist/exact-direct/facilitator/index.mjs +367 -0
  13. package/dist/exact-direct/facilitator/index.mjs.map +1 -0
  14. package/dist/exact-direct/server/index.cjs +247 -0
  15. package/dist/exact-direct/server/index.cjs.map +1 -0
  16. package/dist/exact-direct/server/index.d.cts +109 -0
  17. package/dist/exact-direct/server/index.d.ts +109 -0
  18. package/dist/exact-direct/server/index.mjs +218 -0
  19. package/dist/exact-direct/server/index.mjs.map +1 -0
  20. package/dist/index.cjs +261 -0
  21. package/dist/index.cjs.map +1 -0
  22. package/dist/index.d.cts +126 -0
  23. package/dist/index.d.ts +126 -0
  24. package/dist/index.mjs +212 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/types-Bxzo3eQ1.d.cts +172 -0
  27. package/dist/types-Bxzo3eQ1.d.ts +172 -0
  28. package/package.json +102 -0
  29. package/src/constants.ts +66 -0
  30. package/src/exact-direct/client/index.ts +5 -0
  31. package/src/exact-direct/client/scheme.ts +115 -0
  32. package/src/exact-direct/facilitator/index.ts +4 -0
  33. package/src/exact-direct/facilitator/scheme.ts +308 -0
  34. package/src/exact-direct/server/index.ts +9 -0
  35. package/src/exact-direct/server/register.ts +57 -0
  36. package/src/exact-direct/server/scheme.ts +216 -0
  37. package/src/index.ts +78 -0
  38. package/src/tokens.ts +96 -0
  39. package/src/types.ts +184 -0
  40. package/src/utils.ts +198 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Stacks T402 Constants
3
+ *
4
+ * Stacks is a Bitcoin Layer 2 that brings smart contracts and DeFi
5
+ * to Bitcoin. SIP-010 is the fungible token standard on Stacks.
6
+ */
7
+
8
+ // CAIP-2 namespace for Stacks
9
+ export const STACKS_CAIP2_NAMESPACE = "stacks";
10
+
11
+ // CAIP-2 network identifiers
12
+ // Stacks Mainnet (chain ID: 1)
13
+ export const STACKS_MAINNET_CAIP2 = "stacks:1";
14
+
15
+ // Stacks Testnet (chain ID: 2147483648)
16
+ export const STACKS_TESTNET_CAIP2 = "stacks:2147483648";
17
+
18
+ // Scheme identifier
19
+ export const SCHEME_EXACT_DIRECT = "exact-direct";
20
+
21
+ // Default API endpoints (Hiro API)
22
+ export const DEFAULT_MAINNET_API = "https://api.mainnet.hiro.so";
23
+ export const DEFAULT_TESTNET_API = "https://api.testnet.hiro.so";
24
+
25
+ // Network configurations
26
+ export interface StacksNetworkConfig {
27
+ readonly name: string;
28
+ readonly caip2: string;
29
+ readonly apiUrl: string;
30
+ readonly chainId: number;
31
+ readonly addressPrefix: string;
32
+ readonly isTestnet: boolean;
33
+ }
34
+
35
+ export const STACKS_NETWORKS: Record<string, StacksNetworkConfig> = {
36
+ [STACKS_MAINNET_CAIP2]: {
37
+ name: "Stacks Mainnet",
38
+ caip2: STACKS_MAINNET_CAIP2,
39
+ apiUrl: DEFAULT_MAINNET_API,
40
+ chainId: 1,
41
+ addressPrefix: "SP",
42
+ isTestnet: false,
43
+ },
44
+ [STACKS_TESTNET_CAIP2]: {
45
+ name: "Stacks Testnet",
46
+ caip2: STACKS_TESTNET_CAIP2,
47
+ apiUrl: DEFAULT_TESTNET_API,
48
+ chainId: 2147483648,
49
+ addressPrefix: "ST",
50
+ isTestnet: true,
51
+ },
52
+ };
53
+
54
+ /**
55
+ * Get network configuration by CAIP-2 identifier
56
+ */
57
+ export function getNetworkConfig(network: string): StacksNetworkConfig | undefined {
58
+ return STACKS_NETWORKS[network];
59
+ }
60
+
61
+ /**
62
+ * Check if a network identifier is a Stacks network
63
+ */
64
+ export function isStacksNetwork(network: string): boolean {
65
+ return network.startsWith(`${STACKS_CAIP2_NAMESPACE}:`);
66
+ }
@@ -0,0 +1,5 @@
1
+ export {
2
+ ExactDirectStacksClient,
3
+ createExactDirectStacksClient,
4
+ type ExactDirectStacksClientConfig,
5
+ } from "./scheme.js";
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Stacks Exact-Direct Client Scheme
3
+ *
4
+ * In the exact-direct scheme, the client executes the SIP-010 token transfer
5
+ * directly and provides the transaction ID as proof of payment.
6
+ */
7
+
8
+ import type {
9
+ PaymentPayload,
10
+ PaymentRequirements,
11
+ SchemeNetworkClient,
12
+ } from "@t402/core/types";
13
+ import type { ClientStacksSigner, ExactDirectStacksPayload } from "../../types.js";
14
+ import { SCHEME_EXACT_DIRECT, STACKS_CAIP2_NAMESPACE } from "../../constants.js";
15
+ import { getContractAddress } from "../../tokens.js";
16
+ import { isValidPrincipal } from "../../utils.js";
17
+
18
+ /**
19
+ * Configuration for the exact-direct client
20
+ */
21
+ export interface ExactDirectStacksClientConfig {
22
+ /** Signer for executing transactions */
23
+ signer: ClientStacksSigner;
24
+ }
25
+
26
+ /**
27
+ * Exact-direct client scheme for Stacks
28
+ */
29
+ export class ExactDirectStacksClient implements SchemeNetworkClient {
30
+ readonly scheme = SCHEME_EXACT_DIRECT;
31
+ private readonly signer: ClientStacksSigner;
32
+
33
+ constructor(config: ExactDirectStacksClientConfig) {
34
+ this.signer = config.signer;
35
+ }
36
+
37
+ /**
38
+ * Create a payment payload by executing the transfer
39
+ */
40
+ async createPaymentPayload(
41
+ t402Version: number,
42
+ requirements: PaymentRequirements,
43
+ ): Promise<Pick<PaymentPayload, "t402Version" | "payload">> {
44
+ // Validate requirements
45
+ this.validateRequirements(requirements);
46
+
47
+ const { network, amount, payTo, extra } = requirements;
48
+
49
+ // Get contract address from extra or use default sUSDC
50
+ const symbol = (extra?.assetSymbol as string) || "sUSDC";
51
+ const contractAddress =
52
+ (extra?.contractAddress as string) ?? getContractAddress(network, symbol);
53
+
54
+ if (!contractAddress) {
55
+ throw new Error(`Unknown asset ${symbol} on network ${network}`);
56
+ }
57
+
58
+ // Get sender address
59
+ const from = await this.signer.getAddress();
60
+
61
+ // Execute the transfer
62
+ const { txId } = await this.signer.transferToken(contractAddress, payTo, amount);
63
+
64
+ // Build the payload
65
+ const stacksPayload: ExactDirectStacksPayload = {
66
+ txId,
67
+ from,
68
+ to: payTo,
69
+ amount,
70
+ contractAddress,
71
+ };
72
+
73
+ return {
74
+ t402Version,
75
+ payload: stacksPayload,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Validate payment requirements
81
+ */
82
+ private validateRequirements(requirements: PaymentRequirements): void {
83
+ // Check scheme
84
+ if (requirements.scheme !== SCHEME_EXACT_DIRECT) {
85
+ throw new Error(
86
+ `Invalid scheme: expected ${SCHEME_EXACT_DIRECT}, got ${requirements.scheme}`,
87
+ );
88
+ }
89
+
90
+ // Check network
91
+ if (!requirements.network.startsWith(`${STACKS_CAIP2_NAMESPACE}:`)) {
92
+ throw new Error(`Invalid network: ${requirements.network}`);
93
+ }
94
+
95
+ // Check payTo address
96
+ if (!isValidPrincipal(requirements.payTo)) {
97
+ throw new Error(`Invalid payTo address: ${requirements.payTo}`);
98
+ }
99
+
100
+ // Check amount
101
+ const amount = BigInt(requirements.amount);
102
+ if (amount <= 0n) {
103
+ throw new Error(`Invalid amount: ${requirements.amount}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Create an exact-direct client for Stacks
110
+ */
111
+ export function createExactDirectStacksClient(
112
+ config: ExactDirectStacksClientConfig,
113
+ ): ExactDirectStacksClient {
114
+ return new ExactDirectStacksClient(config);
115
+ }
@@ -0,0 +1,4 @@
1
+ export {
2
+ ExactDirectStacksFacilitator,
3
+ createExactDirectStacksFacilitator,
4
+ } from "./scheme.js";
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Stacks Exact-Direct Facilitator Scheme
3
+ *
4
+ * Verifies that a Stacks SIP-010 token transfer was executed correctly
5
+ * by querying the Hiro API.
6
+ */
7
+
8
+ import type {
9
+ Network,
10
+ PaymentPayload,
11
+ PaymentRequirements,
12
+ SchemeNetworkFacilitator,
13
+ SettleResponse,
14
+ VerifyResponse,
15
+ } from "@t402/core/types";
16
+ import { STACKS_CAIP2_NAMESPACE, SCHEME_EXACT_DIRECT, getNetworkConfig } from "../../constants.js";
17
+ import { getDefaultToken } from "../../tokens.js";
18
+ import type {
19
+ ExactDirectStacksPayload,
20
+ FacilitatorStacksSigner,
21
+ StacksFacilitatorConfig,
22
+ } from "../../types.js";
23
+ import {
24
+ comparePrincipals,
25
+ extractTokenTransfer,
26
+ extractTokenTransferFromPostConditions,
27
+ isValidTxId,
28
+ isValidPrincipal,
29
+ } from "../../utils.js";
30
+
31
+ // Default configuration
32
+ const DEFAULT_MAX_TRANSACTION_AGE = 3600; // 1 hour
33
+ const DEFAULT_CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours in ms
34
+
35
+ /**
36
+ * Exact-direct facilitator scheme for Stacks
37
+ */
38
+ export class ExactDirectStacksFacilitator implements SchemeNetworkFacilitator {
39
+ readonly scheme = SCHEME_EXACT_DIRECT;
40
+ readonly caipFamily = `${STACKS_CAIP2_NAMESPACE}:*`;
41
+
42
+ private readonly signer: FacilitatorStacksSigner;
43
+ private readonly config: Required<StacksFacilitatorConfig>;
44
+ private readonly usedTransactions = new Map<string, number>();
45
+
46
+ constructor(
47
+ signer: FacilitatorStacksSigner,
48
+ config: StacksFacilitatorConfig = {},
49
+ ) {
50
+ this.signer = signer;
51
+ this.config = {
52
+ maxTransactionAge: config.maxTransactionAge ?? DEFAULT_MAX_TRANSACTION_AGE,
53
+ usedTxCacheDuration:
54
+ config.usedTxCacheDuration ?? DEFAULT_CACHE_DURATION,
55
+ };
56
+
57
+ // Start cleanup interval
58
+ this.startCleanupInterval();
59
+ }
60
+
61
+ /**
62
+ * Get extra data for payment requirements
63
+ */
64
+ getExtra(network: Network): Record<string, unknown> | undefined {
65
+ const config = getNetworkConfig(network);
66
+ if (!config) return undefined;
67
+
68
+ const token = getDefaultToken(network);
69
+ return {
70
+ contractAddress: token?.contractAddress,
71
+ assetSymbol: token?.symbol,
72
+ assetDecimals: token?.decimals,
73
+ networkName: config.name,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Get facilitator signer addresses for a network
79
+ */
80
+ getSigners(network: Network): string[] {
81
+ return this.signer.getAddresses(network);
82
+ }
83
+
84
+ /**
85
+ * Verify a payment payload
86
+ */
87
+ async verify(
88
+ payload: PaymentPayload,
89
+ requirements: PaymentRequirements,
90
+ ): Promise<VerifyResponse> {
91
+ const network = requirements.network;
92
+
93
+ // Validate scheme
94
+ if (payload.accepted.scheme !== SCHEME_EXACT_DIRECT) {
95
+ return {
96
+ isValid: false,
97
+ invalidReason: `invalid_scheme: expected ${SCHEME_EXACT_DIRECT}, got ${payload.accepted.scheme}`,
98
+ };
99
+ }
100
+
101
+ // Validate network
102
+ if (payload.accepted.network !== network) {
103
+ return {
104
+ isValid: false,
105
+ invalidReason: `network_mismatch: expected ${network}, got ${payload.accepted.network}`,
106
+ };
107
+ }
108
+
109
+ // Parse payload
110
+ const stacksPayload = payload.payload as unknown as ExactDirectStacksPayload;
111
+
112
+ // Validate required fields
113
+ if (!stacksPayload.txId) {
114
+ return {
115
+ isValid: false,
116
+ invalidReason: "missing_tx_id",
117
+ };
118
+ }
119
+
120
+ // Validate tx ID format
121
+ if (!isValidTxId(stacksPayload.txId)) {
122
+ return {
123
+ isValid: false,
124
+ invalidReason: "invalid_tx_id_format",
125
+ };
126
+ }
127
+
128
+ // Validate from address
129
+ if (!stacksPayload.from) {
130
+ return {
131
+ isValid: false,
132
+ invalidReason: "missing_from_address",
133
+ };
134
+ }
135
+
136
+ if (!isValidPrincipal(stacksPayload.from)) {
137
+ return {
138
+ isValid: false,
139
+ invalidReason: "invalid_from_address",
140
+ payer: stacksPayload.from,
141
+ };
142
+ }
143
+
144
+ // Check for replay attack
145
+ if (this.isTransactionUsed(stacksPayload.txId)) {
146
+ return {
147
+ isValid: false,
148
+ invalidReason: "transaction_already_used",
149
+ payer: stacksPayload.from,
150
+ };
151
+ }
152
+
153
+ // Query transaction
154
+ const txResult = await this.signer.queryTransaction(stacksPayload.txId);
155
+
156
+ if (!txResult) {
157
+ return {
158
+ isValid: false,
159
+ invalidReason: "transaction_not_found",
160
+ payer: stacksPayload.from,
161
+ };
162
+ }
163
+
164
+ // Verify transaction was successful
165
+ if (txResult.txStatus !== "success") {
166
+ return {
167
+ isValid: false,
168
+ invalidReason: `transaction_failed: status=${txResult.txStatus}`,
169
+ payer: stacksPayload.from,
170
+ };
171
+ }
172
+
173
+ // Check transaction age
174
+ if (this.config.maxTransactionAge > 0) {
175
+ const txTime = txResult.burnBlockTime * 1000; // Convert to milliseconds
176
+ const age = (Date.now() - txTime) / 1000;
177
+ if (age > this.config.maxTransactionAge) {
178
+ return {
179
+ isValid: false,
180
+ invalidReason: `transaction_too_old: ${Math.round(age)} seconds`,
181
+ payer: stacksPayload.from,
182
+ };
183
+ }
184
+ }
185
+
186
+ // Extract transfer details
187
+ const expectedContract = (requirements.extra?.contractAddress as string) ??
188
+ stacksPayload.contractAddress;
189
+
190
+ const transfer =
191
+ extractTokenTransfer(txResult, expectedContract) ||
192
+ extractTokenTransferFromPostConditions(txResult, expectedContract);
193
+
194
+ if (!transfer) {
195
+ return {
196
+ isValid: false,
197
+ invalidReason: "not_token_transfer",
198
+ payer: stacksPayload.from,
199
+ };
200
+ }
201
+
202
+ // Verify contract address
203
+ if (expectedContract && !comparePrincipals(transfer.contractAddress, expectedContract)) {
204
+ return {
205
+ isValid: false,
206
+ invalidReason: `contract_mismatch: expected ${expectedContract}, got ${transfer.contractAddress}`,
207
+ payer: stacksPayload.from,
208
+ };
209
+ }
210
+
211
+ // Verify recipient
212
+ if (!comparePrincipals(transfer.to, requirements.payTo)) {
213
+ return {
214
+ isValid: false,
215
+ invalidReason: `recipient_mismatch: expected ${requirements.payTo}, got ${transfer.to}`,
216
+ payer: stacksPayload.from,
217
+ };
218
+ }
219
+
220
+ // Verify amount
221
+ const txAmount = BigInt(transfer.amount);
222
+ const requiredAmount = BigInt(requirements.amount);
223
+ if (txAmount < requiredAmount) {
224
+ return {
225
+ isValid: false,
226
+ invalidReason: `insufficient_amount: expected ${requirements.amount}, got ${transfer.amount}`,
227
+ payer: stacksPayload.from,
228
+ };
229
+ }
230
+
231
+ // Mark transaction as used
232
+ this.markTransactionUsed(stacksPayload.txId);
233
+
234
+ return {
235
+ isValid: true,
236
+ payer: stacksPayload.from,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Settle a payment (for exact-direct, the transfer is already complete)
242
+ */
243
+ async settle(
244
+ payload: PaymentPayload,
245
+ requirements: PaymentRequirements,
246
+ ): Promise<SettleResponse> {
247
+ // Verify first
248
+ const verifyResult = await this.verify(payload, requirements);
249
+
250
+ if (!verifyResult.isValid) {
251
+ return {
252
+ success: false,
253
+ errorReason: verifyResult.invalidReason || "verification_failed",
254
+ payer: verifyResult.payer,
255
+ transaction: "",
256
+ network: requirements.network,
257
+ };
258
+ }
259
+
260
+ const stacksPayload = payload.payload as unknown as ExactDirectStacksPayload;
261
+
262
+ // For exact-direct, settlement is already complete
263
+ return {
264
+ success: true,
265
+ transaction: stacksPayload.txId,
266
+ network: requirements.network,
267
+ payer: verifyResult.payer,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * Check if a transaction has been used
273
+ */
274
+ private isTransactionUsed(txId: string): boolean {
275
+ return this.usedTransactions.has(txId);
276
+ }
277
+
278
+ /**
279
+ * Mark a transaction as used
280
+ */
281
+ private markTransactionUsed(txId: string): void {
282
+ this.usedTransactions.set(txId, Date.now());
283
+ }
284
+
285
+ /**
286
+ * Start the cleanup interval for used transactions cache
287
+ */
288
+ private startCleanupInterval(): void {
289
+ setInterval(() => {
290
+ const cutoff = Date.now() - this.config.usedTxCacheDuration;
291
+ for (const [txId, timestamp] of this.usedTransactions) {
292
+ if (timestamp < cutoff) {
293
+ this.usedTransactions.delete(txId);
294
+ }
295
+ }
296
+ }, 60 * 60 * 1000); // Run every hour
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Create an exact-direct facilitator for Stacks
302
+ */
303
+ export function createExactDirectStacksFacilitator(
304
+ signer: FacilitatorStacksSigner,
305
+ config: StacksFacilitatorConfig = {},
306
+ ): ExactDirectStacksFacilitator {
307
+ return new ExactDirectStacksFacilitator(signer, config);
308
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ ExactDirectStacksServer,
3
+ createExactDirectStacksServer,
4
+ } from "./scheme.js";
5
+
6
+ export {
7
+ registerExactDirectStacksServer,
8
+ type StacksServerRegistrationConfig,
9
+ } from "./register.js";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Registration function for Stacks exact-direct server
3
+ */
4
+
5
+ import { t402ResourceServer } from "@t402/core/server";
6
+ import type { Network } from "@t402/core/types";
7
+ import { STACKS_CAIP2_NAMESPACE } from "../../constants.js";
8
+ import { ExactDirectStacksServer, type ExactDirectStacksServerConfig } from "./scheme.js";
9
+
10
+ /**
11
+ * Configuration for registering Stacks server schemes
12
+ */
13
+ export interface StacksServerRegistrationConfig extends ExactDirectStacksServerConfig {
14
+ /**
15
+ * Optional specific networks to register
16
+ * If not provided, registers wildcard support (stacks:*)
17
+ */
18
+ networks?: Network[];
19
+ }
20
+
21
+ /**
22
+ * Registers Stacks exact-direct payment scheme to a t402ResourceServer instance.
23
+ *
24
+ * @param server - The t402ResourceServer instance to register schemes to
25
+ * @param config - Configuration for Stacks server registration
26
+ * @returns The server instance for chaining
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { registerExactDirectStacksServer } from "@t402/stacks/exact-direct/server";
31
+ * import { t402ResourceServer } from "@t402/core/server";
32
+ *
33
+ * const server = new t402ResourceServer();
34
+ * registerExactDirectStacksServer(server, {
35
+ * networks: ["stacks:1"]
36
+ * });
37
+ * ```
38
+ */
39
+ export function registerExactDirectStacksServer(
40
+ server: t402ResourceServer,
41
+ config: StacksServerRegistrationConfig = {},
42
+ ): t402ResourceServer {
43
+ const scheme = new ExactDirectStacksServer(config);
44
+
45
+ // Register scheme
46
+ if (config.networks && config.networks.length > 0) {
47
+ // Register specific networks
48
+ config.networks.forEach((network) => {
49
+ server.register(network, scheme);
50
+ });
51
+ } else {
52
+ // Register wildcard for all Stacks networks
53
+ server.register(`${STACKS_CAIP2_NAMESPACE}:*`, scheme);
54
+ }
55
+
56
+ return server;
57
+ }