@atomiqlabs/sdk 8.8.3 → 8.9.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 (130) hide show
  1. package/api/index.d.ts +1 -0
  2. package/api/index.js +3 -0
  3. package/dist/ApiList.d.ts +37 -0
  4. package/dist/ApiList.js +30 -0
  5. package/dist/api/ApiEndpoints.d.ts +393 -0
  6. package/dist/api/ApiEndpoints.js +2 -0
  7. package/dist/api/ApiParser.d.ts +10 -0
  8. package/dist/api/ApiParser.js +134 -0
  9. package/dist/api/ApiTypes.d.ts +157 -0
  10. package/dist/api/ApiTypes.js +75 -0
  11. package/dist/api/SerializedAction.d.ts +40 -0
  12. package/dist/api/SerializedAction.js +59 -0
  13. package/dist/api/SwapperApi.d.ts +50 -0
  14. package/dist/api/SwapperApi.js +431 -0
  15. package/dist/api/index.d.ts +5 -0
  16. package/dist/api/index.js +24 -0
  17. package/dist/events/UnifiedSwapEventListener.d.ts +4 -3
  18. package/dist/events/UnifiedSwapEventListener.js +8 -2
  19. package/dist/http/HttpUtils.d.ts +4 -2
  20. package/dist/http/HttpUtils.js +10 -4
  21. package/dist/http/paramcoders/client/StreamingFetchPromise.d.ts +2 -1
  22. package/dist/http/paramcoders/client/StreamingFetchPromise.js +3 -2
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/intermediaries/IntermediaryDiscovery.d.ts +7 -2
  26. package/dist/intermediaries/IntermediaryDiscovery.js +4 -4
  27. package/dist/intermediaries/apis/IntermediaryAPI.d.ts +171 -14
  28. package/dist/intermediaries/apis/IntermediaryAPI.js +174 -28
  29. package/dist/intermediaries/auth/SignedKeyBasedAuth.d.ts +14 -0
  30. package/dist/intermediaries/auth/SignedKeyBasedAuth.js +68 -0
  31. package/dist/storage/IUnifiedStorage.d.ts +45 -3
  32. package/dist/storage/UnifiedSwapStorage.d.ts +8 -2
  33. package/dist/storage/UnifiedSwapStorage.js +46 -8
  34. package/dist/swapper/Swapper.d.ts +36 -3
  35. package/dist/swapper/Swapper.js +54 -18
  36. package/dist/swapper/SwapperUtils.d.ts +18 -2
  37. package/dist/swapper/SwapperUtils.js +39 -1
  38. package/dist/swaps/ISwap.d.ts +70 -9
  39. package/dist/swaps/ISwap.js +28 -6
  40. package/dist/swaps/ISwapWrapper.d.ts +11 -1
  41. package/dist/swaps/ISwapWrapper.js +23 -3
  42. package/dist/swaps/escrow_swaps/IEscrowSwap.d.ts +1 -1
  43. package/dist/swaps/escrow_swaps/IEscrowSwap.js +4 -2
  44. package/dist/swaps/escrow_swaps/IEscrowSwapWrapper.d.ts +2 -1
  45. package/dist/swaps/escrow_swaps/IEscrowSwapWrapper.js +2 -2
  46. package/dist/swaps/escrow_swaps/frombtc/IFromBTCLNWrapper.d.ts +3 -1
  47. package/dist/swaps/escrow_swaps/frombtc/IFromBTCLNWrapper.js +3 -2
  48. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap.d.ts +47 -31
  49. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap.js +201 -67
  50. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.d.ts +3 -1
  51. package/dist/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.js +6 -6
  52. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.d.ts +82 -15
  53. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.js +304 -98
  54. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.d.ts +3 -1
  55. package/dist/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.js +6 -6
  56. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCSwap.d.ts +75 -42
  57. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCSwap.js +424 -87
  58. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.d.ts +3 -1
  59. package/dist/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.js +7 -7
  60. package/dist/swaps/escrow_swaps/tobtc/IToBTCSwap.d.ts +54 -11
  61. package/dist/swaps/escrow_swaps/tobtc/IToBTCSwap.js +214 -41
  62. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.d.ts +2 -1
  63. package/dist/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.js +7 -8
  64. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.d.ts +3 -1
  65. package/dist/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.js +5 -5
  66. package/dist/swaps/spv_swaps/SpvFromBTCSwap.d.ts +76 -19
  67. package/dist/swaps/spv_swaps/SpvFromBTCSwap.js +290 -51
  68. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.d.ts +3 -1
  69. package/dist/swaps/spv_swaps/SpvFromBTCWrapper.js +5 -5
  70. package/dist/swaps/trusted/ln/LnForGasSwap.d.ts +53 -12
  71. package/dist/swaps/trusted/ln/LnForGasSwap.js +163 -49
  72. package/dist/swaps/trusted/ln/LnForGasWrapper.js +1 -2
  73. package/dist/swaps/trusted/onchain/OnchainForGasSwap.d.ts +14 -13
  74. package/dist/swaps/trusted/onchain/OnchainForGasSwap.js +30 -47
  75. package/dist/swaps/trusted/onchain/OnchainForGasWrapper.d.ts +3 -1
  76. package/dist/swaps/trusted/onchain/OnchainForGasWrapper.js +4 -4
  77. package/dist/types/SwapExecutionAction.d.ts +141 -34
  78. package/dist/types/SwapExecutionAction.js +104 -0
  79. package/dist/types/SwapExecutionStep.d.ts +144 -0
  80. package/dist/types/SwapExecutionStep.js +87 -0
  81. package/dist/types/TokenAmount.d.ts +6 -0
  82. package/dist/types/TokenAmount.js +26 -1
  83. package/dist/utils/BitcoinUtils.d.ts +2 -0
  84. package/dist/utils/BitcoinUtils.js +34 -1
  85. package/dist/utils/Utils.d.ts +3 -1
  86. package/dist/utils/Utils.js +7 -1
  87. package/package.json +7 -4
  88. package/src/api/ApiEndpoints.ts +427 -0
  89. package/src/api/ApiParser.ts +138 -0
  90. package/src/api/ApiTypes.ts +229 -0
  91. package/src/api/SerializedAction.ts +97 -0
  92. package/src/api/SwapperApi.ts +545 -0
  93. package/src/api/index.ts +5 -0
  94. package/src/events/UnifiedSwapEventListener.ts +11 -3
  95. package/src/http/HttpUtils.ts +10 -4
  96. package/src/http/paramcoders/client/StreamingFetchPromise.ts +4 -2
  97. package/src/index.ts +1 -0
  98. package/src/intermediaries/IntermediaryDiscovery.ts +9 -2
  99. package/src/intermediaries/apis/IntermediaryAPI.ts +314 -30
  100. package/src/intermediaries/auth/SignedKeyBasedAuth.ts +69 -0
  101. package/src/storage/IUnifiedStorage.ts +45 -4
  102. package/src/storage/UnifiedSwapStorage.ts +42 -8
  103. package/src/swapper/Swapper.ts +87 -18
  104. package/src/swapper/SwapperUtils.ts +42 -2
  105. package/src/swaps/ISwap.ts +88 -16
  106. package/src/swaps/ISwapWrapper.ts +28 -3
  107. package/src/swaps/escrow_swaps/IEscrowSwap.ts +5 -3
  108. package/src/swaps/escrow_swaps/IEscrowSwapWrapper.ts +3 -1
  109. package/src/swaps/escrow_swaps/frombtc/IFromBTCLNWrapper.ts +4 -1
  110. package/src/swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap.ts +264 -67
  111. package/src/swaps/escrow_swaps/frombtc/ln/FromBTCLNWrapper.ts +6 -4
  112. package/src/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap.ts +390 -89
  113. package/src/swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoWrapper.ts +6 -4
  114. package/src/swaps/escrow_swaps/frombtc/onchain/FromBTCSwap.ts +548 -94
  115. package/src/swaps/escrow_swaps/frombtc/onchain/FromBTCWrapper.ts +7 -5
  116. package/src/swaps/escrow_swaps/tobtc/IToBTCSwap.ts +276 -45
  117. package/src/swaps/escrow_swaps/tobtc/ln/ToBTCLNWrapper.ts +7 -6
  118. package/src/swaps/escrow_swaps/tobtc/onchain/ToBTCWrapper.ts +5 -3
  119. package/src/swaps/spv_swaps/SpvFromBTCSwap.ts +393 -57
  120. package/src/swaps/spv_swaps/SpvFromBTCWrapper.ts +5 -3
  121. package/src/swaps/trusted/ln/LnForGasSwap.ts +211 -47
  122. package/src/swaps/trusted/ln/LnForGasWrapper.ts +1 -2
  123. package/src/swaps/trusted/onchain/OnchainForGasSwap.ts +32 -51
  124. package/src/swaps/trusted/onchain/OnchainForGasWrapper.ts +5 -3
  125. package/src/types/SwapExecutionAction.ts +266 -43
  126. package/src/types/SwapExecutionStep.ts +224 -0
  127. package/src/types/TokenAmount.ts +36 -2
  128. package/src/utils/BitcoinUtils.ts +32 -0
  129. package/src/utils/Utils.ts +10 -1
  130. package/src/intermediaries/apis/TrustedIntermediaryAPI.ts +0 -258
