@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,216 @@
1
+ /**
2
+ * Stacks Exact-Direct Server Scheme
3
+ *
4
+ * Handles price parsing and payment requirement enhancement for
5
+ * Stacks 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, isStacksNetwork } from "../../constants.js";
17
+ import { getDefaultToken, getTokenConfig, TOKEN_REGISTRY } from "../../tokens.js";
18
+ import { parseAmount } from "../../utils.js";
19
+
20
+ /**
21
+ * Configuration for ExactDirectStacksServer
22
+ */
23
+ export interface ExactDirectStacksServerConfig {
24
+ /** Preferred token symbol (e.g., "sUSDC"). Defaults to network's default token. */
25
+ preferredToken?: string;
26
+ }
27
+
28
+ /**
29
+ * Stacks Exact-Direct Server
30
+ *
31
+ * Implements the server-side price parsing and payment requirements enhancement.
32
+ */
33
+ export class ExactDirectStacksServer implements SchemeNetworkServer {
34
+ readonly scheme = SCHEME_EXACT_DIRECT;
35
+ private moneyParsers: MoneyParser[] = [];
36
+ private config: ExactDirectStacksServerConfig;
37
+
38
+ constructor(config: ExactDirectStacksServerConfig = {}) {
39
+ this.config = config;
40
+ }
41
+
42
+ /**
43
+ * Register a custom money parser in the parser chain.
44
+ */
45
+ registerMoneyParser(parser: MoneyParser): ExactDirectStacksServer {
46
+ this.moneyParsers.push(parser);
47
+ return this;
48
+ }
49
+
50
+ /**
51
+ * Parse price into Stacks-specific amount
52
+ */
53
+ async parsePrice(price: Price, network: Network): Promise<AssetAmount> {
54
+ // Validate network
55
+ if (!isStacksNetwork(network)) {
56
+ throw new Error(`Invalid Stacks network: ${network}`);
57
+ }
58
+
59
+ // If already an AssetAmount, return it directly
60
+ if (typeof price === "object" && price !== null && "amount" in price) {
61
+ if (!price.asset) {
62
+ throw new Error(`Asset must be specified for AssetAmount on network ${network}`);
63
+ }
64
+ return {
65
+ amount: price.amount,
66
+ asset: price.asset,
67
+ extra: price.extra || {},
68
+ };
69
+ }
70
+
71
+ // Parse Money to decimal number
72
+ const amount = this.parseMoneyToDecimal(price);
73
+
74
+ // Try each custom money parser in order
75
+ for (const parser of this.moneyParsers) {
76
+ const result = await parser(amount, network);
77
+ if (result !== null) {
78
+ return result;
79
+ }
80
+ }
81
+
82
+ // All custom parsers returned null, use default conversion
83
+ return this.defaultMoneyConversion(amount, network);
84
+ }
85
+
86
+ /**
87
+ * Enhance payment requirements with Stacks-specific details
88
+ */
89
+ async enhancePaymentRequirements(
90
+ paymentRequirements: PaymentRequirements,
91
+ supportedKind: {
92
+ t402Version: number;
93
+ scheme: string;
94
+ network: Network;
95
+ extra?: Record<string, unknown>;
96
+ },
97
+ facilitatorExtensions: string[],
98
+ ): Promise<PaymentRequirements> {
99
+ // Mark unused parameters
100
+ void facilitatorExtensions;
101
+
102
+ // Start with existing extra fields
103
+ const extra = { ...paymentRequirements.extra };
104
+
105
+ // Add any facilitator-provided extra fields
106
+ if (supportedKind.extra?.contractAddress) {
107
+ extra.contractAddress = supportedKind.extra.contractAddress;
108
+ }
109
+ if (supportedKind.extra?.assetSymbol) {
110
+ extra.assetSymbol = supportedKind.extra.assetSymbol;
111
+ }
112
+ if (supportedKind.extra?.assetDecimals) {
113
+ extra.assetDecimals = supportedKind.extra.assetDecimals;
114
+ }
115
+ if (supportedKind.extra?.networkName) {
116
+ extra.networkName = supportedKind.extra.networkName;
117
+ }
118
+
119
+ return {
120
+ ...paymentRequirements,
121
+ extra,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Parse Money (string | number) to a decimal number.
127
+ */
128
+ private parseMoneyToDecimal(money: string | number): number {
129
+ if (typeof money === "number") {
130
+ return money;
131
+ }
132
+
133
+ // Remove $ sign and whitespace, then parse
134
+ const cleanMoney = money.replace(/^\$/, "").trim();
135
+ const amount = parseFloat(cleanMoney);
136
+
137
+ if (isNaN(amount)) {
138
+ throw new Error(`Invalid money format: ${money}`);
139
+ }
140
+
141
+ return amount;
142
+ }
143
+
144
+ /**
145
+ * Default money conversion implementation.
146
+ */
147
+ private defaultMoneyConversion(amount: number, network: Network): AssetAmount {
148
+ const token = this.getDefaultAsset(network);
149
+
150
+ // Convert decimal amount to token amount
151
+ const tokenAmount = parseAmount(amount.toString(), token.decimals);
152
+
153
+ return {
154
+ amount: tokenAmount.toString(),
155
+ asset: this.createAssetIdentifier(network, token.contractAddress),
156
+ extra: {
157
+ symbol: token.symbol,
158
+ name: token.name,
159
+ decimals: token.decimals,
160
+ contractAddress: token.contractAddress,
161
+ },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Create a CAIP-19 asset identifier for Stacks tokens
167
+ */
168
+ private createAssetIdentifier(
169
+ network: Network,
170
+ contractAddress: string,
171
+ ): string {
172
+ return `${network}/sip010:${contractAddress}`;
173
+ }
174
+
175
+ /**
176
+ * Get the default asset info for a network.
177
+ */
178
+ private getDefaultAsset(
179
+ network: Network,
180
+ ): { contractAddress: string; symbol: string; name: string; decimals: number } {
181
+ // If a preferred token is configured, try to use it
182
+ if (this.config.preferredToken) {
183
+ const preferred = getTokenConfig(network, this.config.preferredToken);
184
+ if (preferred) return preferred;
185
+ }
186
+
187
+ // Use the network's default token
188
+ const defaultToken = getDefaultToken(network);
189
+ if (defaultToken) return defaultToken;
190
+
191
+ throw new Error(`No tokens configured for network ${network}`);
192
+ }
193
+
194
+ /**
195
+ * Get all supported networks
196
+ */
197
+ static getSupportedNetworks(): string[] {
198
+ return Object.keys(TOKEN_REGISTRY);
199
+ }
200
+
201
+ /**
202
+ * Check if a network is supported
203
+ */
204
+ static isNetworkSupported(network: string): boolean {
205
+ return network in TOKEN_REGISTRY;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Create an exact-direct server for Stacks
211
+ */
212
+ export function createExactDirectStacksServer(
213
+ config: ExactDirectStacksServerConfig = {},
214
+ ): ExactDirectStacksServer {
215
+ return new ExactDirectStacksServer(config);
216
+ }
package/src/index.ts ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @t402/stacks - Stacks mechanism for T402
3
+ *
4
+ * This package provides support for SIP-010 token payments on Stacks (Bitcoin L2)
5
+ * using the exact-direct scheme.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Client usage
10
+ * import { createExactDirectStacksClient } from '@t402/stacks/exact-direct/client';
11
+ *
12
+ * const client = createExactDirectStacksClient({
13
+ * signer: myStacksSigner,
14
+ * });
15
+ *
16
+ * // Server usage
17
+ * import { registerExactDirectStacksServer } from '@t402/stacks/exact-direct/server';
18
+ *
19
+ * registerExactDirectStacksServer(server);
20
+ *
21
+ * // Facilitator usage
22
+ * import { createExactDirectStacksFacilitator } from '@t402/stacks/exact-direct/facilitator';
23
+ *
24
+ * const facilitator = createExactDirectStacksFacilitator(signer);
25
+ * ```
26
+ */
27
+
28
+ // Re-export constants
29
+ export {
30
+ STACKS_CAIP2_NAMESPACE,
31
+ STACKS_MAINNET_CAIP2,
32
+ STACKS_TESTNET_CAIP2,
33
+ SCHEME_EXACT_DIRECT,
34
+ DEFAULT_MAINNET_API,
35
+ DEFAULT_TESTNET_API,
36
+ STACKS_NETWORKS,
37
+ getNetworkConfig,
38
+ isStacksNetwork,
39
+ type StacksNetworkConfig,
40
+ } from "./constants.js";
41
+
42
+ // Re-export token registry
43
+ export {
44
+ SUSDC_MAINNET,
45
+ SUSDC_TESTNET,
46
+ TOKEN_REGISTRY,
47
+ DEFAULT_TOKENS,
48
+ getTokenConfig,
49
+ getDefaultToken,
50
+ getContractAddress,
51
+ type TokenConfig,
52
+ } from "./tokens.js";
53
+
54
+ // Re-export types
55
+ export type {
56
+ ExactDirectStacksPayload,
57
+ StacksTransactionResult,
58
+ StacksContractCall,
59
+ StacksFunctionArg,
60
+ StacksPostCondition,
61
+ StacksEvent,
62
+ ParsedTokenTransfer,
63
+ FacilitatorStacksSigner,
64
+ ClientStacksSigner,
65
+ StacksServerConfig,
66
+ StacksFacilitatorConfig,
67
+ } from "./types.js";
68
+
69
+ // Re-export utilities
70
+ export {
71
+ isValidPrincipal,
72
+ isValidTxId,
73
+ comparePrincipals,
74
+ formatAmount,
75
+ parseAmount,
76
+ extractTokenTransfer,
77
+ extractTokenTransferFromPostConditions,
78
+ } from "./utils.js";
package/src/tokens.ts ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Stacks Token Registry
3
+ *
4
+ * On Stacks, tokens are SIP-010 fungible tokens identified by
5
+ * their contract address (principal.contract-name).
6
+ */
7
+
8
+ import {
9
+ STACKS_MAINNET_CAIP2,
10
+ STACKS_TESTNET_CAIP2,
11
+ } from "./constants.js";
12
+
13
+ /**
14
+ * Token configuration for Stacks SIP-010 tokens
15
+ */
16
+ export interface TokenConfig {
17
+ /** Contract address (principal.contract-name) */
18
+ readonly contractAddress: string;
19
+ /** Token symbol */
20
+ readonly symbol: string;
21
+ /** Token name */
22
+ readonly name: string;
23
+ /** Decimal places */
24
+ readonly decimals: number;
25
+ /** Token issuer */
26
+ readonly issuer?: string;
27
+ }
28
+
29
+ /**
30
+ * sUSDC on Stacks Mainnet
31
+ * Contract: SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-susdc
32
+ * Decimals: 6
33
+ */
34
+ export const SUSDC_MAINNET: TokenConfig = {
35
+ contractAddress: "SP3Y2ZSH8P7D50B0VBTSX11S7XSG24M1VB9YFQA4K.token-susdc",
36
+ symbol: "sUSDC",
37
+ name: "Stacks USDC",
38
+ decimals: 6,
39
+ issuer: "Stacks",
40
+ };
41
+
42
+ /**
43
+ * sUSDC on Stacks Testnet
44
+ * Contract: ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token-susdc
45
+ * Decimals: 6
46
+ */
47
+ export const SUSDC_TESTNET: TokenConfig = {
48
+ contractAddress: "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.token-susdc",
49
+ symbol: "sUSDC",
50
+ name: "Test Stacks USDC",
51
+ decimals: 6,
52
+ };
53
+
54
+ /**
55
+ * Network-specific token registries
56
+ */
57
+ export const TOKEN_REGISTRY: Record<string, Record<string, TokenConfig>> = {
58
+ [STACKS_MAINNET_CAIP2]: {
59
+ sUSDC: SUSDC_MAINNET,
60
+ },
61
+ [STACKS_TESTNET_CAIP2]: {
62
+ sUSDC: SUSDC_TESTNET,
63
+ },
64
+ };
65
+
66
+ /**
67
+ * Default tokens per network
68
+ */
69
+ export const DEFAULT_TOKENS: Record<string, TokenConfig> = {
70
+ [STACKS_MAINNET_CAIP2]: SUSDC_MAINNET,
71
+ [STACKS_TESTNET_CAIP2]: SUSDC_TESTNET,
72
+ };
73
+
74
+ /**
75
+ * Get token configuration by network and symbol
76
+ */
77
+ export function getTokenConfig(
78
+ network: string,
79
+ symbol: string = "sUSDC",
80
+ ): TokenConfig | undefined {
81
+ return TOKEN_REGISTRY[network]?.[symbol];
82
+ }
83
+
84
+ /**
85
+ * Get the default token for a network
86
+ */
87
+ export function getDefaultToken(network: string): TokenConfig | undefined {
88
+ return DEFAULT_TOKENS[network];
89
+ }
90
+
91
+ /**
92
+ * Get contract address for a token on a network
93
+ */
94
+ export function getContractAddress(network: string, symbol: string = "sUSDC"): string | undefined {
95
+ return getTokenConfig(network, symbol)?.contractAddress;
96
+ }
package/src/types.ts ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Stacks T402 Types
3
+ */
4
+
5
+ import type { Network } from "@t402/core/types";
6
+
7
+ /**
8
+ * Payment payload for exact-direct scheme on Stacks
9
+ */
10
+ export type ExactDirectStacksPayload = {
11
+ /** Transaction ID (0x-prefixed hex, 64 chars) */
12
+ txId: string;
13
+ /** Sender address (Stacks principal) */
14
+ from: string;
15
+ /** Recipient address (Stacks principal) */
16
+ to: string;
17
+ /** Amount in smallest unit (atomic) */
18
+ amount: string;
19
+ /** SIP-010 contract address (principal.contract-name) */
20
+ contractAddress: string;
21
+ };
22
+
23
+ /**
24
+ * Result of a Stacks transaction query from Hiro API
25
+ */
26
+ export interface StacksTransactionResult {
27
+ /** Transaction ID */
28
+ txId: string;
29
+ /** Transaction type */
30
+ txType: string;
31
+ /** Transaction status */
32
+ txStatus: "success" | "abort_by_response" | "abort_by_post_condition" | "pending";
33
+ /** Block hash */
34
+ blockHash: string;
35
+ /** Block height */
36
+ blockHeight: number;
37
+ /** Burn block time (unix timestamp) */
38
+ burnBlockTime: number;
39
+ /** Sender address */
40
+ senderAddress: string;
41
+ /** Contract call details (for contract-call transactions) */
42
+ contractCall?: StacksContractCall;
43
+ /** Post-condition mode */
44
+ postConditionMode: string;
45
+ /** Post conditions */
46
+ postConditions: StacksPostCondition[];
47
+ /** Events */
48
+ events: StacksEvent[];
49
+ }
50
+
51
+ /**
52
+ * Stacks contract call details
53
+ */
54
+ export interface StacksContractCall {
55
+ /** Contract ID (principal.contract-name) */
56
+ contractId: string;
57
+ /** Function name */
58
+ functionName: string;
59
+ /** Function arguments */
60
+ functionArgs: StacksFunctionArg[];
61
+ }
62
+
63
+ /**
64
+ * Stacks function argument
65
+ */
66
+ export interface StacksFunctionArg {
67
+ /** Hex-encoded value */
68
+ hex: string;
69
+ /** Human-readable representation */
70
+ repr: string;
71
+ /** Argument type */
72
+ type: string;
73
+ }
74
+
75
+ /**
76
+ * Stacks post condition
77
+ */
78
+ export interface StacksPostCondition {
79
+ /** Principal (sender) */
80
+ principal: {
81
+ type_id: string;
82
+ address: string;
83
+ contract_name?: string;
84
+ };
85
+ /** Condition code */
86
+ conditionCode: string;
87
+ /** Amount */
88
+ amount: string;
89
+ /** Asset info */
90
+ asset?: {
91
+ contractAddress: string;
92
+ contractName: string;
93
+ assetName: string;
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Stacks transaction event
99
+ */
100
+ export interface StacksEvent {
101
+ /** Event type */
102
+ eventType: string;
103
+ /** Event index */
104
+ eventIndex: number;
105
+ /** Fungible token transfer asset event (for ft_transfer events) */
106
+ asset?: {
107
+ assetEventType: string;
108
+ assetId: string;
109
+ sender: string;
110
+ recipient: string;
111
+ amount: string;
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Parsed token transfer from transaction events
117
+ */
118
+ export interface ParsedTokenTransfer {
119
+ /** Contract address (principal.contract-name) */
120
+ contractAddress: string;
121
+ /** Sender address */
122
+ from: string;
123
+ /** Recipient address */
124
+ to: string;
125
+ /** Amount transferred (atomic units) */
126
+ amount: string;
127
+ /** Whether the transfer was successful */
128
+ success: boolean;
129
+ }
130
+
131
+ /**
132
+ * Signer interface for Stacks facilitator
133
+ */
134
+ export interface FacilitatorStacksSigner {
135
+ /**
136
+ * Get the facilitator's addresses for a network
137
+ */
138
+ getAddresses(network: Network): string[];
139
+
140
+ /**
141
+ * Query a transaction by ID from Hiro API
142
+ */
143
+ queryTransaction(txId: string): Promise<StacksTransactionResult | null>;
144
+ }
145
+
146
+ /**
147
+ * Client signer interface for signing transactions
148
+ */
149
+ export interface ClientStacksSigner {
150
+ /**
151
+ * Get the signer's address
152
+ */
153
+ getAddress(): Promise<string>;
154
+
155
+ /**
156
+ * Sign and submit a SIP-010 token transfer
157
+ * Returns the transaction ID
158
+ */
159
+ transferToken(
160
+ contractAddress: string,
161
+ to: string,
162
+ amount: string,
163
+ ): Promise<{ txId: string }>;
164
+ }
165
+
166
+ /**
167
+ * Configuration for Stacks server
168
+ */
169
+ export interface StacksServerConfig {
170
+ /** Custom API URL */
171
+ apiUrl?: string;
172
+ /** Facilitator addresses per network */
173
+ facilitatorAddresses?: Record<string, string>;
174
+ }
175
+
176
+ /**
177
+ * Configuration for Stacks facilitator
178
+ */
179
+ export interface StacksFacilitatorConfig {
180
+ /** Maximum age of transaction to accept (in seconds) */
181
+ maxTransactionAge?: number;
182
+ /** Duration to cache used transaction IDs */
183
+ usedTxCacheDuration?: number;
184
+ }