@miradexio/client 0.1.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/LICENSE +21 -0
- package/README.md +405 -0
- package/dist/address/base58.d.ts +9 -0
- package/dist/address/base58.d.ts.map +1 -0
- package/dist/address/base58.js +33 -0
- package/dist/address/base58.js.map +1 -0
- package/dist/address/bech32.d.ts +13 -0
- package/dist/address/bech32.d.ts.map +1 -0
- package/dist/address/bech32.js +41 -0
- package/dist/address/bech32.js.map +1 -0
- package/dist/address/evm.d.ts +5 -0
- package/dist/address/evm.d.ts.map +1 -0
- package/dist/address/evm.js +27 -0
- package/dist/address/evm.js.map +1 -0
- package/dist/address/index.d.ts +15 -0
- package/dist/address/index.d.ts.map +1 -0
- package/dist/address/index.js +134 -0
- package/dist/address/index.js.map +1 -0
- package/dist/address/monero.d.ts +15 -0
- package/dist/address/monero.d.ts.map +1 -0
- package/dist/address/monero.js +30 -0
- package/dist/address/monero.js.map +1 -0
- package/dist/address/polkadot.d.ts +10 -0
- package/dist/address/polkadot.d.ts.map +1 -0
- package/dist/address/polkadot.js +36 -0
- package/dist/address/polkadot.js.map +1 -0
- package/dist/address/solana.d.ts +5 -0
- package/dist/address/solana.d.ts.map +1 -0
- package/dist/address/solana.js +17 -0
- package/dist/address/solana.js.map +1 -0
- package/dist/address/ton.d.ts +11 -0
- package/dist/address/ton.d.ts.map +1 -0
- package/dist/address/ton.js +28 -0
- package/dist/address/ton.js.map +1 -0
- package/dist/api/index.d.ts +80 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +213 -0
- package/dist/api/index.js.map +1 -0
- package/dist/atomic-swap/drive.d.ts +22 -0
- package/dist/atomic-swap/drive.d.ts.map +1 -0
- package/dist/atomic-swap/drive.js +713 -0
- package/dist/atomic-swap/drive.js.map +1 -0
- package/dist/atomic-swap/extract.d.ts +46 -0
- package/dist/atomic-swap/extract.d.ts.map +1 -0
- package/dist/atomic-swap/extract.js +55 -0
- package/dist/atomic-swap/extract.js.map +1 -0
- package/dist/atomic-swap/index.d.ts +15 -0
- package/dist/atomic-swap/index.d.ts.map +1 -0
- package/dist/atomic-swap/index.js +13 -0
- package/dist/atomic-swap/index.js.map +1 -0
- package/dist/atomic-swap/monero-sweep/errors.d.ts +2 -0
- package/dist/atomic-swap/monero-sweep/errors.d.ts.map +1 -0
- package/dist/atomic-swap/monero-sweep/errors.js +43 -0
- package/dist/atomic-swap/monero-sweep/errors.js.map +1 -0
- package/dist/atomic-swap/monero-sweep/index.d.ts +33 -0
- package/dist/atomic-swap/monero-sweep/index.d.ts.map +1 -0
- package/dist/atomic-swap/monero-sweep/index.js +415 -0
- package/dist/atomic-swap/monero-sweep/index.js.map +1 -0
- package/dist/atomic-swap/monero-sweep/ring-select.d.ts +12 -0
- package/dist/atomic-swap/monero-sweep/ring-select.d.ts.map +1 -0
- package/dist/atomic-swap/monero-sweep/ring-select.js +61 -0
- package/dist/atomic-swap/monero-sweep/ring-select.js.map +1 -0
- package/dist/atomic-swap/presign.d.ts +101 -0
- package/dist/atomic-swap/presign.d.ts.map +1 -0
- package/dist/atomic-swap/presign.js +460 -0
- package/dist/atomic-swap/presign.js.map +1 -0
- package/dist/atomic-swap/refund.d.ts +72 -0
- package/dist/atomic-swap/refund.d.ts.map +1 -0
- package/dist/atomic-swap/refund.js +224 -0
- package/dist/atomic-swap/refund.js.map +1 -0
- package/dist/atomic-swap/run.d.ts +27 -0
- package/dist/atomic-swap/run.d.ts.map +1 -0
- package/dist/atomic-swap/run.js +282 -0
- package/dist/atomic-swap/run.js.map +1 -0
- package/dist/atomic-swap/snapshot.d.ts +111 -0
- package/dist/atomic-swap/snapshot.d.ts.map +1 -0
- package/dist/atomic-swap/snapshot.js +69 -0
- package/dist/atomic-swap/snapshot.js.map +1 -0
- package/dist/atomic-swap/submit-encsig.d.ts +10 -0
- package/dist/atomic-swap/submit-encsig.d.ts.map +1 -0
- package/dist/atomic-swap/submit-encsig.js +56 -0
- package/dist/atomic-swap/submit-encsig.js.map +1 -0
- package/dist/atomic-swap/types.d.ts +168 -0
- package/dist/atomic-swap/types.d.ts.map +1 -0
- package/dist/atomic-swap/types.js +5 -0
- package/dist/atomic-swap/types.js.map +1 -0
- package/dist/blockchain/quorum-provider.d.ts +25 -0
- package/dist/blockchain/quorum-provider.d.ts.map +1 -0
- package/dist/blockchain/quorum-provider.js +144 -0
- package/dist/blockchain/quorum-provider.js.map +1 -0
- package/dist/cooperative-redeem.d.ts +23 -0
- package/dist/cooperative-redeem.d.ts.map +1 -0
- package/dist/cooperative-redeem.js +40 -0
- package/dist/cooperative-redeem.js.map +1 -0
- package/dist/engine/blockchain-querier.d.ts +34 -0
- package/dist/engine/blockchain-querier.d.ts.map +1 -0
- package/dist/engine/blockchain-querier.js +2 -0
- package/dist/engine/blockchain-querier.js.map +1 -0
- package/dist/engine/engine-state.d.ts +20 -0
- package/dist/engine/engine-state.d.ts.map +1 -0
- package/dist/engine/engine-state.js +9 -0
- package/dist/engine/engine-state.js.map +1 -0
- package/dist/engine/flow-context.d.ts +494 -0
- package/dist/engine/flow-context.d.ts.map +1 -0
- package/dist/engine/flow-context.js +147 -0
- package/dist/engine/flow-context.js.map +1 -0
- package/dist/engine/flows/atomic-flow-state.d.ts +124 -0
- package/dist/engine/flows/atomic-flow-state.d.ts.map +1 -0
- package/dist/engine/flows/atomic-flow-state.js +2 -0
- package/dist/engine/flows/atomic-flow-state.js.map +1 -0
- package/dist/engine/flows/atomic-flow.d.ts +88 -0
- package/dist/engine/flows/atomic-flow.d.ts.map +1 -0
- package/dist/engine/flows/atomic-flow.js +1192 -0
- package/dist/engine/flows/atomic-flow.js.map +1 -0
- package/dist/engine/flows/history-flow-state.d.ts +19 -0
- package/dist/engine/flows/history-flow-state.d.ts.map +1 -0
- package/dist/engine/flows/history-flow-state.js +2 -0
- package/dist/engine/flows/history-flow-state.js.map +1 -0
- package/dist/engine/flows/providers-flow-state.d.ts +14 -0
- package/dist/engine/flows/providers-flow-state.d.ts.map +1 -0
- package/dist/engine/flows/providers-flow-state.js +2 -0
- package/dist/engine/flows/providers-flow-state.js.map +1 -0
- package/dist/engine/flows/quote-flow-state.d.ts +20 -0
- package/dist/engine/flows/quote-flow-state.d.ts.map +1 -0
- package/dist/engine/flows/quote-flow-state.js +2 -0
- package/dist/engine/flows/quote-flow-state.js.map +1 -0
- package/dist/engine/flows/swap-flow-state.d.ts +155 -0
- package/dist/engine/flows/swap-flow-state.d.ts.map +1 -0
- package/dist/engine/flows/swap-flow-state.js +2 -0
- package/dist/engine/flows/swap-flow-state.js.map +1 -0
- package/dist/engine/flows/swap-flow.d.ts +81 -0
- package/dist/engine/flows/swap-flow.d.ts.map +1 -0
- package/dist/engine/flows/swap-flow.js +720 -0
- package/dist/engine/flows/swap-flow.js.map +1 -0
- package/dist/engine/flows/tokens-flow-state.d.ts +16 -0
- package/dist/engine/flows/tokens-flow-state.d.ts.map +1 -0
- package/dist/engine/flows/tokens-flow-state.js +2 -0
- package/dist/engine/flows/tokens-flow-state.js.map +1 -0
- package/dist/engine/miradex-engine.d.ts +152 -0
- package/dist/engine/miradex-engine.d.ts.map +1 -0
- package/dist/engine/miradex-engine.js +278 -0
- package/dist/engine/miradex-engine.js.map +1 -0
- package/dist/engine/pipeline.d.ts +27 -0
- package/dist/engine/pipeline.d.ts.map +1 -0
- package/dist/engine/pipeline.js +166 -0
- package/dist/engine/pipeline.js.map +1 -0
- package/dist/engine/platform.d.ts +220 -0
- package/dist/engine/platform.d.ts.map +1 -0
- package/dist/engine/platform.js +2 -0
- package/dist/engine/platform.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +62 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/blockchain.d.ts +33 -0
- package/dist/interfaces/blockchain.d.ts.map +1 -0
- package/dist/interfaces/blockchain.js +2 -0
- package/dist/interfaces/blockchain.js.map +1 -0
- package/dist/interfaces/logger.d.ts +14 -0
- package/dist/interfaces/logger.d.ts.map +1 -0
- package/dist/interfaces/logger.js +7 -0
- package/dist/interfaces/logger.js.map +1 -0
- package/dist/lib/bitcoin/deposit-watcher.d.ts +66 -0
- package/dist/lib/bitcoin/deposit-watcher.d.ts.map +1 -0
- package/dist/lib/bitcoin/deposit-watcher.js +218 -0
- package/dist/lib/bitcoin/deposit-watcher.js.map +1 -0
- package/dist/lib/bitcoin/script-hash.d.ts +8 -0
- package/dist/lib/bitcoin/script-hash.d.ts.map +1 -0
- package/dist/lib/bitcoin/script-hash.js +29 -0
- package/dist/lib/bitcoin/script-hash.js.map +1 -0
- package/dist/lib/bitcoin/sweep.d.ts +56 -0
- package/dist/lib/bitcoin/sweep.d.ts.map +1 -0
- package/dist/lib/bitcoin/sweep.js +185 -0
- package/dist/lib/bitcoin/sweep.js.map +1 -0
- package/dist/lib/bitcoin/tx-verify.d.ts +43 -0
- package/dist/lib/bitcoin/tx-verify.d.ts.map +1 -0
- package/dist/lib/bitcoin/tx-verify.js +202 -0
- package/dist/lib/bitcoin/tx-verify.js.map +1 -0
- package/dist/lib/bitcoin/wallet.d.ts +71 -0
- package/dist/lib/bitcoin/wallet.d.ts.map +1 -0
- package/dist/lib/bitcoin/wallet.js +141 -0
- package/dist/lib/bitcoin/wallet.js.map +1 -0
- package/dist/lib/crypto/bytes.d.ts +21 -0
- package/dist/lib/crypto/bytes.d.ts.map +1 -0
- package/dist/lib/crypto/bytes.js +39 -0
- package/dist/lib/crypto/bytes.js.map +1 -0
- package/dist/lib/crypto/errors.d.ts +12 -0
- package/dist/lib/crypto/errors.d.ts.map +1 -0
- package/dist/lib/crypto/errors.js +16 -0
- package/dist/lib/crypto/errors.js.map +1 -0
- package/dist/lib/crypto/keygen.d.ts +19 -0
- package/dist/lib/crypto/keygen.d.ts.map +1 -0
- package/dist/lib/crypto/keygen.js +24 -0
- package/dist/lib/crypto/keygen.js.map +1 -0
- package/dist/lib/crypto/libp2p-identity.d.ts +25 -0
- package/dist/lib/crypto/libp2p-identity.d.ts.map +1 -0
- package/dist/lib/crypto/libp2p-identity.js +80 -0
- package/dist/lib/crypto/libp2p-identity.js.map +1 -0
- package/dist/lib/crypto/mnemonic.d.ts +28 -0
- package/dist/lib/crypto/mnemonic.d.ts.map +1 -0
- package/dist/lib/crypto/mnemonic.js +97 -0
- package/dist/lib/crypto/mnemonic.js.map +1 -0
- package/dist/lib/crypto/platform.d.ts +10 -0
- package/dist/lib/crypto/platform.d.ts.map +1 -0
- package/dist/lib/crypto/platform.js +38 -0
- package/dist/lib/crypto/platform.js.map +1 -0
- package/dist/lib/crypto/scalars.d.ts +33 -0
- package/dist/lib/crypto/scalars.d.ts.map +1 -0
- package/dist/lib/crypto/scalars.js +81 -0
- package/dist/lib/crypto/scalars.js.map +1 -0
- package/dist/lib/crypto/types.d.ts +24 -0
- package/dist/lib/crypto/types.d.ts.map +1 -0
- package/dist/lib/crypto/types.js +2 -0
- package/dist/lib/crypto/types.js.map +1 -0
- package/dist/lib/crypto/wasm.d.ts +51 -0
- package/dist/lib/crypto/wasm.d.ts.map +1 -0
- package/dist/lib/crypto/wasm.js +192 -0
- package/dist/lib/crypto/wasm.js.map +1 -0
- package/dist/lib/default-config.d.ts +140 -0
- package/dist/lib/default-config.d.ts.map +1 -0
- package/dist/lib/default-config.js +239 -0
- package/dist/lib/default-config.js.map +1 -0
- package/dist/lib/delay.d.ts +6 -0
- package/dist/lib/delay.d.ts.map +1 -0
- package/dist/lib/delay.js +20 -0
- package/dist/lib/delay.js.map +1 -0
- package/dist/lib/errors.d.ts +90 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +129 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +43 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/keystore.d.ts +85 -0
- package/dist/lib/keystore.d.ts.map +1 -0
- package/dist/lib/keystore.js +105 -0
- package/dist/lib/keystore.js.map +1 -0
- package/dist/lib/monero/output-scanner.d.ts +53 -0
- package/dist/lib/monero/output-scanner.d.ts.map +1 -0
- package/dist/lib/monero/output-scanner.js +180 -0
- package/dist/lib/monero/output-scanner.js.map +1 -0
- package/dist/lib/monero/rpc.d.ts +131 -0
- package/dist/lib/monero/rpc.d.ts.map +1 -0
- package/dist/lib/monero/rpc.js +267 -0
- package/dist/lib/monero/rpc.js.map +1 -0
- package/dist/lib/monero/verify-lock.d.ts +50 -0
- package/dist/lib/monero/verify-lock.d.ts.map +1 -0
- package/dist/lib/monero/verify-lock.js +161 -0
- package/dist/lib/monero/verify-lock.js.map +1 -0
- package/dist/lib/monero/verify-sweep.d.ts +59 -0
- package/dist/lib/monero/verify-sweep.d.ts.map +1 -0
- package/dist/lib/monero/verify-sweep.js +82 -0
- package/dist/lib/monero/verify-sweep.js.map +1 -0
- package/dist/lib/monero/wasm.d.ts +19 -0
- package/dist/lib/monero/wasm.d.ts.map +1 -0
- package/dist/lib/monero/wasm.js +24 -0
- package/dist/lib/monero/wasm.js.map +1 -0
- package/dist/lib/pow-solver.d.ts +4 -0
- package/dist/lib/pow-solver.d.ts.map +1 -0
- package/dist/lib/pow-solver.js +37 -0
- package/dist/lib/pow-solver.js.map +1 -0
- package/dist/lib/retry.d.ts +86 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/lib/retry.js +104 -0
- package/dist/lib/retry.js.map +1 -0
- package/dist/portable.d.ts +23 -0
- package/dist/portable.d.ts.map +1 -0
- package/dist/portable.js +13 -0
- package/dist/portable.js.map +1 -0
- package/dist/quote-binding.d.ts +31 -0
- package/dist/quote-binding.d.ts.map +1 -0
- package/dist/quote-binding.js +40 -0
- package/dist/quote-binding.js.map +1 -0
- package/dist/swap-executor.d.ts +51 -0
- package/dist/swap-executor.d.ts.map +1 -0
- package/dist/swap-executor.js +138 -0
- package/dist/swap-executor.js.map +1 -0
- package/dist/types/api.d.ts +34 -0
- package/dist/types/api.d.ts.map +1 -0
- package/dist/types/api.js +18 -0
- package/dist/types/api.js.map +1 -0
- package/dist/types/errors.d.ts +94 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +93 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +10 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/keys.d.ts +33 -0
- package/dist/types/keys.d.ts.map +1 -0
- package/dist/types/keys.js +2 -0
- package/dist/types/keys.js.map +1 -0
- package/dist/types/protocol.d.ts +93 -0
- package/dist/types/protocol.d.ts.map +1 -0
- package/dist/types/protocol.js +18 -0
- package/dist/types/protocol.js.map +1 -0
- package/dist/types/status.d.ts +3 -0
- package/dist/types/status.d.ts.map +1 -0
- package/dist/types/status.js +23 -0
- package/dist/types/status.js.map +1 -0
- package/dist/types/verification.d.ts +49 -0
- package/dist/types/verification.d.ts.map +1 -0
- package/dist/types/verification.js +34 -0
- package/dist/types/verification.js.map +1 -0
- package/dist/verification/atomic-swap.d.ts +9 -0
- package/dist/verification/atomic-swap.d.ts.map +1 -0
- package/dist/verification/atomic-swap.js +22 -0
- package/dist/verification/atomic-swap.js.map +1 -0
- package/dist/verification/chainflip-networks.d.ts +46 -0
- package/dist/verification/chainflip-networks.d.ts.map +1 -0
- package/dist/verification/chainflip-networks.js +24 -0
- package/dist/verification/chainflip-networks.js.map +1 -0
- package/dist/verification/chainflip.d.ts +61 -0
- package/dist/verification/chainflip.d.ts.map +1 -0
- package/dist/verification/chainflip.js +377 -0
- package/dist/verification/chainflip.js.map +1 -0
- package/dist/verification/constants.d.ts +52 -0
- package/dist/verification/constants.d.ts.map +1 -0
- package/dist/verification/constants.js +54 -0
- package/dist/verification/constants.js.map +1 -0
- package/dist/verification/index.d.ts +71 -0
- package/dist/verification/index.d.ts.map +1 -0
- package/dist/verification/index.js +91 -0
- package/dist/verification/index.js.map +1 -0
- package/dist/verification/memo.d.ts +27 -0
- package/dist/verification/memo.d.ts.map +1 -0
- package/dist/verification/memo.js +52 -0
- package/dist/verification/memo.js.map +1 -0
- package/dist/verification/near-intents.d.ts +91 -0
- package/dist/verification/near-intents.d.ts.map +1 -0
- package/dist/verification/near-intents.js +213 -0
- package/dist/verification/near-intents.js.map +1 -0
- package/dist/verification/rate-oracle.d.ts +32 -0
- package/dist/verification/rate-oracle.d.ts.map +1 -0
- package/dist/verification/rate-oracle.js +43 -0
- package/dist/verification/rate-oracle.js.map +1 -0
- package/dist/verification/shared.d.ts +20 -0
- package/dist/verification/shared.d.ts.map +1 -0
- package/dist/verification/shared.js +25 -0
- package/dist/verification/shared.js.map +1 -0
- package/dist/verification/thorchain-networks.d.ts +35 -0
- package/dist/verification/thorchain-networks.d.ts.map +1 -0
- package/dist/verification/thorchain-networks.js +35 -0
- package/dist/verification/thorchain-networks.js.map +1 -0
- package/dist/verification/thorchain.d.ts +55 -0
- package/dist/verification/thorchain.d.ts.map +1 -0
- package/dist/verification/thorchain.js +232 -0
- package/dist/verification/thorchain.js.map +1 -0
- package/dist/wasm-pins.d.ts +4 -0
- package/dist/wasm-pins.d.ts.map +1 -0
- package/dist/wasm-pins.js +6 -0
- package/dist/wasm-pins.js.map +1 -0
- package/dist/wire/chainflip.zod.d.ts +144 -0
- package/dist/wire/chainflip.zod.d.ts.map +1 -0
- package/dist/wire/chainflip.zod.js +33 -0
- package/dist/wire/chainflip.zod.js.map +1 -0
- package/dist/wire/near-intents.zod.d.ts +376 -0
- package/dist/wire/near-intents.zod.d.ts.map +1 -0
- package/dist/wire/near-intents.zod.js +101 -0
- package/dist/wire/near-intents.zod.js.map +1 -0
- package/dist/wire/server/action.zod.d.ts +1119 -0
- package/dist/wire/server/action.zod.d.ts.map +1 -0
- package/dist/wire/server/action.zod.js +173 -0
- package/dist/wire/server/action.zod.js.map +1 -0
- package/dist/wire/server/common.zod.d.ts +62 -0
- package/dist/wire/server/common.zod.d.ts.map +1 -0
- package/dist/wire/server/common.zod.js +43 -0
- package/dist/wire/server/common.zod.js.map +1 -0
- package/dist/wire/server/index.d.ts +8 -0
- package/dist/wire/server/index.d.ts.map +1 -0
- package/dist/wire/server/index.js +8 -0
- package/dist/wire/server/index.js.map +1 -0
- package/dist/wire/server/pow.zod.d.ts +45 -0
- package/dist/wire/server/pow.zod.d.ts.map +1 -0
- package/dist/wire/server/pow.zod.js +18 -0
- package/dist/wire/server/pow.zod.js.map +1 -0
- package/dist/wire/server/quotes.zod.d.ts +694 -0
- package/dist/wire/server/quotes.zod.d.ts.map +1 -0
- package/dist/wire/server/quotes.zod.js +103 -0
- package/dist/wire/server/quotes.zod.js.map +1 -0
- package/dist/wire/server/swap.zod.d.ts +1981 -0
- package/dist/wire/server/swap.zod.d.ts.map +1 -0
- package/dist/wire/server/swap.zod.js +270 -0
- package/dist/wire/server/swap.zod.js.map +1 -0
- package/dist/wire/server/tokens.zod.d.ts +93 -0
- package/dist/wire/server/tokens.zod.d.ts.map +1 -0
- package/dist/wire/server/tokens.zod.js +28 -0
- package/dist/wire/server/tokens.zod.js.map +1 -0
- package/dist/wire/server/verify.zod.d.ts +30 -0
- package/dist/wire/server/verify.zod.d.ts.map +1 -0
- package/dist/wire/server/verify.zod.js +12 -0
- package/dist/wire/server/verify.zod.js.map +1 -0
- package/dist/wire/thorchain.zod.d.ts +224 -0
- package/dist/wire/thorchain.zod.d.ts.map +1 -0
- package/dist/wire/thorchain.zod.js +51 -0
- package/dist/wire/thorchain.zod.js.map +1 -0
- package/package.json +128 -0
- package/wasm/miradex-rust/README.md +74 -0
- package/wasm/miradex-rust/miradex_rust.d.ts +149 -0
- package/wasm/miradex-rust/miradex_rust.js +943 -0
- package/wasm/miradex-rust/miradex_rust_bg.wasm +0 -0
- package/wasm/miradex-rust/miradex_rust_bg.wasm.d.ts +31 -0
- package/wasm/miradex-rust/package.json +24 -0
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
import { ApiError, NetworkError } from '../../api/index.js';
|
|
2
|
+
import { TERMINAL_STATUSES, ProtocolError } from '../../types/index.js';
|
|
3
|
+
import { createFlowContext, mergeFlowContext, validatePopulated, validateVerified, } from '../flow-context.js';
|
|
4
|
+
import { resumeAtomicSwap as coreResumeAtomicSwap, SwapCancelledError, } from '../../atomic-swap/index.js';
|
|
5
|
+
import { generateMnemonicKeys } from '../../lib/crypto/mnemonic.js';
|
|
6
|
+
import { generateClientKeysFromSeed, ensureWasm } from '../../lib/crypto/wasm.js';
|
|
7
|
+
import { walletFromWif } from '../../lib/bitcoin/wallet.js';
|
|
8
|
+
import { createKeystore } from '../../lib/keystore.js';
|
|
9
|
+
import { deriveLibp2pIdentity } from '../../lib/crypto/libp2p-identity.js';
|
|
10
|
+
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
|
|
11
|
+
import { sweepMonero } from '../../atomic-swap/monero-sweep/index.js';
|
|
12
|
+
import { delay } from '../../lib/delay.js';
|
|
13
|
+
import { discoverAndVerifyTxCancel } from '../../lib/bitcoin/tx-verify.js';
|
|
14
|
+
import { buildFullRefund, buildPartialRefund, signRefund, } from '../../atomic-swap/refund.js';
|
|
15
|
+
import { buildMultisigWitnessScript } from '../../atomic-swap/presign.js';
|
|
16
|
+
import { extractProtocolData } from '../../atomic-swap/extract.js';
|
|
17
|
+
const DEFAULT_POLL_MS = 5_000;
|
|
18
|
+
const MAX_TRANSIENT_RETRIES = 5;
|
|
19
|
+
function isTransientError(err) {
|
|
20
|
+
if (err instanceof NetworkError)
|
|
21
|
+
return true;
|
|
22
|
+
if (err instanceof ApiError) {
|
|
23
|
+
if (err.statusCode >= 500)
|
|
24
|
+
return true;
|
|
25
|
+
if (err.statusCode === 429)
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* True iff the protocol params carry enough material for the client to
|
|
32
|
+
* construct a refund without sidecar cooperation. Two valid shapes:
|
|
33
|
+
* - Legacy/Full: `tx_full_refund_encsig` alone is enough — the client
|
|
34
|
+
* builds and broadcasts TxFullRefund spending TxCancel.
|
|
35
|
+
* - Partial (amnesty): `tx_partial_refund_encsig` plus the amnesty
|
|
36
|
+
* triple (`amnesty_amount_sats`, `tx_partial_refund_fee_sats`) drive
|
|
37
|
+
* the partial-refund branch. The amnesty output stays at multisig
|
|
38
|
+
* until a separate TxReclaim phase (not yet implemented client-side).
|
|
39
|
+
*/
|
|
40
|
+
function hasRefundEscapeHatch(params) {
|
|
41
|
+
if (params.tx_full_refund_encsig)
|
|
42
|
+
return true;
|
|
43
|
+
return (!!params.tx_partial_refund_encsig &&
|
|
44
|
+
params.amnesty_amount_sats !== undefined &&
|
|
45
|
+
params.amnesty_amount_sats !== null &&
|
|
46
|
+
params.tx_partial_refund_fee_sats !== undefined &&
|
|
47
|
+
params.tx_partial_refund_fee_sats !== null);
|
|
48
|
+
}
|
|
49
|
+
export class AtomicFlow {
|
|
50
|
+
api;
|
|
51
|
+
platform;
|
|
52
|
+
config;
|
|
53
|
+
emitFn;
|
|
54
|
+
abortController = null;
|
|
55
|
+
userActionResolver = null;
|
|
56
|
+
keystore = null;
|
|
57
|
+
deposit = null;
|
|
58
|
+
keystoreId = '';
|
|
59
|
+
lastEmittedState = { phase: 'idle', snapshot: null };
|
|
60
|
+
flowCtx = null;
|
|
61
|
+
lastProgressKey = null;
|
|
62
|
+
pollMs;
|
|
63
|
+
/**
|
|
64
|
+
* True once the driver has emitted a terminal phase (`completed`,
|
|
65
|
+
* `failed`, `refunded`, etc.). Used to suppress the post-driver
|
|
66
|
+
* `requiredAction` re-check that would otherwise trigger a phantom
|
|
67
|
+
* second sweep — the server has no scanner for the on-chain XMR sweep,
|
|
68
|
+
* so a fresh `getSwapDetail` right after the driver completes still
|
|
69
|
+
* reports `requiredAction.type === 'sweep'`.
|
|
70
|
+
*/
|
|
71
|
+
hasReachedTerminal = false;
|
|
72
|
+
/**
|
|
73
|
+
* Optional maker pin forwarded to `coreResumeAtomicSwap` via
|
|
74
|
+
* `params.variantId`. Set by `start()` from `StartAtomicSwapParams`;
|
|
75
|
+
* unused (and irrelevant) on the resume path because the maker is
|
|
76
|
+
* already chosen at that point.
|
|
77
|
+
*/
|
|
78
|
+
variantId;
|
|
79
|
+
constructor(api, platform, config, emitFn, options) {
|
|
80
|
+
this.api = api;
|
|
81
|
+
this.platform = platform;
|
|
82
|
+
this.config = config;
|
|
83
|
+
this.emitFn = emitFn;
|
|
84
|
+
this.pollMs = options?.pollMs ?? DEFAULT_POLL_MS;
|
|
85
|
+
}
|
|
86
|
+
get signal() {
|
|
87
|
+
return this.abortController?.signal ?? AbortSignal.abort();
|
|
88
|
+
}
|
|
89
|
+
get logger() {
|
|
90
|
+
return this.platform.logger;
|
|
91
|
+
}
|
|
92
|
+
setFlowContext(partial) {
|
|
93
|
+
this.flowCtx = this.flowCtx
|
|
94
|
+
? mergeFlowContext(this.flowCtx, partial)
|
|
95
|
+
: createFlowContext(partial);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validate FlowContext as PopulatedFlowContext.
|
|
99
|
+
* On failure, emits a 'failed' phase with structured error and returns null.
|
|
100
|
+
*/
|
|
101
|
+
requirePopulated(phase) {
|
|
102
|
+
if (!this.flowCtx) {
|
|
103
|
+
this.emitError(phase, 'FlowContext not initialized');
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const result = validatePopulated(this.flowCtx, phase);
|
|
107
|
+
if (result.ok)
|
|
108
|
+
return result.data;
|
|
109
|
+
this.logger.error({ fields: result.error.fields }, result.error.message);
|
|
110
|
+
this.emitError(phase, result.error.message);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate FlowContext as VerifiedFlowContext.
|
|
115
|
+
* On failure, emits a 'failed' phase with structured error and returns null.
|
|
116
|
+
*/
|
|
117
|
+
requireVerified(phase) {
|
|
118
|
+
if (!this.flowCtx) {
|
|
119
|
+
this.emitError(phase, 'FlowContext not initialized');
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const result = validateVerified(this.flowCtx, phase);
|
|
123
|
+
if (result.ok)
|
|
124
|
+
return result.data;
|
|
125
|
+
this.logger.error({ fields: result.error.fields }, result.error.message);
|
|
126
|
+
this.emitError(phase, result.error.message);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
emitError(phase, message) {
|
|
130
|
+
this.transition({
|
|
131
|
+
phase: 'failed',
|
|
132
|
+
snapshot: this.flowCtx,
|
|
133
|
+
error: `[${phase}] ${message}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
transition(state) {
|
|
137
|
+
const prevPhase = this.lastEmittedState.phase;
|
|
138
|
+
if (state.phase === prevPhase)
|
|
139
|
+
return;
|
|
140
|
+
this.logger.info({ phase: state.phase, prevPhase, swapId: this.flowCtx?.swapId ?? null }, 'Atomic phase transition');
|
|
141
|
+
this.lastEmittedState = state;
|
|
142
|
+
this.emitFn(state);
|
|
143
|
+
}
|
|
144
|
+
cancel() {
|
|
145
|
+
this.abortController?.abort();
|
|
146
|
+
this.userActionResolver?.();
|
|
147
|
+
this.userActionResolver = null;
|
|
148
|
+
}
|
|
149
|
+
async start(params) {
|
|
150
|
+
this.abortController = new AbortController();
|
|
151
|
+
this.hasReachedTerminal = false;
|
|
152
|
+
this.variantId = params.variantId;
|
|
153
|
+
this.logger.info({
|
|
154
|
+
destAddress: params.destAddress,
|
|
155
|
+
amount: params.amount,
|
|
156
|
+
variantId: params.variantId ?? null,
|
|
157
|
+
existingKeystoreId: params.existingKeystoreId ?? null,
|
|
158
|
+
}, 'AtomicFlow.start()');
|
|
159
|
+
try {
|
|
160
|
+
this.setFlowContext({
|
|
161
|
+
fromToken: 'BTC',
|
|
162
|
+
toToken: 'XMR',
|
|
163
|
+
destAddress: params.destAddress,
|
|
164
|
+
refundAddress: params.refundAddress,
|
|
165
|
+
provider: 'atomicswap',
|
|
166
|
+
extra: { text: 'Initializing keygen...', type: 'message' },
|
|
167
|
+
});
|
|
168
|
+
this.transition({ phase: 'keygen', snapshot: this.flowCtx, message: 'Initializing keygen...' });
|
|
169
|
+
await ensureWasm();
|
|
170
|
+
const network = this.config.network;
|
|
171
|
+
// Either reuse an existing keystore (re-quote-after-failure path) or
|
|
172
|
+
// generate a fresh one (default path). In both cases the rest of the
|
|
173
|
+
// flow is identical from this point forward — same deposit polling,
|
|
174
|
+
// same swap creation, same drive loop.
|
|
175
|
+
let wallet;
|
|
176
|
+
if (params.existingKeystoreId !== undefined) {
|
|
177
|
+
this.setFlowContext({ extra: { text: 'Loading keystore...', type: 'message' } });
|
|
178
|
+
this.transition({ phase: 'keygen', snapshot: this.flowCtx, message: 'Loading keystore...' });
|
|
179
|
+
// Trust: the keystore was created here originally and its keys
|
|
180
|
+
// already passed `verifyKeys`. Skipping the API call avoids
|
|
181
|
+
// doubling the call's transient-failure surface for a no-op check.
|
|
182
|
+
this.keystore = await this.platform.loadKeystore(params.existingKeystoreId);
|
|
183
|
+
this.keystoreId = params.existingKeystoreId;
|
|
184
|
+
wallet = walletFromWif(this.keystore.btc.wif, network);
|
|
185
|
+
this.logger.info({ keystoreId: this.keystoreId, btcAddress: wallet.address }, 'Reusing existing keystore — skipping keygen + saveKeystore');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.setFlowContext({ extra: { text: 'Generating swap keys...', type: 'message' } });
|
|
189
|
+
this.transition({ phase: 'keygen', snapshot: this.flowCtx, message: 'Generating swap keys...' });
|
|
190
|
+
const mnemonicKeys = generateMnemonicKeys(network);
|
|
191
|
+
const keys = generateClientKeysFromSeed(mnemonicKeys.s_b_seed, mnemonicKeys.v_b_seed, mnemonicKeys.b_seed);
|
|
192
|
+
wallet = walletFromWif(mnemonicKeys.wif, network);
|
|
193
|
+
this.logger.debug({ operation: 'keygen' }, 'Keys generated');
|
|
194
|
+
const keyCheck = await this.api.verifyKeys({
|
|
195
|
+
s_b_bitcoin: keys.s_b_bitcoin, s_b_monero: keys.s_b_monero,
|
|
196
|
+
dleq_proof: keys.dleq_proof, v_b: keys.v_b,
|
|
197
|
+
});
|
|
198
|
+
this.logger.info({ valid: keyCheck.valid }, 'Key verification result');
|
|
199
|
+
if (!keyCheck.valid) {
|
|
200
|
+
throw new Error(`Key verification failed: ${keyCheck.reason}. DO NOT DEPOSIT.`);
|
|
201
|
+
}
|
|
202
|
+
this.checkAborted();
|
|
203
|
+
const masterSeedHex = bytesToHex(randomBytes(32));
|
|
204
|
+
const libp2pIdentity = await deriveLibp2pIdentity(masterSeedHex);
|
|
205
|
+
this.keystore = createKeystore({
|
|
206
|
+
wif: wallet.wif, btcAddress: wallet.address, network,
|
|
207
|
+
s_b: keys.s_b, v_b: keys.v_b, S_b_bitcoin: keys.s_b_bitcoin,
|
|
208
|
+
S_b_monero: keys.s_b_monero, dleq_proof: keys.dleq_proof,
|
|
209
|
+
b: keys.b, B: keys.B,
|
|
210
|
+
eigenwallet_master_seed: masterSeedHex,
|
|
211
|
+
libp2p_peer_id: libp2pIdentity.libp2pPeerId,
|
|
212
|
+
receiveAddress: params.destAddress, refundAddress: params.refundAddress,
|
|
213
|
+
mnemonic: mnemonicKeys.mnemonic, derivation: mnemonicKeys.derivation,
|
|
214
|
+
});
|
|
215
|
+
const saveResult = await this.platform.saveKeystore(this.keystore, params.amount);
|
|
216
|
+
this.keystoreId = saveResult.id;
|
|
217
|
+
this.logger.info({ keystoreId: this.keystoreId }, 'Keystore saved');
|
|
218
|
+
}
|
|
219
|
+
// Emit `keystoreId` to engine state IMMEDIATELY, before the slow
|
|
220
|
+
// pre-deposit prep work (`estimateFee` → mempool.space, `getQuotes`
|
|
221
|
+
// → swap-engine, `generateQr` → wasm). The web app's
|
|
222
|
+
// EngineRegistry races a 30 s timeout against `state.atomic.
|
|
223
|
+
// snapshot.keystoreId` to decide whether to keep this engine; if
|
|
224
|
+
// the slow chain below blows the budget the engine is destroyed
|
|
225
|
+
// mid-flight and the user has to retry. Emitting a placeholder
|
|
226
|
+
// keystore-saved transition here guarantees the registry binds
|
|
227
|
+
// the engine on first save, regardless of subsequent network
|
|
228
|
+
// latency.
|
|
229
|
+
this.setFlowContext({ keystoreId: this.keystoreId });
|
|
230
|
+
this.transition({
|
|
231
|
+
phase: 'keystore-saved',
|
|
232
|
+
snapshot: this.flowCtx,
|
|
233
|
+
message: 'Keystore saved. Computing deposit details...',
|
|
234
|
+
});
|
|
235
|
+
this.checkAborted();
|
|
236
|
+
const { feeSats } = await this.platform.estimateFee(network);
|
|
237
|
+
const userSats = Math.round(parseFloat(params.amount) * 1e8);
|
|
238
|
+
const requiredBtc = ((userSats + feeSats) / 1e8).toFixed(8);
|
|
239
|
+
let expectedXmr = null;
|
|
240
|
+
try {
|
|
241
|
+
const quotes = await this.api.getQuotes({ from: 'BTC', to: 'XMR', amount: params.amount });
|
|
242
|
+
const q = quotes.quotes?.find((x) => x.provider === 'atomicswap');
|
|
243
|
+
expectedXmr = q?.expectedOutput ?? null;
|
|
244
|
+
}
|
|
245
|
+
catch { /* non-fatal */ }
|
|
246
|
+
const qr = await this.platform.generateQr(wallet.address);
|
|
247
|
+
// Now that the slow chain has finished, populate the full
|
|
248
|
+
// FlowContext with the deposit address, expected XMR amount, and
|
|
249
|
+
// QR. The first `keystore-saved` transition above was the bare-
|
|
250
|
+
// bones state emit needed to keep the registry from destroying
|
|
251
|
+
// this engine; this second `setFlowContext` enriches it before
|
|
252
|
+
// `awaiting-deposit` validates against `PopulatedFlowContext`.
|
|
253
|
+
this.setFlowContext({
|
|
254
|
+
depositAddr: wallet.address,
|
|
255
|
+
depositAmount: requiredBtc,
|
|
256
|
+
expectedOut: expectedXmr,
|
|
257
|
+
qr,
|
|
258
|
+
extra: { text: `Send ${requiredBtc} BTC to the address above.`, type: 'message' },
|
|
259
|
+
});
|
|
260
|
+
this.checkAborted();
|
|
261
|
+
// Validate before emitting awaiting-deposit
|
|
262
|
+
const populated = this.requirePopulated('awaiting-deposit');
|
|
263
|
+
if (!populated)
|
|
264
|
+
return;
|
|
265
|
+
this.transition({
|
|
266
|
+
phase: 'awaiting-deposit',
|
|
267
|
+
snapshot: populated,
|
|
268
|
+
message: `Send ${requiredBtc} BTC to the address above.`,
|
|
269
|
+
});
|
|
270
|
+
this.checkAborted();
|
|
271
|
+
this.deposit = await this.platform.watchDeposit(wallet.address, network, this.signal, (msg) => this.logger.debug({}, msg));
|
|
272
|
+
this.logger.info({ txid: this.deposit.txid, value: this.deposit.value }, 'Deposit detected');
|
|
273
|
+
this.setFlowContext({ extra: { text: 'Deposit detected. Creating swap...', type: 'message' } });
|
|
274
|
+
const populatedAfterDeposit = this.requirePopulated('deposit-detected');
|
|
275
|
+
if (!populatedAfterDeposit)
|
|
276
|
+
return;
|
|
277
|
+
this.transition({
|
|
278
|
+
phase: 'deposit-detected',
|
|
279
|
+
snapshot: populatedAfterDeposit,
|
|
280
|
+
deposit: {
|
|
281
|
+
txid: this.deposit.txid,
|
|
282
|
+
vout: this.deposit.vout,
|
|
283
|
+
value: this.deposit.value,
|
|
284
|
+
utxos: this.deposit.utxos?.map((u) => ({ txid: u.txid, vout: u.vout, value: u.value })),
|
|
285
|
+
},
|
|
286
|
+
message: 'Deposit detected. Creating swap...',
|
|
287
|
+
});
|
|
288
|
+
await this.driveSwapToCompletion(null);
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
this.handleError(err);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async resumeFromKeystore(keystoreId, existingSwapId) {
|
|
295
|
+
this.abortController = new AbortController();
|
|
296
|
+
this.hasReachedTerminal = false;
|
|
297
|
+
this.logger.info({ keystoreId, existingSwapId: existingSwapId ?? null }, 'AtomicFlow.resumeFromKeystore()');
|
|
298
|
+
try {
|
|
299
|
+
await ensureWasm();
|
|
300
|
+
this.keystoreId = keystoreId;
|
|
301
|
+
this.keystore = await this.platform.loadKeystore(keystoreId);
|
|
302
|
+
const network = (this.keystore.btc?.network ?? 'mainnet');
|
|
303
|
+
const btcAddress = this.keystore.btc.address;
|
|
304
|
+
this.setFlowContext({
|
|
305
|
+
fromToken: 'BTC',
|
|
306
|
+
toToken: 'XMR',
|
|
307
|
+
keystoreId,
|
|
308
|
+
depositAddr: btcAddress,
|
|
309
|
+
destAddress: this.keystore.swap.receiveAddress,
|
|
310
|
+
refundAddress: this.keystore.swap.refundAddress,
|
|
311
|
+
provider: 'atomicswap',
|
|
312
|
+
extra: { text: 'Loading keystore...', type: 'message' },
|
|
313
|
+
});
|
|
314
|
+
this.transition({
|
|
315
|
+
phase: 'creating-swap',
|
|
316
|
+
snapshot: this.flowCtx,
|
|
317
|
+
message: 'Loading keystore...',
|
|
318
|
+
});
|
|
319
|
+
if (existingSwapId) {
|
|
320
|
+
this.logger.info({ swapId: existingSwapId }, 'Resume Path A: existing swap');
|
|
321
|
+
const detail = await this.fetchDetailWithRetry(existingSwapId);
|
|
322
|
+
this.setFlowContext({
|
|
323
|
+
swapId: existingSwapId,
|
|
324
|
+
swapNumber: detail.swapNumber ?? null,
|
|
325
|
+
destAddress: this.keystore.swap.receiveAddress || detail.destAddress || null,
|
|
326
|
+
refundAddress: this.keystore.swap.refundAddress || detail.refundAddress || null,
|
|
327
|
+
depositAmount: detail.amountIn || null,
|
|
328
|
+
expectedOut: detail.expectedAmountOut || null,
|
|
329
|
+
expectedOutUsd: detail.expectedAmountOutUsd ?? null,
|
|
330
|
+
amountInUsd: detail.amountInUsd ?? null,
|
|
331
|
+
expiresAt: detail.expiresAt || null,
|
|
332
|
+
});
|
|
333
|
+
if (TERMINAL_STATUSES.has(detail.status)) {
|
|
334
|
+
this.emitTerminal(existingSwapId, detail.status, detail);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// Funding-address UTXO is only meaningful pre-broadcast — once the
|
|
338
|
+
// swap is `deposited` or beyond, the BTC has already been swept
|
|
339
|
+
// into TxLock at the lock address and the funding address is
|
|
340
|
+
// empty. Re-querying it on every resume costs 2-15 s on
|
|
341
|
+
// testnet/stagenet electrs (cold TLS, multi-server fallback,
|
|
342
|
+
// flaky public endpoints) and the result feeds nothing
|
|
343
|
+
// downstream once the protocol is past funding. Skip it for
|
|
344
|
+
// post-funding statuses so resume settles immediately on the
|
|
345
|
+
// current state instead of stalling on a useless network probe.
|
|
346
|
+
const PRE_FUNDING_STATUSES = new Set([
|
|
347
|
+
'initializing',
|
|
348
|
+
'pending',
|
|
349
|
+
'awaiting_funding',
|
|
350
|
+
]);
|
|
351
|
+
const deposit = PRE_FUNDING_STATUSES.has(detail.status)
|
|
352
|
+
? await this.platform.fetchUtxo(btcAddress, network)
|
|
353
|
+
: null;
|
|
354
|
+
if (deposit)
|
|
355
|
+
this.deposit = deposit;
|
|
356
|
+
// Verify the contract for the resumed swap
|
|
357
|
+
let verification = null;
|
|
358
|
+
if (detail.depositAddress && detail.verification) {
|
|
359
|
+
const { verifyDepositAddress } = await import('../../verification/index.js');
|
|
360
|
+
verification = await verifyDepositAddress({
|
|
361
|
+
depositAddress: detail.depositAddress,
|
|
362
|
+
verification: detail.verification,
|
|
363
|
+
destAddress: this.keystore.swap.receiveAddress,
|
|
364
|
+
refundAddress: this.keystore.swap.refundAddress,
|
|
365
|
+
toToken: 'XMR',
|
|
366
|
+
amount: '',
|
|
367
|
+
// Lock address lives at the top level of SwapDetail; timelock
|
|
368
|
+
// blocks come from the typed atomicswap params when present.
|
|
369
|
+
protocol: detail.depositAddress &&
|
|
370
|
+
detail.protocolData?.type === 'atomicswap' &&
|
|
371
|
+
detail.protocolData.params
|
|
372
|
+
? {
|
|
373
|
+
lock_address: detail.depositAddress,
|
|
374
|
+
timelock_blocks: detail.protocolData.params.cancel_timelock,
|
|
375
|
+
}
|
|
376
|
+
: undefined,
|
|
377
|
+
expectedAmountOut: detail.expectedAmountOut ?? undefined,
|
|
378
|
+
expectedDestAddress: this.keystore.swap.receiveAddress,
|
|
379
|
+
fetchFn: this.config.fetchFn,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// Fallback: if server has progressed past verification, trust it
|
|
383
|
+
if (!verification) {
|
|
384
|
+
verification = { verified: true, provider: 'atomicswap', checks: [], timestamp: Date.now() };
|
|
385
|
+
}
|
|
386
|
+
const qr = await this.platform.generateQr(btcAddress);
|
|
387
|
+
this.setFlowContext({
|
|
388
|
+
qr,
|
|
389
|
+
verification,
|
|
390
|
+
depositAmount: deposit ? (deposit.value / 1e8).toFixed(8) : this.flowCtx?.depositAmount ?? null,
|
|
391
|
+
extra: { text: `Resuming swap (${detail.status})...`, type: 'message' },
|
|
392
|
+
});
|
|
393
|
+
// For resumed swaps the core will provide verification via progress callbacks.
|
|
394
|
+
// Emit as swapping (base FlowContext is sufficient for resume entry).
|
|
395
|
+
this.transition({
|
|
396
|
+
phase: 'creating-swap',
|
|
397
|
+
snapshot: this.flowCtx,
|
|
398
|
+
message: `Resuming swap (${detail.status})...`,
|
|
399
|
+
});
|
|
400
|
+
await this.driveSwapToCompletion(existingSwapId);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.logger.info({ btcAddress }, 'Resume Path B: local keystore');
|
|
404
|
+
const deposit = await this.platform.fetchUtxo(btcAddress, network);
|
|
405
|
+
// Fetch expected XMR output for both branches (required for PopulatedFlowContext).
|
|
406
|
+
// Pre-funding resume: there's no deposit yet, so we can't derive the
|
|
407
|
+
// amount from on-chain. Read the original amount the swap was started
|
|
408
|
+
// with from the keystore metadata (saved by saveKeystore as `label`).
|
|
409
|
+
let amountForQuote;
|
|
410
|
+
if (deposit) {
|
|
411
|
+
amountForQuote = (deposit.value / 1e8).toFixed(8);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const meta = await this.platform.listKeystores();
|
|
415
|
+
const ksMeta = meta.find((m) => m.id === keystoreId);
|
|
416
|
+
amountForQuote = ksMeta?.amount ?? '0';
|
|
417
|
+
}
|
|
418
|
+
let expectedOut = this.flowCtx?.expectedOut ?? null;
|
|
419
|
+
if (!expectedOut && amountForQuote !== '0') {
|
|
420
|
+
try {
|
|
421
|
+
const quotes = await this.api.getQuotes({
|
|
422
|
+
from: 'BTC',
|
|
423
|
+
to: 'XMR',
|
|
424
|
+
amount: amountForQuote,
|
|
425
|
+
});
|
|
426
|
+
const q = quotes.quotes?.find((x) => x.provider === 'atomicswap');
|
|
427
|
+
expectedOut = q?.expectedOutput ?? null;
|
|
428
|
+
}
|
|
429
|
+
catch { /* non-fatal — requirePopulated will catch if still null */ }
|
|
430
|
+
}
|
|
431
|
+
const qr = await this.platform.generateQr(btcAddress);
|
|
432
|
+
if (deposit) {
|
|
433
|
+
this.deposit = deposit;
|
|
434
|
+
this.setFlowContext({
|
|
435
|
+
qr,
|
|
436
|
+
expectedOut,
|
|
437
|
+
depositAmount: amountForQuote,
|
|
438
|
+
extra: { text: 'Deposit found. Creating swap...', type: 'message' },
|
|
439
|
+
});
|
|
440
|
+
const populated = this.requirePopulated('deposit-detected');
|
|
441
|
+
if (!populated)
|
|
442
|
+
return;
|
|
443
|
+
this.transition({
|
|
444
|
+
phase: 'deposit-detected',
|
|
445
|
+
snapshot: populated,
|
|
446
|
+
deposit: { txid: deposit.txid, vout: deposit.vout, value: deposit.value },
|
|
447
|
+
message: 'Deposit found. Creating swap...',
|
|
448
|
+
});
|
|
449
|
+
await this.driveSwapToCompletion(null);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
this.setFlowContext({
|
|
453
|
+
qr,
|
|
454
|
+
expectedOut,
|
|
455
|
+
depositAmount: amountForQuote === '0' ? null : amountForQuote,
|
|
456
|
+
extra: { text: 'Send BTC to the address above to begin the swap.', type: 'message' },
|
|
457
|
+
});
|
|
458
|
+
const populated = this.requirePopulated('awaiting-deposit');
|
|
459
|
+
if (!populated)
|
|
460
|
+
return;
|
|
461
|
+
this.transition({
|
|
462
|
+
phase: 'awaiting-deposit',
|
|
463
|
+
snapshot: populated,
|
|
464
|
+
message: 'Send BTC to the address above to begin the swap.',
|
|
465
|
+
});
|
|
466
|
+
this.checkAborted();
|
|
467
|
+
this.deposit = await this.platform.watchDeposit(btcAddress, network, this.signal, (msg) => this.logger.debug({}, msg));
|
|
468
|
+
this.setFlowContext({
|
|
469
|
+
depositAmount: (this.deposit.value / 1e8).toFixed(8),
|
|
470
|
+
extra: { text: 'Deposit detected. Creating swap...', type: 'message' },
|
|
471
|
+
});
|
|
472
|
+
const populatedAfterDeposit = this.requirePopulated('deposit-detected');
|
|
473
|
+
if (!populatedAfterDeposit)
|
|
474
|
+
return;
|
|
475
|
+
this.transition({
|
|
476
|
+
phase: 'deposit-detected',
|
|
477
|
+
snapshot: populatedAfterDeposit,
|
|
478
|
+
deposit: {
|
|
479
|
+
txid: this.deposit.txid,
|
|
480
|
+
vout: this.deposit.vout,
|
|
481
|
+
value: this.deposit.value,
|
|
482
|
+
utxos: this.deposit.utxos?.map((u) => ({ txid: u.txid, vout: u.vout, value: u.value })),
|
|
483
|
+
},
|
|
484
|
+
message: 'Deposit detected. Creating swap...',
|
|
485
|
+
});
|
|
486
|
+
await this.driveSwapToCompletion(null);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
this.handleError(err);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async fetchDetailWithRetry(swapId) {
|
|
494
|
+
let lastErr;
|
|
495
|
+
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
496
|
+
try {
|
|
497
|
+
return await this.api.getSwapDetail(swapId);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
if (!isTransientError(err))
|
|
501
|
+
throw err;
|
|
502
|
+
lastErr = err;
|
|
503
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
504
|
+
this.logger.warn({ swapId, attempt: attempt + 1, error: msg }, 'Server error fetching swap, retrying');
|
|
505
|
+
this.setFlowContext({
|
|
506
|
+
extra: { text: `Server error, retrying (${attempt + 1}/${MAX_TRANSIENT_RETRIES})...`, type: 'warning' },
|
|
507
|
+
});
|
|
508
|
+
this.transition({
|
|
509
|
+
phase: 'creating-swap',
|
|
510
|
+
snapshot: this.flowCtx,
|
|
511
|
+
message: `Server error, retrying (${attempt + 1}/${MAX_TRANSIENT_RETRIES})...`,
|
|
512
|
+
});
|
|
513
|
+
await delay(this.pollMs * (attempt + 1), this.signal).catch(() => { });
|
|
514
|
+
if (this.signal.aborted)
|
|
515
|
+
throw new SwapCancelledError();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
throw lastErr ?? new Error(`Failed to fetch swap ${swapId} after retries`);
|
|
519
|
+
}
|
|
520
|
+
async driveSwapToCompletion(existingSwapId) {
|
|
521
|
+
if (!this.keystore) {
|
|
522
|
+
throw new Error('keystore must be set before driving swap');
|
|
523
|
+
}
|
|
524
|
+
const network = (this.keystore.btc?.network ?? 'mainnet');
|
|
525
|
+
// Provide a blockchain provider so the resume path can reconstruct the
|
|
526
|
+
// TxLock from on-chain data when recomputing the redeem digest (Fix 1
|
|
527
|
+
// on resume). The provider is shared across the driver's lifetime.
|
|
528
|
+
const blockchain = await this.platform.createBlockchainProvider(network);
|
|
529
|
+
const result = await coreResumeAtomicSwap({
|
|
530
|
+
api: this.api,
|
|
531
|
+
params: {
|
|
532
|
+
keystore: this.keystore,
|
|
533
|
+
deposit: this.deposit ?? { txid: '', vout: 0, value: 0, confirmations: 0, status: 'mempool', utxos: [] },
|
|
534
|
+
network,
|
|
535
|
+
blockchain,
|
|
536
|
+
existingSwapId: existingSwapId ?? undefined,
|
|
537
|
+
logger: this.logger,
|
|
538
|
+
monerodNodes: this.config.monerodNodes,
|
|
539
|
+
...(this.variantId !== undefined ? { variantId: this.variantId } : {}),
|
|
540
|
+
},
|
|
541
|
+
onProgress: (p) => this.mapCoreProgress(p),
|
|
542
|
+
signal: this.signal,
|
|
543
|
+
fetchFn: this.config.fetchFn,
|
|
544
|
+
saveProtocolSnapshot: this.platform.saveProtocolSnapshot,
|
|
545
|
+
loadProtocolSnapshot: this.platform.loadProtocolSnapshot,
|
|
546
|
+
});
|
|
547
|
+
// If the driver already reached a terminal phase via mapCoreProgress
|
|
548
|
+
// (sweep complete, refunded, cancelled, failed, punished), the post-driver
|
|
549
|
+
// server re-check is meaningless — the server has no scanner for the
|
|
550
|
+
// on-chain XMR sweep, so it still reports requiredAction=sweep /
|
|
551
|
+
// status=sending right after the client broadcast its sweep tx. Treating
|
|
552
|
+
// that as actionable triggers a phantom second executeSweep that emits a
|
|
553
|
+
// spurious `completed → sweeping` transition.
|
|
554
|
+
if (this.hasReachedTerminal)
|
|
555
|
+
return;
|
|
556
|
+
let finalStatus = 'completed';
|
|
557
|
+
let requiredAction = null;
|
|
558
|
+
try {
|
|
559
|
+
const detail = await this.api.getSwapDetail(result.swapId);
|
|
560
|
+
finalStatus = detail.status;
|
|
561
|
+
requiredAction = detail.requiredAction ?? null;
|
|
562
|
+
this.setFlowContext({ swapId: result.swapId, swapNumber: detail.swapNumber ?? null });
|
|
563
|
+
}
|
|
564
|
+
catch { /* best effort */ }
|
|
565
|
+
if (requiredAction?.type === 'refund') {
|
|
566
|
+
await this.executeRefund(result.swapId);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (requiredAction?.type === 'sweep' || finalStatus === 'sending') {
|
|
570
|
+
await this.executeSweep(result.swapId);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (TERMINAL_STATUSES.has(finalStatus)) {
|
|
574
|
+
this.emitTerminal(result.swapId, finalStatus, undefined);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
// Non-terminal status with no immediately-actionable requiredAction
|
|
578
|
+
// (e.g., server is still broadcasting TxCancel, or waiting for cancel
|
|
579
|
+
// confirmation). Sidecar handles TxCancel automatically; the client just
|
|
580
|
+
// needs to watch for the requiredAction to flip to 'refund' (or the
|
|
581
|
+
// swap to reach terminal). pollUntilTerminal does both.
|
|
582
|
+
this.setFlowContext({
|
|
583
|
+
extra: { text: requiredAction?.message ?? 'Waiting for server...', type: 'message' },
|
|
584
|
+
});
|
|
585
|
+
await this.pollUntilTerminal(result.swapId);
|
|
586
|
+
}
|
|
587
|
+
mapCoreProgress(p) {
|
|
588
|
+
const progressKey = `${p.stage}|${p.swapId ?? ''}|${p.message}`;
|
|
589
|
+
if (progressKey !== this.lastProgressKey) {
|
|
590
|
+
this.logger.debug({ stage: p.stage, swapId: p.swapId ?? null, message: p.message }, 'Core progress');
|
|
591
|
+
this.lastProgressKey = progressKey;
|
|
592
|
+
}
|
|
593
|
+
if (p.swapId)
|
|
594
|
+
this.setFlowContext({ swapId: p.swapId });
|
|
595
|
+
if (p.swapNumber)
|
|
596
|
+
this.setFlowContext({ swapNumber: p.swapNumber });
|
|
597
|
+
if (p.verification)
|
|
598
|
+
this.setFlowContext({ verification: p.verification });
|
|
599
|
+
switch (p.stage) {
|
|
600
|
+
case 'keygen':
|
|
601
|
+
case 'keystore_saved':
|
|
602
|
+
case 'awaiting_deposit':
|
|
603
|
+
case 'deposit_detected':
|
|
604
|
+
case 'creating_swap':
|
|
605
|
+
case 'initializing':
|
|
606
|
+
case 'pending':
|
|
607
|
+
case 'awaiting_funding':
|
|
608
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
609
|
+
this.transition({
|
|
610
|
+
phase: 'creating-swap',
|
|
611
|
+
snapshot: this.flowCtx,
|
|
612
|
+
message: p.message,
|
|
613
|
+
});
|
|
614
|
+
break;
|
|
615
|
+
case 'verifying_xmr': {
|
|
616
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
617
|
+
const v = this.requireVerified('confirming');
|
|
618
|
+
if (v)
|
|
619
|
+
this.transition({ phase: 'confirming', snapshot: v, message: p.message });
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case 'signing_psbt': {
|
|
623
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
624
|
+
const v = this.requireVerified('signing');
|
|
625
|
+
if (v)
|
|
626
|
+
this.transition({ phase: 'signing', snapshot: v, message: p.message });
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
case 'funding': {
|
|
630
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
631
|
+
const v = this.requireVerified('funding');
|
|
632
|
+
if (v)
|
|
633
|
+
this.transition({ phase: 'funding', snapshot: v, message: p.message });
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
case 'submit_encsig': {
|
|
637
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
638
|
+
const v = this.requireVerified('computing-encsig');
|
|
639
|
+
if (v)
|
|
640
|
+
this.transition({ phase: 'computing-encsig', snapshot: v, message: p.message });
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
case 'confirming':
|
|
644
|
+
case 'deposited': {
|
|
645
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
646
|
+
const v = this.requireVerified('confirming');
|
|
647
|
+
if (v)
|
|
648
|
+
this.transition({ phase: 'confirming', snapshot: v, message: p.message });
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 'swapping':
|
|
652
|
+
case 'sending': {
|
|
653
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
654
|
+
const v = this.requireVerified('swapping');
|
|
655
|
+
if (v)
|
|
656
|
+
this.transition({ phase: 'swapping', snapshot: v, message: p.message });
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case 'sweeping': {
|
|
660
|
+
this.setFlowContext({ extra: { text: p.message, type: 'message' } });
|
|
661
|
+
const v = this.requireVerified('sweeping');
|
|
662
|
+
if (v)
|
|
663
|
+
this.transition({ phase: 'sweeping', snapshot: v, message: p.message, sweepStep: 'get-outputs' });
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case 'complete':
|
|
667
|
+
case 'completed':
|
|
668
|
+
this.setFlowContext({ extra: { text: 'Swap completed', type: 'message' } });
|
|
669
|
+
this.hasReachedTerminal = true;
|
|
670
|
+
this.transition({
|
|
671
|
+
phase: 'completed',
|
|
672
|
+
snapshot: this.flowCtx,
|
|
673
|
+
outputTxHash: p.txHash ?? null,
|
|
674
|
+
// Atomic swaps have no slippage — the buyer receives exactly the
|
|
675
|
+
// negotiated rate × deposit amount. Default to the snapshot's
|
|
676
|
+
// expectedOut so the receipt shows the actual amount the user got
|
|
677
|
+
// even when the in-flight progress event doesn't carry it.
|
|
678
|
+
actualOut: this.flowCtx?.expectedOut ?? '',
|
|
679
|
+
durationSec: null,
|
|
680
|
+
});
|
|
681
|
+
break;
|
|
682
|
+
case 'failed':
|
|
683
|
+
case 'error':
|
|
684
|
+
this.setFlowContext({ extra: { text: p.message, type: 'error' } });
|
|
685
|
+
this.hasReachedTerminal = true;
|
|
686
|
+
this.transition({
|
|
687
|
+
phase: 'failed',
|
|
688
|
+
snapshot: this.flowCtx,
|
|
689
|
+
error: p.message,
|
|
690
|
+
});
|
|
691
|
+
break;
|
|
692
|
+
case 'refunded':
|
|
693
|
+
this.hasReachedTerminal = true;
|
|
694
|
+
this.transition({
|
|
695
|
+
phase: 'refunded',
|
|
696
|
+
snapshot: this.flowCtx,
|
|
697
|
+
swapId: p.swapId ?? this.flowCtx?.swapId ?? '',
|
|
698
|
+
refundTxid: p.txHash ?? null,
|
|
699
|
+
});
|
|
700
|
+
break;
|
|
701
|
+
case 'cancelled':
|
|
702
|
+
this.hasReachedTerminal = true;
|
|
703
|
+
this.transition({
|
|
704
|
+
phase: 'cancelled',
|
|
705
|
+
snapshot: this.flowCtx,
|
|
706
|
+
swapId: p.swapId ?? this.flowCtx?.swapId ?? null,
|
|
707
|
+
txCancelTxid: null,
|
|
708
|
+
});
|
|
709
|
+
break;
|
|
710
|
+
case 'cancelling':
|
|
711
|
+
// Intermediate: BTC TxCancel is in flight, the refund will follow.
|
|
712
|
+
// Stay in a watching phase so the driver keeps polling until the row
|
|
713
|
+
// settles in `refunded` (or `failed` if the refund never lands).
|
|
714
|
+
this.setFlowContext({ extra: { text: p.message, type: 'warning' } });
|
|
715
|
+
this.transition({
|
|
716
|
+
phase: 'creating-swap',
|
|
717
|
+
snapshot: this.flowCtx,
|
|
718
|
+
message: p.message,
|
|
719
|
+
});
|
|
720
|
+
break;
|
|
721
|
+
case 'withheld':
|
|
722
|
+
case 'expired':
|
|
723
|
+
// Terminal: the deposit window closed (`expired`) or the maker
|
|
724
|
+
// refused to release funds (`withheld`). Both are user-visible
|
|
725
|
+
// terminal statuses in `TerminalStatus`. Emit a `failed` phase so
|
|
726
|
+
// poll consumers (engine driver, TUI exec state) observe a terminal
|
|
727
|
+
// transition and resolve.
|
|
728
|
+
this.transition({
|
|
729
|
+
phase: 'failed',
|
|
730
|
+
snapshot: this.flowCtx,
|
|
731
|
+
error: p.message,
|
|
732
|
+
});
|
|
733
|
+
break;
|
|
734
|
+
case 'punished': {
|
|
735
|
+
// Not terminal. Alice published TxPunish (Bob missed the refund
|
|
736
|
+
// window), but the sidecar autonomously runs
|
|
737
|
+
// `cooperative_xmr_redeem_after_punish` to recover s_a — once Alice
|
|
738
|
+
// cooperates, the server flips `requiredAction.type` to `sweep` and
|
|
739
|
+
// the driver finishes the swap with an XMR sweep that lands the row
|
|
740
|
+
// in `completed`. Stay in a watching phase so `drive.ts`'s existing
|
|
741
|
+
// sweep branch can pick up the action transition.
|
|
742
|
+
const reason = p.message ||
|
|
743
|
+
'BTC punished — recovering XMR via cooperative_xmr_redeem_after_punish...';
|
|
744
|
+
this.setFlowContext({ extra: { text: reason, type: 'warning' } });
|
|
745
|
+
const verified = this.requireVerified('swapping');
|
|
746
|
+
if (verified) {
|
|
747
|
+
this.transition({ phase: 'swapping', snapshot: verified, message: reason });
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
default:
|
|
752
|
+
this.logger.warn({ stage: p.stage }, `Unhandled atomic progress stage: ${p.stage}`);
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
async userCancel() {
|
|
757
|
+
const state = this.getCurrentAwaitingActionState();
|
|
758
|
+
if (!state)
|
|
759
|
+
return;
|
|
760
|
+
const { requiredAction } = state;
|
|
761
|
+
const swapId = this.flowCtx?.swapId ?? '';
|
|
762
|
+
this.logger.info({ swapId, action: 'cancel', blocksRemaining: requiredAction.blocksRemaining }, 'User cancel initiated');
|
|
763
|
+
if (requiredAction.blocksRemaining && requiredAction.blocksRemaining > 0) {
|
|
764
|
+
this.transition({
|
|
765
|
+
...state,
|
|
766
|
+
error: `TxCancel not available — ${requiredAction.blocksRemaining} blocks remaining`,
|
|
767
|
+
});
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
this.setFlowContext({ extra: { text: 'Broadcasting TxCancel...', type: 'message' } });
|
|
771
|
+
this.transition({
|
|
772
|
+
phase: 'cancelling',
|
|
773
|
+
snapshot: this.flowCtx,
|
|
774
|
+
message: 'Broadcasting TxCancel...',
|
|
775
|
+
});
|
|
776
|
+
try {
|
|
777
|
+
const result = await this.api.executeAction(swapId, { type: 'cancel' });
|
|
778
|
+
this.transition({
|
|
779
|
+
phase: 'cancelled',
|
|
780
|
+
snapshot: this.flowCtx,
|
|
781
|
+
swapId,
|
|
782
|
+
txCancelTxid: result.protocolData && 'tx_cancel_txid' in result.protocolData
|
|
783
|
+
? result.protocolData.tx_cancel_txid
|
|
784
|
+
: null,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
catch (err) {
|
|
788
|
+
this.transition({
|
|
789
|
+
...state,
|
|
790
|
+
error: `Cancel failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
this.resumeActionLoop();
|
|
794
|
+
}
|
|
795
|
+
async userRefund() {
|
|
796
|
+
const state = this.getCurrentAwaitingActionState();
|
|
797
|
+
if (!state || !this.keystore)
|
|
798
|
+
return;
|
|
799
|
+
const swapId = this.flowCtx?.swapId ?? '';
|
|
800
|
+
this.logger.info({ swapId, action: 'refund' }, 'User refund initiated');
|
|
801
|
+
await this.executeRefund(swapId);
|
|
802
|
+
this.resumeActionLoop();
|
|
803
|
+
}
|
|
804
|
+
async userRetrySweep() {
|
|
805
|
+
const swapId = this.flowCtx?.swapId ?? '';
|
|
806
|
+
if (!swapId)
|
|
807
|
+
return;
|
|
808
|
+
this.logger.info({ swapId, action: 'retry-sweep' }, 'User retry sweep');
|
|
809
|
+
await this.executeSweep(swapId);
|
|
810
|
+
}
|
|
811
|
+
async executeSweep(swapId) {
|
|
812
|
+
if (!this.keystore) {
|
|
813
|
+
this.emitError('sweeping', 'Keystore not available for sweep');
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
this.logger.info({ swapId }, 'Starting XMR sweep');
|
|
817
|
+
this.setFlowContext({ extra: { text: 'Starting XMR sweep...', type: 'message' } });
|
|
818
|
+
const verified = this.requireVerified('sweeping');
|
|
819
|
+
if (!verified)
|
|
820
|
+
return;
|
|
821
|
+
this.transition({
|
|
822
|
+
phase: 'sweeping', snapshot: verified,
|
|
823
|
+
message: 'Starting XMR sweep...', sweepStep: 'get-outputs',
|
|
824
|
+
});
|
|
825
|
+
try {
|
|
826
|
+
const s_b_bytes = new Uint8Array((this.keystore.keys.s_b.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
|
|
827
|
+
const freshDetail = await this.api.getSwapDetail(swapId);
|
|
828
|
+
const pp = freshDetail.protocolData?.type === 'atomicswap'
|
|
829
|
+
? freshDetail.protocolData.params
|
|
830
|
+
: null;
|
|
831
|
+
if (!pp?.S_a_monero) {
|
|
832
|
+
throw new ProtocolError('E_PROTOCOL_PARAMS_MISSING', 'sweep requires S_a_monero in protocol params');
|
|
833
|
+
}
|
|
834
|
+
const result = await sweepMonero(this.api, {
|
|
835
|
+
swapId, s_b: s_b_bytes,
|
|
836
|
+
receiveAddress: this.keystore.swap.receiveAddress,
|
|
837
|
+
expectedSAMonero: pp.S_a_monero,
|
|
838
|
+
monerodNodes: this.config.monerodNodes,
|
|
839
|
+
onProgress: (stage) => {
|
|
840
|
+
const sweepStep = stage.includes('key') ? 'key-images'
|
|
841
|
+
: stage.includes('submit') || stage.includes('broadcast') ? 'broadcasting'
|
|
842
|
+
: stage.includes('sign') ? 'submitting' : 'get-outputs';
|
|
843
|
+
this.setFlowContext({ extra: { text: stage, type: 'message' } });
|
|
844
|
+
const v = this.requireVerified('sweeping');
|
|
845
|
+
if (v)
|
|
846
|
+
this.transition({
|
|
847
|
+
phase: 'sweeping', snapshot: v, message: stage,
|
|
848
|
+
sweepStep: sweepStep,
|
|
849
|
+
});
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
const amountXmr = result.amount !== 'unknown'
|
|
853
|
+
? (Number(result.amount) / 1e12).toFixed(12).replace(/\.?0+$/, '') : '';
|
|
854
|
+
this.logger.info({ swapId, txHash: result.txHash, amountXmr }, 'Sweep completed');
|
|
855
|
+
this.setFlowContext({ extra: { text: 'Sweep complete', type: 'message' } });
|
|
856
|
+
this.transition({
|
|
857
|
+
phase: 'completed', snapshot: this.flowCtx,
|
|
858
|
+
outputTxHash: result.txHash, actualOut: amountXmr, durationSec: null,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
catch (err) {
|
|
862
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
863
|
+
this.logger.error({ swapId, error: msg }, 'Sweep failed');
|
|
864
|
+
if (msg.includes("'completed'") || msg.includes('"completed"')) {
|
|
865
|
+
try {
|
|
866
|
+
const detail = await this.api.getSwapDetail(swapId);
|
|
867
|
+
this.emitTerminal(swapId, 'completed', detail);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
catch { /* fall through */ }
|
|
871
|
+
}
|
|
872
|
+
this.setFlowContext({ extra: { text: `Sweep failed: ${msg}. Press w to retry.`, type: 'error' } });
|
|
873
|
+
this.transition({
|
|
874
|
+
phase: 'failed', snapshot: this.flowCtx, error: `Sweep failed: ${msg}. Press w to retry.`,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
async executeRefund(swapId) {
|
|
879
|
+
if (!this.keystore) {
|
|
880
|
+
this.emitError('refunding', 'Keystore not available for refund');
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.logger.info({ swapId }, 'Starting client-side refund');
|
|
884
|
+
const detail = await this.api.getSwapDetail(swapId).catch(() => null);
|
|
885
|
+
if (!detail) {
|
|
886
|
+
this.emitError('refunding', 'Could not fetch swap detail for refund');
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
const lockAddress = this.resolveLockAddress(detail);
|
|
890
|
+
if (!lockAddress) {
|
|
891
|
+
this.logger.error({ swapId }, 'Cannot determine lock address for refund');
|
|
892
|
+
this.setFlowContext({
|
|
893
|
+
extra: {
|
|
894
|
+
text: 'Cannot determine lock address — unable to verify TxCancel',
|
|
895
|
+
type: 'error',
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
this.transition({
|
|
899
|
+
phase: 'failed',
|
|
900
|
+
snapshot: this.flowCtx,
|
|
901
|
+
error: 'Cannot determine lock address — unable to verify TxCancel',
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const protocolParams = await this.resolveProtocolParams(swapId, detail);
|
|
906
|
+
if (!protocolParams) {
|
|
907
|
+
this.emitRefundAborted(swapId, 'Server did not return protocol params and no local cache available — cannot build refund');
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!hasRefundEscapeHatch(protocolParams)) {
|
|
911
|
+
this.emitRefundAborted(swapId, 'No refund encsig in protocol params (need either tx_full_refund_encsig, or tx_partial_refund_encsig with the amnesty triple) — cannot construct refund');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this.logger.info({ swapId, lockAddress }, 'Verifying TxCancel on-chain');
|
|
915
|
+
this.setFlowContext({ extra: { text: 'Verifying TxCancel on-chain...', type: 'message' } });
|
|
916
|
+
this.transition({
|
|
917
|
+
phase: 'verifying-cancel',
|
|
918
|
+
snapshot: this.flowCtx,
|
|
919
|
+
message: 'Verifying TxCancel on-chain...',
|
|
920
|
+
});
|
|
921
|
+
const blockchain = await this.platform.createBlockchainProvider(this.config.network);
|
|
922
|
+
const verification = await discoverAndVerifyTxCancel(blockchain, lockAddress, detail.depositTxHash ?? '', this.config.network);
|
|
923
|
+
if (!verification.verified || !verification.txCancelHex) {
|
|
924
|
+
this.logger.error({ swapId, reason: verification.reason }, 'TxCancel verification failed — refund aborted');
|
|
925
|
+
const reason = verification.reason || 'TxCancel not verified';
|
|
926
|
+
this.setFlowContext({ extra: { text: `REFUSED: ${reason}. Refund aborted.`, type: 'error' } });
|
|
927
|
+
this.transition({
|
|
928
|
+
phase: 'failed',
|
|
929
|
+
snapshot: this.flowCtx,
|
|
930
|
+
error: `REFUSED: ${reason}. Refund aborted.`,
|
|
931
|
+
});
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
this.logger.info({ swapId, reason: verification.reason }, 'TxCancel verified — assembling refund locally');
|
|
935
|
+
this.setFlowContext({
|
|
936
|
+
extra: { text: 'TxCancel confirmed. Signing refund locally...', type: 'message' },
|
|
937
|
+
});
|
|
938
|
+
this.transition({
|
|
939
|
+
phase: 'refunding',
|
|
940
|
+
snapshot: this.flowCtx,
|
|
941
|
+
message: 'TxCancel confirmed. Signing refund locally...',
|
|
942
|
+
});
|
|
943
|
+
await ensureWasm();
|
|
944
|
+
const useAmnesty = protocolParams.tx_partial_refund_encsig !== undefined &&
|
|
945
|
+
protocolParams.amnesty_amount_sats !== undefined &&
|
|
946
|
+
protocolParams.tx_partial_refund_fee_sats !== undefined;
|
|
947
|
+
const aPubHex = protocolParams.A;
|
|
948
|
+
const bPubHex = this.keystore.keys.B;
|
|
949
|
+
const witnessScript = buildMultisigWitnessScript(aPubHex, bPubHex);
|
|
950
|
+
const refundAddress = this.keystore.swap.refundAddress;
|
|
951
|
+
let txRefundHex;
|
|
952
|
+
let cancelOutputValueSats;
|
|
953
|
+
let encsigRefund;
|
|
954
|
+
if (useAmnesty) {
|
|
955
|
+
const built = buildPartialRefund({
|
|
956
|
+
txCancelHex: verification.txCancelHex,
|
|
957
|
+
refundAddress,
|
|
958
|
+
refundFeeSats: BigInt(protocolParams.tx_partial_refund_fee_sats ?? 0),
|
|
959
|
+
network: this.config.network,
|
|
960
|
+
amnestyAmountSats: BigInt(protocolParams.amnesty_amount_sats ?? 0),
|
|
961
|
+
partialRefundFeeSats: BigInt(protocolParams.tx_partial_refund_fee_sats ?? 0),
|
|
962
|
+
aPubHex,
|
|
963
|
+
bPubHex,
|
|
964
|
+
});
|
|
965
|
+
txRefundHex = built.txRefundHex;
|
|
966
|
+
cancelOutputValueSats = built.cancelOutputValueSats;
|
|
967
|
+
encsigRefund = protocolParams.tx_partial_refund_encsig ?? '';
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
const built = buildFullRefund({
|
|
971
|
+
txCancelHex: verification.txCancelHex,
|
|
972
|
+
refundAddress,
|
|
973
|
+
refundFeeSats: BigInt(protocolParams.tx_refund_fee_sats),
|
|
974
|
+
network: this.config.network,
|
|
975
|
+
});
|
|
976
|
+
txRefundHex = built.txRefundHex;
|
|
977
|
+
cancelOutputValueSats = built.cancelOutputValueSats;
|
|
978
|
+
encsigRefund = protocolParams.tx_full_refund_encsig ?? '';
|
|
979
|
+
}
|
|
980
|
+
let assembled;
|
|
981
|
+
try {
|
|
982
|
+
assembled = signRefund({
|
|
983
|
+
txRefundHex,
|
|
984
|
+
witnessScript,
|
|
985
|
+
txCancelOutputValueSats: cancelOutputValueSats,
|
|
986
|
+
encsigRefund,
|
|
987
|
+
sBHexLE: this.keystore.keys.s_b,
|
|
988
|
+
bHex: this.keystore.keys.b,
|
|
989
|
+
aPubHex,
|
|
990
|
+
bPubHex,
|
|
991
|
+
sBPubHex: this.keystore.keys.S_b_bitcoin,
|
|
992
|
+
logger: this.logger,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
997
|
+
this.logger.error({ swapId, error: msg }, 'Refund assembly failed');
|
|
998
|
+
this.setFlowContext({ extra: { text: `Refund failed: ${msg}`, type: 'error' } });
|
|
999
|
+
this.transition({ phase: 'failed', snapshot: this.flowCtx, error: `Refund failed: ${msg}` });
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
this.logger.info({ swapId, txid: assembled.txid }, 'Broadcasting client-signed refund');
|
|
1003
|
+
let broadcastTxid;
|
|
1004
|
+
try {
|
|
1005
|
+
broadcastTxid = await blockchain.broadcastTransaction(assembled.hex);
|
|
1006
|
+
}
|
|
1007
|
+
catch (err) {
|
|
1008
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1009
|
+
if (/already in mempool|already exists|already known/i.test(msg)) {
|
|
1010
|
+
this.logger.warn({ swapId, txid: assembled.txid }, 'Refund already broadcast; continuing');
|
|
1011
|
+
broadcastTxid = assembled.txid;
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
this.logger.error({ swapId, error: msg }, 'Refund broadcast failed');
|
|
1015
|
+
this.setFlowContext({
|
|
1016
|
+
extra: { text: `Refund broadcast failed: ${msg}`, type: 'error' },
|
|
1017
|
+
});
|
|
1018
|
+
this.transition({
|
|
1019
|
+
phase: 'failed',
|
|
1020
|
+
snapshot: this.flowCtx,
|
|
1021
|
+
error: `Refund broadcast failed: ${msg}`,
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
try {
|
|
1027
|
+
await this.api.executeAction(swapId, {
|
|
1028
|
+
type: 'notify-refund',
|
|
1029
|
+
refund_txid: broadcastTxid,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1034
|
+
this.logger.warn({ swapId, txid: broadcastTxid, error: msg }, 'notify-refund server call failed; refund tx is on-chain — continuing');
|
|
1035
|
+
}
|
|
1036
|
+
this.logger.info({ swapId, refundTxid: broadcastTxid }, 'Client-side refund broadcast successful');
|
|
1037
|
+
this.transition({
|
|
1038
|
+
phase: 'refunded',
|
|
1039
|
+
snapshot: this.flowCtx,
|
|
1040
|
+
swapId,
|
|
1041
|
+
refundTxid: broadcastTxid,
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
resolveLockAddress(detail) {
|
|
1045
|
+
if (detail.depositAddress)
|
|
1046
|
+
return detail.depositAddress;
|
|
1047
|
+
const v = detail.verification;
|
|
1048
|
+
if (v && typeof v === 'object' && 'lock_address' in v) {
|
|
1049
|
+
const candidate = v.lock_address;
|
|
1050
|
+
if (typeof candidate === 'string' && candidate.length > 0)
|
|
1051
|
+
return candidate;
|
|
1052
|
+
}
|
|
1053
|
+
return '';
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Read protocol params from the server response; fall back to the platform
|
|
1057
|
+
* adapter's optional write-through cache when the server didn't return the
|
|
1058
|
+
* refund encsig. On success from the server, populate the cache so later
|
|
1059
|
+
* refunds survive a briefly-unreachable backend.
|
|
1060
|
+
*/
|
|
1061
|
+
async resolveProtocolParams(swapId, detail) {
|
|
1062
|
+
const fromServer = extractProtocolData(detail).params;
|
|
1063
|
+
if (fromServer && hasRefundEscapeHatch(fromServer)) {
|
|
1064
|
+
await this.cacheProtocolParams(swapId, fromServer);
|
|
1065
|
+
return fromServer;
|
|
1066
|
+
}
|
|
1067
|
+
const loadFn = this.platform.loadSwapProtocol;
|
|
1068
|
+
if (loadFn) {
|
|
1069
|
+
try {
|
|
1070
|
+
const cached = await loadFn(swapId);
|
|
1071
|
+
if (cached && hasRefundEscapeHatch(cached)) {
|
|
1072
|
+
this.logger.info({ swapId }, 'Server omitted refund encsig — loaded from local protocol cache');
|
|
1073
|
+
return cached;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
catch (err) {
|
|
1077
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1078
|
+
this.logger.warn({ swapId, error: msg }, 'Local protocol cache read failed — proceeding with server response');
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return fromServer;
|
|
1082
|
+
}
|
|
1083
|
+
async cacheProtocolParams(swapId, params) {
|
|
1084
|
+
const saveFn = this.platform.saveSwapProtocol;
|
|
1085
|
+
if (!saveFn)
|
|
1086
|
+
return;
|
|
1087
|
+
try {
|
|
1088
|
+
await saveFn(swapId, params);
|
|
1089
|
+
}
|
|
1090
|
+
catch (err) {
|
|
1091
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1092
|
+
this.logger.warn({ swapId, error: msg }, 'Failed to cache protocol params — refund still works via server');
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
emitRefundAborted(swapId, reason) {
|
|
1096
|
+
this.logger.error({ swapId, reason }, 'Refund aborted');
|
|
1097
|
+
this.setFlowContext({ extra: { text: `REFUSED: ${reason}`, type: 'error' } });
|
|
1098
|
+
this.transition({
|
|
1099
|
+
phase: 'failed',
|
|
1100
|
+
snapshot: this.flowCtx,
|
|
1101
|
+
error: `REFUSED: ${reason}`,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
async pollUntilTerminal(swapId) {
|
|
1105
|
+
while (!this.signal.aborted) {
|
|
1106
|
+
try {
|
|
1107
|
+
const detail = await this.api.getSwapDetail(swapId);
|
|
1108
|
+
if (TERMINAL_STATUSES.has(detail.status)) {
|
|
1109
|
+
this.emitTerminal(swapId, detail.status, detail);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
// Opportunistically prime the protocol cache while the server is
|
|
1113
|
+
// reachable. The refund path will use this cache if the server is
|
|
1114
|
+
// briefly unreachable later.
|
|
1115
|
+
const polledParams = extractProtocolData(detail).params;
|
|
1116
|
+
if (polledParams?.tx_full_refund_encsig) {
|
|
1117
|
+
await this.cacheProtocolParams(swapId, polledParams);
|
|
1118
|
+
}
|
|
1119
|
+
// Server flipped the required action to 'refund' — TxCancel has
|
|
1120
|
+
// confirmed on-chain and Bob's refund window is open. Auto-trigger
|
|
1121
|
+
// the client-side refund so we don't drift toward the punish
|
|
1122
|
+
// deadline while idly polling.
|
|
1123
|
+
const action = detail.requiredAction ?? null;
|
|
1124
|
+
if (action?.type === 'refund') {
|
|
1125
|
+
this.logger.info({ swapId, blocksRemaining: action.blocksRemaining ?? null }, 'Required action flipped to refund — auto-triggering client-side refund');
|
|
1126
|
+
await this.executeRefund(swapId);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
if (action?.type === 'sweep') {
|
|
1130
|
+
this.logger.info({ swapId }, 'Required action flipped to sweep — auto-triggering sweep');
|
|
1131
|
+
await this.executeSweep(swapId);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
catch { /* retry */ }
|
|
1136
|
+
await delay(this.pollMs, this.signal).catch(() => { });
|
|
1137
|
+
if (this.signal.aborted)
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
emitTerminal(swapId, status, detail) {
|
|
1142
|
+
this.setFlowContext({ swapId, swapNumber: detail?.swapNumber ?? null });
|
|
1143
|
+
if (status === 'completed') {
|
|
1144
|
+
this.setFlowContext({ extra: { text: 'Swap completed', type: 'message' } });
|
|
1145
|
+
this.transition({
|
|
1146
|
+
phase: 'completed', snapshot: this.flowCtx,
|
|
1147
|
+
outputTxHash: detail?.outputTxHash ?? null,
|
|
1148
|
+
actualOut: detail?.actualAmountOut ?? '', durationSec: detail?.durationSeconds ?? null,
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
else if (status === 'refunded') {
|
|
1152
|
+
this.transition({
|
|
1153
|
+
phase: 'refunded', snapshot: this.flowCtx, swapId,
|
|
1154
|
+
refundTxid: detail?.refundTxHash ?? null,
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
this.setFlowContext({ extra: { text: `Swap ended: ${status}`, type: 'error' } });
|
|
1159
|
+
this.transition({
|
|
1160
|
+
phase: 'failed', snapshot: this.flowCtx, error: `Swap ended: ${status}`,
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
resumeActionLoop() {
|
|
1165
|
+
this.userActionResolver?.();
|
|
1166
|
+
this.userActionResolver = null;
|
|
1167
|
+
}
|
|
1168
|
+
checkAborted() {
|
|
1169
|
+
if (this.signal.aborted)
|
|
1170
|
+
throw new SwapCancelledError();
|
|
1171
|
+
}
|
|
1172
|
+
handleError(err) {
|
|
1173
|
+
if (err instanceof SwapCancelledError || this.signal.aborted) {
|
|
1174
|
+
this.logger.info({ swapId: this.flowCtx?.swapId ?? null }, 'AtomicFlow cancelled');
|
|
1175
|
+
this.transition({ phase: 'cancelled', snapshot: this.flowCtx, swapId: null, txCancelTxid: null });
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1179
|
+
this.logger.error({ swapId: this.flowCtx?.swapId ?? null, error: message }, 'AtomicFlow error');
|
|
1180
|
+
this.setFlowContext({ extra: { text: message, type: 'error' } });
|
|
1181
|
+
this.transition({
|
|
1182
|
+
phase: 'failed', snapshot: this.flowCtx,
|
|
1183
|
+
error: message,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
getCurrentAwaitingActionState() {
|
|
1187
|
+
if (this.lastEmittedState.phase === 'awaiting-user-action')
|
|
1188
|
+
return this.lastEmittedState;
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
//# sourceMappingURL=atomic-flow.js.map
|