@@ -0,0 +1,545 @@
1
+ import {MultiChain, Swapper} from "../swapper/Swapper";
2
+ import {ApiEndpoint, createApiEndpoint, toApiAmount, toApiLNURL, toApiToken} from "./ApiTypes";
3
+ import {ISwap} from "../swaps/ISwap";
4
+ import {serializeAction} from "./SerializedAction";
5
+ import {FeeType} from "../enums/FeeType";
6
+ import {SwapSide} from "../enums/SwapSide";
7
+ import {SwapType} from "../enums/SwapType";
8
+ import {MinimalBitcoinWalletInterface} from "../types/wallets/MinimalBitcoinWalletInterface";
9
+ import {FromBTCLNSwap, FromBTCLNSwapState} from "../swaps/escrow_swaps/frombtc/ln/FromBTCLNSwap";
10
+ import {FromBTCLNAutoSwap, FromBTCLNAutoSwapState} from "../swaps/escrow_swaps/frombtc/ln_auto/FromBTCLNAutoSwap";
11
+ import {
12
+ CreateSwapInput,
13
+ CreateSwapOutput,
14
+ GetSpendableBalanceInput,
15
+ GetSpendableBalanceOutput,
16
+ GetSupportedTokensInput,
17
+ GetSupportedTokensOutput,
18
+ GetSwapCounterTokensInput,
19
+ GetSwapCounterTokensOutput,
20
+ GetSwapLimitsInput,
21
+ GetSwapLimitsOutput,
22
+ GetSwapStatusInput,
23
+ GetSwapStatusOutput,
24
+ ListPendingSwapsInput,
25
+ ListPendingSwapsOutput,
26
+ ListSwapOutput,
27
+ ListSwapsInput,
28
+ ListSwapsOutput,
29
+ ParseAddressInput,
30
+ ParseAddressOutput,
31
+ SettleWithLnurlInput,
32
+ SettleWithLnurlOutput,
33
+ SubmitTransactionInput,
34
+ SubmitTransactionOutput,
35
+ SwapOutputBase
36
+ } from "./ApiEndpoints";
37
+ import {SwapExecutionStep} from "../types/SwapExecutionStep";
38
+ import {SwapStateInfo} from "../types/SwapStateInfo";
39
+ import {IEscrowSwap} from "../swaps/escrow_swaps/IEscrowSwap";
40
+ import {ToBTCLNSwap} from "../swaps/escrow_swaps/tobtc/ln/ToBTCLNSwap";
41
+
42
+ function requiresSecretRevealForApi(swap: ISwap, state: number): boolean | undefined {
43
+ if(swap instanceof FromBTCLNSwap) {
44
+ if(swap.hasSecretPreimage()) return false;
45
+ return state===FromBTCLNSwapState.PR_PAID || state===FromBTCLNSwapState.CLAIM_COMMITED;
46
+ }
47
+ if(swap instanceof FromBTCLNAutoSwap) {
48
+ if(swap.hasSecretPreimage()) return false;
49
+ return state===FromBTCLNAutoSwapState.CLAIM_COMMITED;
50
+ }
51
+ }
52
+
53
+ function createSwapOutputBase(
54
+ swap: ISwap,
55
+ steps: SwapExecutionStep[],
56
+ stateInfo: SwapStateInfo<number>
57
+ ): SwapOutputBase {
58
+ const input = swap.getInput();
59
+ const output = swap.getOutput();
60
+ const feeBreakdown = swap.getFeeBreakdown();
61
+
62
+ // Build fees from breakdown
63
+ const swapFeeEntry = feeBreakdown.find(f => f.type === FeeType.SWAP);
64
+ const networkFeeEntry = feeBreakdown.find(f => f.type === FeeType.NETWORK_OUTPUT);
65
+
66
+ return {
67
+ swapId: swap.getId(),
68
+ swapType: SwapType[swap.getType()],
69
+
70
+ state: {
71
+ number: stateInfo.state,
72
+ name: stateInfo.name,
73
+ description: stateInfo.description
74
+ },
75
+
76
+ quote: {
77
+ inputAmount: toApiAmount(input),
78
+ outputAmount: toApiAmount(output),
79
+ fees: {
80
+ swap: swapFeeEntry
81
+ ? toApiAmount(swapFeeEntry.fee.amountInSrcToken)
82
+ : { amount: "0", rawAmount: "0", decimals: 0, symbol: "", chain: "" },
83
+ ...(networkFeeEntry ? {
84
+ networkOutput: toApiAmount(networkFeeEntry.fee.amountInSrcToken)
85
+ } : {})
86
+ },
87
+ expiry: swap.getQuoteExpiry(),
88
+ outputAddress: swap.getOutputAddress()!
89
+ },
90
+
91
+ createdAt: swap.createdAt,
92
+
93
+ steps,
94
+
95
+ ...(swap instanceof ToBTCLNSwap && swap.isLNURL() ? {
96
+ lnurl: {
97
+ pay: swap.getLNURL()!,
98
+ successAction: swap.getSuccessAction() ?? undefined
99
+ }
100
+ } : (swap instanceof FromBTCLNSwap || swap instanceof FromBTCLNAutoSwap) && swap.isLNURL() ? {
101
+ lnurl: {
102
+ withdraw: swap.getLNURL()!,
103
+ }
104
+ } : {})
105
+ };
106
+ }
107
+
108
+ function createListSwapOutput(
109
+ swap: ISwap,
110
+ steps: SwapExecutionStep[],
111
+ stateInfo: SwapStateInfo<number>
112
+ ): ListSwapOutput {
113
+ return {
114
+ ...createSwapOutputBase(swap, steps, stateInfo),
115
+
116
+ isFinished: swap.isFinished(),
117
+ isSuccess: swap.isSuccessful(),
118
+ isFailed: swap.isFailed(),
119
+ isExpired: swap.isQuoteExpired()
120
+ };
121
+ }
122
+
123
+ function parseSwapSide(side: "INPUT" | "OUTPUT"): SwapSide {
124
+ return side === "INPUT" ? SwapSide.INPUT : SwapSide.OUTPUT;
125
+ }
126
+
127
+ export type SwapperApiConfig = {
128
+ syncOnGetStatus?: boolean,
129
+ idempotentTxSubmission?: boolean
130
+ };
131
+
132
+ export class SwapperApi<T extends MultiChain> {
133
+
134
+ readonly endpoints: {
135
+ createSwap: ApiEndpoint<CreateSwapInput, CreateSwapOutput, "POST">;
136
+ listSwaps: ApiEndpoint<ListSwapsInput, ListSwapsOutput, "GET">;
137
+ listPendingSwaps: ApiEndpoint<ListPendingSwapsInput, ListPendingSwapsOutput, "GET">;
138
+ getSupportedTokens: ApiEndpoint<GetSupportedTokensInput, GetSupportedTokensOutput, "GET">;
139
+ getSwapCounterTokens: ApiEndpoint<GetSwapCounterTokensInput, GetSwapCounterTokensOutput, "GET">;
140
+ getSwapLimits: ApiEndpoint<GetSwapLimitsInput, GetSwapLimitsOutput, "GET">;
141
+ parseAddress: ApiEndpoint<ParseAddressInput, ParseAddressOutput, "GET">;
142
+ getSpendableBalance: ApiEndpoint<GetSpendableBalanceInput, GetSpendableBalanceOutput, "GET">;
143
+ getSwapStatus: ApiEndpoint<GetSwapStatusInput, GetSwapStatusOutput, "GET">;
144
+ submitTransaction: ApiEndpoint<SubmitTransactionInput, SubmitTransactionOutput, "POST">;
145
+ settleWithLnurl: ApiEndpoint<SettleWithLnurlInput, SettleWithLnurlOutput, "POST">;
146
+ };
147
+
148
+ constructor(private swapper: Swapper<T>, private readonly config?: SwapperApiConfig) {
149
+ this.config ??= {};
150
+ this.config.syncOnGetStatus ??= true;
151
+ this.endpoints = {
152
+ createSwap: createApiEndpoint<CreateSwapInput, CreateSwapOutput, "POST">("POST", "Create a new cross-chain atomic swap. Returns a swap object with swapId, swapType, state, createdAt, quote (with inputAmount and outputAmount as ApiAmount objects each having amount/rawAmount/decimals/symbol/chain, a fees breakdown {swap, networkOutput?} of ApiAmount values, expiry, and outputAddress), a steps array, and optional lnurl. After creation, poll getSwapStatus periodically to get the next required action.", this.createSwap.bind(this), {
153
+ srcToken: { type: "string", required: true, description: "Source token ticker (e.g. 'BITCOIN-BTC', 'LIGHTNING-BTC', 'STARKNET-STRK', 'SOLANA-SOL')" },
154
+ dstToken: { type: "string", required: true, description: "Destination token ticker" },
155
+ amount: { type: "bigint", required: true, description: "Amount in base units as an integer" },
156
+ amountType: { type: "string", required: true, description: "EXACT_IN or EXACT_OUT", allowedValues: ["EXACT_IN", "EXACT_OUT"] },
157
+ srcAddress: { type: "string", required: false, description: "Source address (only required for Smart chain -> BTC/Lightning swaps)" },
158
+ dstAddress: { type: "string", required: true, description: "Destination address" },
159
+ gasAmount: { type: "bigint", required: false, description: "Gas token amount to receive on destination chain, in base units" },
160
+ paymentHash: { type: "string", required: false, description: "Custom payment hash for Lightning swaps" },
161
+ lightningInvoiceDescription: { type: "string", required: false, description: "Description for Lightning invoice" },
162
+ lightningInvoiceDescriptionHash: { type: "string", required: false, description: "Description hash for Lightning invoice (hex)" },
163
+ lightningPaymentHTLCTimeout: { type: "number", required: false, description: "Custom expiry time in seconds" }
164
+ }),
165
+ listSwaps: createApiEndpoint<ListSwapsInput, ListSwapsOutput, "GET">("GET", "List all swaps for a given signer address. Returns an array of swap objects, each with swapId, swapType, state, quote, steps, and terminal state flags (isFinished, isSuccess, isFailed, isExpired). Optionally filter by smart chain.", this.listSwaps.bind(this), {
166
+ signer: { type: "string", required: true, description: "Smart chain signer address to filter swaps for" },
167
+ chainId: { type: "string", required: false, description: "Optional smart chain identifier to filter swaps" }
168
+ }),
169
+ listPendingSwaps: createApiEndpoint<ListPendingSwapsInput, ListPendingSwapsOutput, "GET">("GET", "List swaps that require user action for a given signer address. Returns an array of swap objects with the same structure as listSwaps.", this.listPendingSwaps.bind(this), {
170
+ signer: { type: "string", required: true, description: "Smart chain signer address to filter pending swaps for" },
171
+ chainId: { type: "string", required: false, description: "Optional smart chain identifier to filter pending swaps" }
172
+ }),
173
+ getSupportedTokens: createApiEndpoint<GetSupportedTokensInput, GetSupportedTokensOutput, "GET">("GET", "List all tokens available as swap input or output. Returns an array of ApiToken objects, each with id (e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK), chainId, ticker, name, decimals, and address.", this.getSupportedTokens.bind(this), {
174
+ side: {
175
+ type: "string",
176
+ required: true,
177
+ description: "Whether to list valid source tokens (INPUT) or destination tokens (OUTPUT)",
178
+ allowedValues: ["INPUT", "OUTPUT"]
179
+ }
180
+ }),
181
+ getSwapCounterTokens: createApiEndpoint<GetSwapCounterTokensInput, GetSwapCounterTokensOutput, "GET">("GET", "Get tokens that can be swapped against a given token. Returns an array of ApiToken objects (id, chainId, ticker, name, decimals, address). Use to discover valid trading pairs.", this.getSwapCounterTokens.bind(this), {
182
+ token: {
183
+ type: "string",
184
+ required: true,
185
+ description: "Token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK, or a token address"
186
+ },
187
+ side: {
188
+ type: "string",
189
+ required: true,
190
+ description: "Treat the provided token as a source token (INPUT) or destination token (OUTPUT)",
191
+ allowedValues: ["INPUT", "OUTPUT"]
192
+ }
193
+ }),
194
+ getSwapLimits: createApiEndpoint<GetSwapLimitsInput, GetSwapLimitsOutput, "GET">("GET", "Get minimum and maximum swap amounts for a source/destination token pair. Returns {input: {min, max?}, output: {min, max?}} where each value is an ApiAmount object with amount (decimal string), rawAmount (base units string), decimals, symbol, and chain.", this.getSwapLimits.bind(this), {
195
+ srcToken: { type: "string", required: true, description: "Source token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK" },
196
+ dstToken: { type: "string", required: true, description: "Destination token identifier accepted by the API, e.g. BITCOIN-BTC, LIGHTNING-BTC, STARKNET-STRK" }
197
+ }),
198
+ parseAddress: createApiEndpoint<ParseAddressInput, ParseAddressOutput, "GET">("GET", "Parse and validate an address, Lightning invoice, LNURL, or Bitcoin URI. Returns {address, type} and optionally lnurl (ApiLNURL with pay/withdraw details), min/max/amount (as ApiAmount objects).", this.parseAddress.bind(this), {
199
+ address: { type: "string", required: true, description: "Address, invoice, LNURL, or URI string to parse" }
200
+ }),
201
+ getSpendableBalance: createApiEndpoint<GetSpendableBalanceInput, GetSpendableBalanceOutput, "GET">("GET", "Get the spendable balance for a wallet address and token, accounting for chain fees. Returns {balance: ApiAmount, feeRate?} where ApiAmount has amount (decimal string), rawAmount (base units string), decimals, symbol, and chain.", this.getSpendableBalance.bind(this), {
202
+ wallet: { type: "string", required: true, description: "Wallet address to query" },
203
+ token: { type: "string", required: true, description: "Token identifier accepted by the API, e.g. BITCOIN-BTC, STARKNET-STRK, or a token address" },
204
+ targetChain: { type: "string", required: false, description: "Destination smart chain for Bitcoin SPV-vault fee estimation" },
205
+ gasDrop: { type: "boolean", required: false, description: "Whether to include gas-drop footprint when estimating Bitcoin SPV-vault spendable balance" },
206
+ feeRate: { type: "string", required: false, description: "Manual fee rate override" },
207
+ minBitcoinFeeRate: { type: "number", required: false, description: "Minimum Bitcoin fee rate to enforce" },
208
+ feeMultiplier: { type: "number", required: false, description: "Multiplier applied to smart-chain native token commit fee estimate" }
209
+ }),
210
+ getSwapStatus: createApiEndpoint<GetSwapStatusInput, GetSwapStatusOutput, "GET">("GET", "Get the current status and next required action for a swap. Returns swap state, terminal flags (isFinished, isSuccess, isFailed, isExpired), and currentAction (an action object, or null when no action is currently required). For Lightning-to-smart-chain swaps it may also return requiresSecretReveal: true — when set, pass the secret parameter in subsequent getSwapStatus calls so the HTLC can be claimed. Handle each action type: SignPSBT — ask user to sign the Bitcoin PSBT with their wallet, then submit via submitTransaction. SignSmartChainTransaction — ask user to sign with their Solana/Starknet/EVM wallet, then submit via submitTransaction. SendToAddress — show the address and amount to the user, they pay externally, keep polling. Wait — poll again after pollTimeSeconds. Poll repeatedly until isFinished is true.", this.getSwapStatus.bind(this), {
211
+ swapId: { type: "string", required: true, description: "The swap identifier" },
212
+ secret: { type: "string", required: false, description: "Revealed swap secret pre-image (in hexadecimal format) for lightning network swaps" },
213
+ bitcoinAddress: { type: "string", required: false, description: "Bitcoin wallet address to obtain funded PSBT" },
214
+ bitcoinPublicKey: { type: "string", required: false, description: "Bitcoin wallet public key (in hexadecimal format) to obtain funded PSBT" },
215
+ bitcoinFeeRate: { type: "number", required: false, description: "Fee rate to use when creating a funded PSBT" },
216
+ signer: { type: "string", required: false, description: "Alternative different smart chain signer to use for refunds and manual settlement" }
217
+ }),
218
+ submitTransaction: createApiEndpoint<SubmitTransactionInput, SubmitTransactionOutput, "POST">("POST", "Submit signed transaction(s) for a swap. Call this after the user has signed the transaction returned by getSwapStatus. Returns {txHashes: string[]} with the submitted transaction hashes. After submission, continue polling getSwapStatus.", this.submitTransaction.bind(this), {
219
+ swapId: { type: "string", required: true, description: "The swap identifier" },
220
+ signedTxs: {
221
+ type: "array",
222
+ required: true,
223
+ description: "Array of signed transaction data",
224
+ items: {type: "string", required: true, description: "Single string-serialized & signed transaction"}
225
+ }
226
+ }),
227
+ settleWithLnurl: createApiEndpoint<SettleWithLnurlInput, SettleWithLnurlOutput, "POST">("POST", "Settle a Lightning Network swap using an LNURL-withdraw link. Returns {paymentHash: string} on success.", this.settleWithLnurl.bind(this), {
228
+ swapId: { type: "string", required: true, description: "The swap identifier" },
229
+ lnurlWithdraw: { type: "string", required: false, description: "LNURL-withdraw link to use to settle the Lightning network swap, if the swap was already created with the LNURL-withdraw link, this is optional" }
230
+ })
231
+ };
232
+ }
233
+
234
+ private txSerializer(chainId: string, tx: any): Promise<string> {
235
+ const chain = (this.swapper._chains as any)[chainId];
236
+ if (chain == null) throw new Error("Unknown chain: " + chainId);
237
+ return chain.chainInterface.serializeTx(tx);
238
+ }
239
+
240
+ async init(): Promise<void> {
241
+ await this.swapper.init();
242
+ }
243
+
244
+ /**
245
+ * Should be ran periodically, this synchronizes the swap's state with the on-chain data and also purges
246
+ * expired swaps from the persistent storage
247
+ */
248
+ async sync(): Promise<void> {
249
+ await this.swapper._syncSwaps();
250
+ }
251
+
252
+ /**
253
+ * Optionally good to run this periodically, such that any LPs that are dropped off because they are unresponsive
254
+ * can be found again.
255
+ */
256
+ async reloadLps(): Promise<void> {
257
+ await this.swapper.intermediaryDiscovery.reloadIntermediaries();
258
+ }
259
+
260
+ private async createSwap(input: CreateSwapInput): Promise<CreateSwapOutput> {
261
+ const exactIn = input.amountType === "EXACT_IN";
262
+
263
+ // Build options from input
264
+ const options: any = {};
265
+ if (input.gasAmount != null) options.gasAmount = input.gasAmount;
266
+ if (input.paymentHash != null) options.paymentHash = Buffer.from(input.paymentHash, "hex");
267
+ if (input.lightningInvoiceDescription != null) options.description = input.lightningInvoiceDescription;
268
+ if (input.lightningInvoiceDescriptionHash != null) options.descriptionHash = Buffer.from(input.lightningInvoiceDescriptionHash, "hex");
269
+ if (input.lightningPaymentHTLCTimeout != null) options.expirySeconds = input.lightningPaymentHTLCTimeout;
270
+
271
+ // swapper.swap() handles routing based on token types
272
+ const swap = await this.swapper.swap(
273
+ input.srcToken,
274
+ input.dstToken,
275
+ input.amount,
276
+ exactIn,
277
+ input.srcAddress,
278
+ input.dstAddress,
279
+ Object.keys(options).length > 0 ? options : undefined
280
+ );
281
+
282
+ const {steps, stateInfo} = await swap.getExecutionStatus({skipBuildingAction: true});
283
+
284
+ return createSwapOutputBase(swap, steps, stateInfo);
285
+ }
286
+
287
+ private validateSwapListInput(input: ListSwapsInput): void {
288
+ if (input.chainId != null && !this.swapper.getSmartChains().includes(input.chainId as any)) {
289
+ throw new Error("Unknown chainId: " + input.chainId);
290
+ }
291
+
292
+ if (!this.swapper.Utils.isValidSmartChainAddress(input.signer, input.chainId as any)) {
293
+ throw new Error(
294
+ input.chainId != null
295
+ ? `Invalid ${input.chainId} signer address: ` + input.signer
296
+ : `Invalid smart chain signer address: ` + input.signer
297
+ );
298
+ }
299
+ }
300
+
301
+ private async createListedSwapOutputs(swaps: ISwap[]): Promise<ListSwapsOutput> {
302
+ return Promise.all(
303
+ swaps
304
+ .filter(swap => swap.getType() !== SwapType.TRUSTED_FROM_BTC)
305
+ .map(async swap => {
306
+ const {steps, stateInfo} = await swap.getExecutionStatus({skipBuildingAction: true});
307
+ return createListSwapOutput(swap, steps, stateInfo);
308
+ })
309
+ );
310
+ }
311
+
312
+ private async listSwaps(input: ListSwapsInput): Promise<ListSwapsOutput> {
313
+ this.validateSwapListInput(input);
314
+
315
+ const swaps = await this.swapper.getAllSwaps(input.chainId as any, input.signer);
316
+ return this.createListedSwapOutputs(swaps);
317
+ }
318
+
319
+ private async listPendingSwaps(input: ListPendingSwapsInput): Promise<ListPendingSwapsOutput> {
320
+ this.validateSwapListInput(input);
321
+
322
+ const swaps = await this.swapper.getPendingSwaps(input.chainId as any, input.signer);
323
+ return this.createListedSwapOutputs(swaps);
324
+ }
325
+
326
+ private async getSupportedTokens(input: GetSupportedTokensInput): Promise<GetSupportedTokensOutput> {
327
+ return this.swapper.getSupportedTokens(parseSwapSide(input.side)).map(toApiToken);
328
+ }
329
+
330
+ private async getSwapCounterTokens(input: GetSwapCounterTokensInput): Promise<GetSwapCounterTokensOutput> {
331
+ const token = this.swapper.getToken(input.token);
332
+ return this.swapper.getSwapCounterTokens(token, parseSwapSide(input.side)).map(toApiToken);
333
+ }
334
+
335
+ private async getSwapLimits(input: GetSwapLimitsInput): Promise<GetSwapLimitsOutput> {
336
+ const srcToken = this.swapper.getToken(input.srcToken);
337
+ const dstToken = this.swapper.getToken(input.dstToken);
338
+ let limits = this.swapper.getSwapLimits(srcToken, dstToken);
339
+
340
+ if(dstToken.chainId!=="LIGHTNING") {
341
+ if(limits.input.min.rawAmount===1n || limits.output.min.rawAmount===1n) {
342
+ // Execute a dummy swap to get the proper limits
343
+ try {
344
+ await this.swapper.swap(
345
+ srcToken, dstToken,
346
+ 1n, limits.input.min.rawAmount===1n,
347
+ srcToken.chainId==="LIGHTNING" ? undefined : this.swapper.Utils.randomAddress(srcToken.chainId),
348
+ this.swapper.Utils.randomAddress(dstToken.chainId)
349
+ );
350
+ } catch (e) {}
351
+ limits = this.swapper.getSwapLimits(srcToken, dstToken);
352
+ }
353
+ }
354
+
355
+ return {
356
+ input: {
357
+ min: toApiAmount(limits.input.min),
358
+ ...(limits.input.max != null ? {max: toApiAmount(limits.input.max)} : {})
359
+ },
360
+ output: {
361
+ min: toApiAmount(limits.output.min),
362
+ ...(limits.output.max != null ? {max: toApiAmount(limits.output.max)} : {})
363
+ }
364
+ };
365
+ }
366
+
367
+ private async parseAddress(input: ParseAddressInput): Promise<ParseAddressOutput> {
368
+ const result = await this.swapper.Utils.parseAddress(input.address);
369
+ if(result == null) throw new Error("Invalid address");
370
+
371
+ return {
372
+ address: result.address,
373
+ type: result.type,
374
+ ...(result.lnurl != null ? {lnurl: toApiLNURL(result.lnurl, this.swapper)} : {}),
375
+ ...(result.min != null ? {min: toApiAmount(result.min)} : {}),
376
+ ...(result.max != null ? {max: toApiAmount(result.max)} : {}),
377
+ ...(result.amount != null ? {amount: toApiAmount(result.amount)} : {})
378
+ };
379
+ }
380
+
381
+ private async getSpendableBalance(input: GetSpendableBalanceInput): Promise<GetSpendableBalanceOutput> {
382
+ const token = this.swapper.getToken(input.token);
383
+
384
+ if(token.chainId === "LIGHTNING")
385
+ throw new Error("Lightning wallet spendable balance is not supported by this endpoint.");
386
+
387
+ if(input.feeRate != null && input.feeMultiplier != null)
388
+ throw new Error("`feeMultiplier` cannot be specified alongside the `feeRate` parameter.");
389
+
390
+ if(token.chainId === "BITCOIN") {
391
+ if(input.targetChain != null && !this.swapper.getSmartChains().includes(input.targetChain as any)) {
392
+ throw new Error("Unknown targetChain: " + input.targetChain);
393
+ }
394
+
395
+ if (!this.swapper.Utils.isValidBitcoinAddress(input.wallet))
396
+ throw new Error(`Invalid BITCOIN wallet address: ` + input.wallet);
397
+
398
+ let btcFeeRate: number;
399
+ if(input.feeRate != null) {
400
+ btcFeeRate = parseFloat(input.feeRate);
401
+ if(isNaN(btcFeeRate) || btcFeeRate <= 0) throw new Error("Bitcoin `feeRate` must be a valid positive number!");
402
+ } else btcFeeRate = await this.swapper._bitcoinRpc.getFeeRate()
403
+ if(input.feeMultiplier != null) btcFeeRate *= input.feeMultiplier;
404
+
405
+ const {balance, feeRate} = await this.swapper.Utils.getBitcoinSpendableBalance(input.wallet, input.targetChain as any, {
406
+ gasDrop: input.gasDrop,
407
+ feeRate: btcFeeRate,
408
+ minFeeRate: input.minBitcoinFeeRate
409
+ });
410
+
411
+ return {
412
+ balance: toApiAmount(balance),
413
+ feeRate
414
+ };
415
+ }
416
+
417
+ if(input.gasDrop === true) throw new Error("`gasDrop` is only supported for Bitcoin balances.");
418
+ if(input.minBitcoinFeeRate != null) throw new Error("`minBitcoinFeeRate` is only supported for Bitcoin balances.");
419
+
420
+ if (!this.swapper.Utils.isValidSmartChainAddress(input.wallet, token.chainId))
421
+ throw new Error(`Invalid ${token.chainId} wallet address: ` + input.wallet);
422
+
423
+ const balance = await this.swapper.Utils.getSpendableBalance(input.wallet, token as any, {
424
+ feeMultiplier: input.feeMultiplier,
425
+ feeRate: input.feeRate
426
+ });
427
+
428
+ return {
429
+ balance: toApiAmount(balance)
430
+ };
431
+ }
432
+
433
+ private async getSwapStatus(input: GetSwapStatusInput): Promise<GetSwapStatusOutput> {
434
+ const swap = await this.swapper.getSwapById(input.swapId);
435
+ if (swap == null) {
436
+ throw new Error("Swap not found: " + input.swapId);
437
+ }
438
+
439
+ if (input.signer != null && !this.swapper.Utils.isValidSmartChainAddress(input.signer, swap.chainIdentifier)) {
440
+ throw new Error(`Invalid ${swap.chainIdentifier} signer address: ` + input.signer);
441
+ }
442
+
443
+ if (input.secret != null) {
444
+ try {
445
+ Buffer.from(input.secret, "hex");
446
+ } catch (e) {
447
+ throw new Error(`Invalid secret passed, has to be a hexadecimal string!`);
448
+ }
449
+ }
450
+
451
+ let bitcoinWallet: MinimalBitcoinWalletInterface | undefined;
452
+ if (input.bitcoinAddress != null && input.bitcoinPublicKey != null) {
453
+ bitcoinWallet = {
454
+ publicKey: input.bitcoinPublicKey,
455
+ address: input.bitcoinAddress
456
+ };
457
+ } else if(input.bitcoinAddress != null || input.bitcoinPublicKey != null) {
458
+ throw new Error("When specifying bitcoin wallet you have to pass both `bitcoinAddress` and `bitcoinPublicKey` params!");
459
+ }
460
+
461
+ if (input.bitcoinFeeRate != null) {
462
+ if(isNaN(input.bitcoinFeeRate)) throw new Error("Bitcoin fee rate passed cannot be NaN!");
463
+ if(input.bitcoinFeeRate <= 0) throw new Error("Bitcoin fee rate passed cannot be negative or 0!");
464
+ }
465
+
466
+ if(this.config?.syncOnGetStatus) await swap._sync(true);
467
+
468
+ const {steps, stateInfo, currentAction} = await swap.getExecutionStatus({
469
+ secret: input.secret,
470
+
471
+ bitcoinWallet,
472
+ bitcoinFeeRate: input.bitcoinFeeRate,
473
+
474
+ manualSettlementSmartChainSigner: input.signer,
475
+ refundSmartChainSigner: input.signer
476
+ });
477
+
478
+ return {
479
+ ...createListSwapOutput(swap, steps, stateInfo),
480
+
481
+ currentAction: currentAction ? await serializeAction(currentAction, this.txSerializer.bind(this)) : null,
482
+ requiresSecretReveal: requiresSecretRevealForApi(swap, stateInfo.state),
483
+
484
+ escrow: swap instanceof IEscrowSwap && swap._data!=null ? {
485
+ data: swap._data.getEscrowStruct(),
486
+ initTxId: swap._commitTxId
487
+ } : undefined
488
+ };
489
+ }
490
+
491
+ private async submitTransaction(input: SubmitTransactionInput, abortSignal?: AbortSignal): Promise<SubmitTransactionOutput> {
492
+ const swap = await this.swapper.getSwapById(input.swapId);
493
+ if (swap == null) {
494
+ throw new Error("Swap not found: " + input.swapId);
495
+ }
496
+
497
+ return {
498
+ txHashes: await swap._submitExecutionTransactions(input.signedTxs, abortSignal, undefined, this.config?.idempotentTxSubmission)
499
+ }
500
+ }
501
+
502
+ private async settleWithLnurl(input: SettleWithLnurlInput, abortSignal?: AbortSignal): Promise<SettleWithLnurlOutput> {
503
+ const swap = await this.swapper.getSwapById(input.swapId);
504
+ if (swap == null) throw new Error("Swap not found: " + input.swapId);
505
+
506
+ if (swap instanceof FromBTCLNAutoSwap) {
507
+ if(swap._state!==FromBTCLNAutoSwapState.PR_CREATED)
508
+ throw new Error("Invalid swap state, must be in PR_CREATED state!");
509
+ } else if (swap instanceof FromBTCLNSwap) {
510
+ if(swap._state!==FromBTCLNSwapState.PR_CREATED)
511
+ throw new Error("Invalid swap state, must be in PR_CREATED state!");
512
+ } else {
513
+ throw new Error("Endpoint only supports swaps from Lightning");
514
+ }
515
+
516
+ if (!swap.isLNURL()) {
517
+ if (input.lnurlWithdraw==null)
518
+ throw new Error("The swap is not configured to use LNURL, please pass the `lnurlWithdraw` parameter!");
519
+
520
+ if (!this.swapper.Utils.isValidLNURL(input.lnurlWithdraw))
521
+ throw new Error("Invalid LNURL-withdraw link provided: " + input.lnurlWithdraw);
522
+
523
+ await swap.settleWithLNURLWithdraw(input.lnurlWithdraw);
524
+ } else {
525
+ if (input.lnurlWithdraw!=null)
526
+ throw new Error("The swap is already configured with an LNURL link, don't pass the `lnurlWithdraw` parameter!");
527
+ }
528
+
529
+ let success: boolean;
530
+ if (swap instanceof FromBTCLNAutoSwap) {
531
+ // For non-legacy swap, we don't need to wait till the swap advances all the way to committed state
532
+ success = await swap._waitForLpPaymentReceived(2, abortSignal);
533
+ } else {
534
+ // For legacy swap waitForPayment waits just for the swap to transition into PR_PAID
535
+ success = await swap.waitForPayment(undefined, 2, abortSignal);
536
+ }
537
+
538
+ if(!success) throw new Error("Failed to settle the swap with the LNURL-withdraw link!");
539
+
540
+ return {
541
+ paymentHash: swap.getInputTxId()!
542
+ };
543
+ }
544
+
545
+ }
@@ -0,0 +1,5 @@
1
+ export {ApiAmount, ApiEndpoint, ApiLNURL, ApiLNURLPay, ApiLNURLWithdraw, ApiToken, InputSchema, InputSchemaField, toApiLNURL, toApiToken} from "./ApiTypes";
2
+ export {SerializedAction} from "./SerializedAction";
3
+ export * from "./SwapperApi";
4
+ export * from "./ApiEndpoints";
5
+ export {parseApiInput} from "./ApiParser";
@@ -136,13 +136,16 @@ export class UnifiedSwapEventListener<
136
136
 
