@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.
- package/README.md +178 -0
- package/dist/exact-direct/client/index.cjs +167 -0
- package/dist/exact-direct/client/index.cjs.map +1 -0
- package/dist/exact-direct/client/index.d.cts +39 -0
- package/dist/exact-direct/client/index.d.ts +39 -0
- package/dist/exact-direct/client/index.mjs +139 -0
- package/dist/exact-direct/client/index.mjs.map +1 -0
- package/dist/exact-direct/facilitator/index.cjs +395 -0
- package/dist/exact-direct/facilitator/index.cjs.map +1 -0
- package/dist/exact-direct/facilitator/index.d.cts +55 -0
- package/dist/exact-direct/facilitator/index.d.ts +55 -0
- package/dist/exact-direct/facilitator/index.mjs +367 -0
- package/dist/exact-direct/facilitator/index.mjs.map +1 -0
- package/dist/exact-direct/server/index.cjs +247 -0
- package/dist/exact-direct/server/index.cjs.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.mjs +218 -0
- package/dist/exact-direct/server/index.mjs.map +1 -0
- package/dist/index.cjs +261 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +126 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.mjs +212 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-Bxzo3eQ1.d.cts +172 -0
- package/dist/types-Bxzo3eQ1.d.ts +172 -0
- package/package.json +102 -0
- package/src/constants.ts +66 -0
- package/src/exact-direct/client/index.ts +5 -0
- package/src/exact-direct/client/scheme.ts +115 -0
- package/src/exact-direct/facilitator/index.ts +4 -0
- package/src/exact-direct/facilitator/scheme.ts +308 -0
- package/src/exact-direct/server/index.ts +9 -0
- package/src/exact-direct/server/register.ts +57 -0
- package/src/exact-direct/server/scheme.ts +216 -0
- package/src/index.ts +78 -0
- package/src/tokens.ts +96 -0
- package/src/types.ts +184 -0
- package/src/utils.ts +198 -0
package/src/constants.ts
ADDED
|
@@ -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,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,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,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
|
+
}
|