137
137
  }
138
138
 
139
- listener?: EventListener<T["Data"]>;
140
- async start() {
139
+ private noAutomaticPoll?: boolean;
140
+ private listener?: EventListener<T["Data"]>;
141
+
142
+ async start(noAutomaticPoll?: boolean) {
141
143
  if(this.listener!=null) return;
142
144
  logger.info("start(): Starting unified swap event listener");
143
145
  await this.storage.init();
144
146
  logger.debug("start(): Storage initialized");
145
- await this.events.init();
147
+ await this.events.init(noAutomaticPoll);
148
+ this.noAutomaticPoll = noAutomaticPoll;
146
149
  logger.debug("start(): Events initialized");
147
150
  this.events.registerListener(this.listener = async (events) => {
148
151
  await this.processEvents(events);
@@ -151,6 +154,11 @@ export class UnifiedSwapEventListener<
151
154
  logger.info("start(): Successfully initiated the unified swap event listener!");
152
155
  }
153
156
 
157
+ poll(previousState: any): Promise<any> {
158
+ if(!this.noAutomaticPoll) throw new Error("Only supported when no automatic events polling is configured!");
159
+ return this.events.poll(previousState);
160
+ }
161
+
154
162
  stop(): Promise<void> {
155
163
  logger.info("stop(): Stopping unified swap event listener");
156
164
  if(this.listener!=null) this.events.unregisterListener(this.listener);
@@ -29,13 +29,15 @@ export function fetchWithTimeout(input: RequestInfo | URL, init: RequestInit & {
29
29
  * @param timeout Timeout (in milliseconds) for the request to conclude
30
30
  * @param abortSignal
31
31
  * @param allowNon200 Whether to allow non-200 status code HTTP responses
32
+ * @param headers
32
33
  * @throws {RequestError} if non 200 response code was returned or body cannot be parsed
33
34
  */
34
- export async function httpGet<T>(url: string, timeout?: number, abortSignal?: AbortSignal, allowNon200: boolean = false): Promise<T> {
35
+ export async function httpGet<T>(url: string, timeout?: number, abortSignal?: AbortSignal, allowNon200: boolean = false, headers: Record<string, string> = {}): Promise<T> {
35
36
  const init = {
36
37
  method: "GET",
37
38
  timeout,
38
- signal: abortSignal
39
+ signal: abortSignal,
40
+ headers
39
41
  };
40
42
 
41
43
  const response: Response = await fetchWithTimeout(url, init);
@@ -65,14 +67,18 @@ export async function httpGet<T>(url: string, timeout?: number, abortSignal?: Ab
65
67
  * @param body A HTTP request body to send to the server
66
68
  * @param timeout Timeout (in milliseconds) for the request to conclude
67
69
  * @param abortSignal
70
+ * @param headers
68
71
  * @throws {RequestError} if non 200 response code was returned
69
72
  */
70
- export async function httpPost<T>(url: string, body: any, timeout?: number, abortSignal?: AbortSignal): Promise<T> {
73
+ export async function httpPost<T>(url: string, body: any, timeout?: number, abortSignal?: AbortSignal, headers: Record<string, string> = {}): Promise<T> {
71
74
  const init = {
72
75
  method: "POST",
73
76
  timeout,
74
77
  body: JSON.stringify(body),
75
- headers: {'Content-Type': 'application/json'},
78
+ headers: {
79
+ ...headers,
80
+ 'Content-Type': 'application/json'
81
+ },
76
82
  signal: abortSignal
77
83
  };
78
84
 
@@ -46,6 +46,7 @@ logger.info("Environment supports request stream: "+supportsRequestStreams);
46
46
  * @param timeout Timeout in millseconds for the request to succeed & all its response properties to resolve
47
47
  * @param signal Abort signal
48
48
  * @param streamRequest Whether the request should be streamed or not
49
+ * @param _headers
49
50
  * @throws {RequestError} When the response code is not 200
50
51
  */
51
52
  export async function streamingFetchPromise<T extends RequestSchema>(
@@ -54,12 +55,13 @@ export async function streamingFetchPromise<T extends RequestSchema>(
54
55
  schema: T,
55
56
  timeout?: number,
56
57
  signal?: AbortSignal,
57
- streamRequest?: boolean
58
+ streamRequest?: boolean,
59
+ _headers: Record<string, string> = {}
58
60
  ): Promise<RequestSchemaResultPromise<T>> {
59
61
  if(streamRequest==null) streamRequest = supportsRequestStreams;
60
62
  if(timeout!=null) signal = timeoutSignal(timeout, new Error("Network request timed out"), signal);
61
63
 
62
- const headers: Record<string, string> = {};
64
+ const headers = {..._headers};
63
65
  const init: RequestInit = {
64
66
  method: "POST",
65
67
  headers
package/src/index.ts CHANGED
@@ -132,6 +132,7 @@ export * from "./types/SwapStateInfo";
132
132
  export * from "./types/AmountData";
133
133
  export * from "./types/CustomPriceFunction";
134
134
  export * from "./types/SwapExecutionAction";
135
+ export * from "./types/SwapExecutionStep";
135
136
  export * from "./types/SwapWithSigner";
136
137
  export * from "./types/Token";
137
138
  export * from "./types/TokenAmount";