@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
520 lines (519 loc) • 23.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SwapperUtils = void 0;
const bolt11_1 = require("@atomiqlabs/bolt11");
const btc_signer_1 = require("@scure/btc-signer");
const LNURL_1 = require("../lnurl/LNURL");
const SwapType_1 = require("../enums/SwapType");
const SingleAddressBitcoinWallet_1 = require("../bitcoin/wallet/SingleAddressBitcoinWallet");
const base_1 = require("@atomiqlabs/base");
const Utils_1 = require("../utils/Utils");
const TokenAmount_1 = require("../types/TokenAmount");
const Token_1 = require("../types/Token");
const LNURLWithdraw_1 = require("../types/lnurl/LNURLWithdraw");
const LNURLPay_1 = require("../types/lnurl/LNURLPay");
const BitcoinWalletUtils_1 = require("../utils/BitcoinWalletUtils");
/**
* Utility class providing helper methods for address parsing, token balances, serialization
* and other miscellaneous things.
*
* @category Core
*/
class SwapperUtils {
constructor(root) {
this.bitcoinNetwork = root._btcNetwork;
this.root = root;
}
/**
* Checks whether a passed address is a valid address on the smart chain
*
* @param address Address
* @param chainId Smart chain identifier string to check the address for
*/
isValidSmartChainAddress(address, chainId) {
if (chainId != null) {
if (this.root._chains[chainId] == null)
throw new Error(`Unknown chain id: ${chainId}`);
return this.root._chains[chainId].chainInterface.isValidAddress(address);
}
for (let chainId of this.root.getSmartChains()) {
if (this.root._chains[chainId].chainInterface.isValidAddress(address))
return true;
}
return false;
}
/**
* Checks whether an address is a valid BOLT11 bitcoin lightning invoice
*
* @param address Address to check
*/
isLightningInvoice(address) {
try {
(0, bolt11_1.decode)(address);
return true;
}
catch (e) { }
return false;
}
/**
* Checks whether an address is a valid bitcoin address
*
* @param address Address to check
*/
isValidBitcoinAddress(address) {
try {
(0, btc_signer_1.Address)(this.bitcoinNetwork).decode(address);
return true;
}
catch (e) {
return false;
}
}
/**
* Checks whether an address is a valid BOLT11 bitcoin lightning invoice WITH AMOUNT
*
* @param address Address to check
*/
isValidLightningInvoice(address) {
try {
const parsed = (0, bolt11_1.decode)(address);
if (parsed.millisatoshis != null)
return true;
}
catch (e) { }
return false;
}
/**
* Checks whether an address is a valid LNURL (no checking on type is performed)
*
* @param address Address to check
*/
isValidLNURL(address) {
return LNURL_1.LNURL.isLNURL(address);
}
/**
* Returns type and data about an LNURL
*
* @param lnurl LNURL link to check, can be either `pay` or `withdraw` type
* @param shouldRetry Optional whether HTTP requests should retried on failure
*/
getLNURLTypeAndData(lnurl, shouldRetry) {
return LNURL_1.LNURL.getLNURLType(lnurl, shouldRetry);
}
/**
* Returns satoshi value of BOLT11 bitcoin lightning invoice WITH AMOUNT, returns null otherwise
*
* @param lnpr
*/
getLightningInvoiceValue(lnpr) {
const parsed = (0, bolt11_1.decode)(lnpr);
if (parsed.millisatoshis != null)
return (BigInt(parsed.millisatoshis) + 999n) / 1000n;
return null;
}
parseBitcoinAddress(resultText) {
let _amount = undefined;
if (resultText.includes("?")) {
const arr = resultText.split("?");
resultText = arr[0];
const params = arr[1].split("&");
for (let param of params) {
const arr2 = param.split("=");
const key = arr2[0];
const value = decodeURIComponent(arr2[1]);
if (key === "amount") {
_amount = (0, Utils_1.fromDecimal)(parseFloat(value).toFixed(8), 8);
}
}
}
if (this.isValidBitcoinAddress(resultText)) {
return {
address: resultText,
type: "BITCOIN",
swapType: SwapType_1.SwapType.TO_BTC,
amount: _amount == null ? undefined : (0, TokenAmount_1.toTokenAmount)(_amount, Token_1.BitcoinTokens.BTC, this.root.prices)
};
}
return null;
}
parseLNURLSync(resultText) {
if (this.isValidLNURL(resultText)) {
return {
address: resultText,
type: "LNURL",
swapType: null
};
}
return null;
}
async parseLNURL(resultText) {
if (this.isValidLNURL(resultText)) {
try {
const result = await this.getLNURLTypeAndData(resultText);
if (result == null)
throw new Error("Invalid LNURL specified!");
const swapType = (0, LNURLPay_1.isLNURLPay)(result) ? SwapType_1.SwapType.TO_BTCLN : (0, LNURLWithdraw_1.isLNURLWithdraw)(result) ? SwapType_1.SwapType.FROM_BTCLN : null;
if (swapType == null)
return null;
const response = {
address: resultText,
type: "LNURL",
swapType,
lnurl: result
};
if (result.min === result.max) {
return {
...response,
amount: result.min == null ? undefined : (0, TokenAmount_1.toTokenAmount)(result.min, Token_1.BitcoinTokens.BTCLN, this.root.prices)
};
}
else {
return {
...response,
min: result.min == null ? undefined : (0, TokenAmount_1.toTokenAmount)(result.min, Token_1.BitcoinTokens.BTCLN, this.root.prices),
max: result.min == null ? undefined : (0, TokenAmount_1.toTokenAmount)(result.max, Token_1.BitcoinTokens.BTCLN, this.root.prices)
};
}
}
catch (e) {
throw new Error("Failed to contact LNURL service, check your internet connection and retry later.");
}
}
return null;
}
parseLightningInvoice(resultText) {
if (this.isLightningInvoice(resultText)) {
if (this.isValidLightningInvoice(resultText)) {
const amount = this.getLightningInvoiceValue(resultText);
if (amount == null)
throw new Error();
return {
address: resultText,
type: "LIGHTNING",
swapType: SwapType_1.SwapType.TO_BTCLN,
amount: (0, TokenAmount_1.toTokenAmount)(amount, Token_1.BitcoinTokens.BTCLN, this.root.prices)
};
}
else {
throw new Error("Lightning invoice needs to contain an amount!");
}
}
return null;
}
parseSmartchainAddress(resultText) {
for (let chainId of this.root.getSmartChains()) {
if (this.root._chains[chainId].chainInterface.isValidAddress(resultText)) {
return {
address: resultText,
type: chainId,
swapType: null
};
}
}
return null;
}
/**
* General parser for bitcoin addresses, LNURLs, lightning invoices, smart chain addresses. Also fetches LNURL data
* (hence async and returns Promise).
*
* @param addressString Address to parse
* @throws {Error} Error in address parsing
* @returns Address data or `null` if address doesn't conform to any known format
*/
async parseAddress(addressString) {
if (addressString.startsWith("bitcoin:")) {
const parsedBitcoinAddress = this.parseBitcoinAddress(addressString.substring(8));
if (parsedBitcoinAddress != null)
return parsedBitcoinAddress;
throw new Error("Invalid bitcoin address!");
}
const parsedBitcoinAddress = this.parseBitcoinAddress(addressString);
if (parsedBitcoinAddress != null)
return parsedBitcoinAddress;
if (addressString.startsWith("lightning:")) {
const resultText = addressString.substring(10);
const resultLnurl = await this.parseLNURL(resultText);
if (resultLnurl != null)
return resultLnurl;
const resultLightningInvoice = this.parseLightningInvoice(resultText);
if (resultLightningInvoice != null)
return resultLightningInvoice;
throw new Error("Invalid lightning network invoice or LNURL!");
}
const resultLnurl = await this.parseLNURL(addressString);
if (resultLnurl != null)
return resultLnurl;
const resultLightningInvoice = this.parseLightningInvoice(addressString);
if (resultLightningInvoice != null)
return resultLightningInvoice;
return this.parseSmartchainAddress(addressString);
}
/**
* Synchronous general parser for bitcoin addresses, LNURLs, lightning invoices, smart chain addresses, doesn't fetch
* LNURL data, returns `swapType: null` instead to prevent returning a Promise
*
* @param addressString Address to parse
* @throws {Error} Error in address parsing
* @returns Address data or `null` if address doesn't conform to any known format
*/
parseAddressSync(addressString) {
if (addressString.startsWith("bitcoin:")) {
const parsedBitcoinAddress = this.parseBitcoinAddress(addressString.substring(8));
if (parsedBitcoinAddress != null)
return parsedBitcoinAddress;
throw new Error("Invalid bitcoin address!");
}
const parsedBitcoinAddress = this.parseBitcoinAddress(addressString);
if (parsedBitcoinAddress != null)
return parsedBitcoinAddress;
if (addressString.startsWith("lightning:")) {
const resultText = addressString.substring(10);
const resultLnurl = this.parseLNURLSync(resultText);
if (resultLnurl != null)
return resultLnurl;
const resultLightningInvoice = this.parseLightningInvoice(resultText);
if (resultLightningInvoice != null)
return resultLightningInvoice;
throw new Error("Invalid lightning network invoice or LNURL!");
}
const resultLnurl = this.parseLNURLSync(addressString);
if (resultLnurl != null)
return resultLnurl;
const resultLightningInvoice = this.parseLightningInvoice(addressString);
if (resultLightningInvoice != null)
return resultLightningInvoice;
return this.parseSmartchainAddress(addressString);
}
/**
* Strips the URL encoding around `bitcoin:` and `lightning:` addresses, leaving just the raw address
*
* @param addressString Address to strip
*
* @returns Raw clean address
*/
stripAddress(addressString) {
if (addressString.startsWith("lightning:") || addressString.startsWith("bitcoin:")) {
addressString = addressString.substring(addressString.indexOf(":") + 1);
const delimeterIndex = addressString.indexOf("?");
if (delimeterIndex !== -1)
addressString = addressString.substring(0, delimeterIndex);
}
return addressString;
}
/**
* Returns a random PSBT that can be used for fee estimation for SPV vault (UTXO-controlled vault) based swaps
* {@link SwapType.SPV_VAULT_FROM_BTC}, the last output (the LP output) is omitted to allow for coinselection
* algorithm to determine maximum sendable amount there
*
* @param chainIdentifier Smart chain to swap to
* @param includeGasToken Whether to return the PSBT also with the gas token amount (increases the vSize by 8)
*/
getRandomSpvVaultPsbt(chainIdentifier, includeGasToken) {
const wrapper = this.root._chains[chainIdentifier].wrappers[SwapType_1.SwapType.SPV_VAULT_FROM_BTC];
if (wrapper == null)
throw new Error("Chain doesn't support spv vault swaps!");
return wrapper.getDummySwapPsbt(includeGasToken);
}
/**
* Returns the spendable balance of a bitcoin wallet
*
* @param wallet Bitcoin wallet to check the spendable balance for, can either be a simple
* bitcoin address string or a wallet object
* @param targetChain Destination smart chain for the swap, the ensures proper spendable balance
* is estimated taking into consideration different swap primitives available on different chains
* @param options Additional options
*/
async getBitcoinSpendableBalance(wallet, targetChain, options) {
let bitcoinWallet;
if (typeof (wallet) === "string") {
bitcoinWallet = new SingleAddressBitcoinWallet_1.SingleAddressBitcoinWallet(this.root._bitcoinRpc, this.bitcoinNetwork, { address: wallet, publicKey: "" });
}
else {
bitcoinWallet = (0, BitcoinWalletUtils_1.toBitcoinWallet)(wallet, this.root._bitcoinRpc, this.bitcoinNetwork);
}
let feeRate = options?.feeRate ?? await bitcoinWallet.getFeeRate();
if (options?.minFeeRate != null)
feeRate = Math.max(feeRate, options.minFeeRate);
let result;
if (targetChain != null && this.root.supportsSwapType(targetChain, SwapType_1.SwapType.SPV_VAULT_FROM_BTC)) {
result = await bitcoinWallet.getSpendableBalance(this.getRandomSpvVaultPsbt(targetChain, options?.gasDrop), feeRate);
}
else {
result = await bitcoinWallet.getSpendableBalance(undefined, feeRate);
}
return {
balance: (0, TokenAmount_1.toTokenAmount)(result.balance, Token_1.BitcoinTokens.BTC, this.root.prices),
feeRate: result.feeRate
};
}
/**
* Returns the maximum spendable balance of the smart chain wallet, deducting the fee needed
* to initiate a swap for native balances
*/
async getSpendableBalance(wallet, token, options) {
if (this.root._chains[token.chainId] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + token.chainId);
const { defaultVersion, versionedContracts, chainInterface } = this.root._chains[token.chainId];
const { swapContract } = versionedContracts[defaultVersion];
let signer;
if (typeof (wallet) === "string") {
signer = wallet;
}
else {
const abstractSigner = (0, base_1.isAbstractSigner)(wallet) ? wallet : await chainInterface.wrapSigner(wallet);
signer = abstractSigner.getAddress();
}
let finalBalance;
if (chainInterface.getNativeCurrencyAddress() !== token.address) {
finalBalance = await chainInterface.getBalance(signer, token.address);
}
else {
let [balance, commitFee] = await Promise.all([
chainInterface.getBalance(signer, token.address),
swapContract.getCommitFee(signer,
//Use large amount, such that the fee for wrapping more tokens is always included!
await swapContract.createSwapData(base_1.ChainSwapType.HTLC, signer, chainInterface.randomAddress(), token.address, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn, swapContract.getHashForHtlc((0, Utils_1.randomBytes)(32)).toString("hex"), base_1.BigIntBufferUtils.fromBuffer((0, Utils_1.randomBytes)(8)), BigInt(Math.floor(Date.now() / 1000)), true, false, base_1.BigIntBufferUtils.fromBuffer((0, Utils_1.randomBytes)(2)), base_1.BigIntBufferUtils.fromBuffer((0, Utils_1.randomBytes)(2))), options?.feeRate)
]);
if (options?.feeMultiplier != null) {
commitFee = commitFee * (BigInt(Math.floor(options.feeMultiplier * 1000000))) / 1000000n;
}
finalBalance = (0, Utils_1.bigIntMax)(balance - commitFee, 0n);
}
return (0, TokenAmount_1.toTokenAmount)(finalBalance, token, this.root.prices);
}
/**
* Returns the address of the native currency of the smart chain
*/
getNativeToken(chainIdentifier) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._tokens[chainIdentifier][this.root._chains[chainIdentifier].chainInterface.getNativeCurrencyAddress()];
}
/**
* Returns whether when swapping to the provided token a gas drop can be requested
*
* @param token
*/
destinationTokenSupportsGasDrop(token) {
if (this.root._chains[token.chainId] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + token.chainId);
const { chainInterface } = this.root._chains[token.chainId];
if (chainInterface.shouldGetNativeTokenDrop != null)
return chainInterface.shouldGetNativeTokenDrop(token.address);
return chainInterface.getNativeCurrencyAddress() !== token.address;
}
/**
* Returns a random signer for a given smart chain
*
* @param chainIdentifier
*/
randomSigner(chainIdentifier) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.randomSigner();
}
/**
* Returns a random address for a given smart chain or bitcoin
*
* @param chainIdentifier
*/
randomAddress(chainIdentifier) {
if (chainIdentifier === "BITCOIN") {
// Return random p2wkh address
return (0, btc_signer_1.Address)(this.bitcoinNetwork).encode({
type: "wpkh",
hash: (0, Utils_1.randomBytes)(20)
});
}
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.randomAddress();
}
/**
* Signs and broadcasts the supplied smart chain transaction
*
* @param chainIdentifier Smart chain identifier string
* @param signer Signer to use for signing the transactions
* @param txs An array of transactions to sign
* @param abortSignal Abort signal
* @param onBeforePublish Callback invoked before a transaction is sent (invoked for every transaction to be sent)
*/
sendAndConfirm(chainIdentifier, signer, txs, abortSignal, onBeforePublish) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.sendAndConfirm(signer, txs, true, abortSignal, false, onBeforePublish);
}
/**
* Broadcasts already signed smart chain transactions
*
* @param chainIdentifier Smart chain identifier string
* @param txs An array of already signed transactions
* @param abortSignal Abort signal
* @param onBeforePublish Callback invoked before a transaction is sent (invoked for every transaction to be sent)
*/
sendSignedAndConfirm(chainIdentifier, txs, abortSignal, onBeforePublish) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.sendSignedAndConfirm(txs, true, abortSignal, false, onBeforePublish);
}
/**
* Prepares a set of unsigned transactions for signing, by adding required nonces or recent blockhashes, might
* also add hints of account deployment on e.g. Starknet
*
* @param chainIdentifier A chain for which to prepare the txs
* @param txs Transactions to prepare
*/
prepareUnsignedTransactions(chainIdentifier, txs) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
const chainInterface = this.root._chains[chainIdentifier].chainInterface;
if (chainInterface.prepareTxs == null)
throw new Error("Chain doesn't support tx preparation, chainId: " + chainIdentifier);
return chainInterface.prepareTxs(txs);
}
/**
* Serializes an unsigned smart chain transaction
*
* @param chainIdentifier Smart chain string identifier
* @param tx An unsigned transaction to serialize
*/
serializeUnsignedTransaction(chainIdentifier, tx) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.serializeTx(tx);
}
/**
* Deserializes an unsigned smart chain transaction
*
* @param chainIdentifier Smart chain string identifier
* @param tx Serialized unsigned transaction
*/
deserializeUnsignedTransaction(chainIdentifier, tx) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.deserializeTx(tx);
}
/**
* Serializes a signed smart chain transaction
*
* @param chainIdentifier Smart chain string identifier
* @param tx A signed transaction to serialize
*/
serializeSignedTransaction(chainIdentifier, tx) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.serializeSignedTx(tx);
}
/**
* Deserializes a signed smart chain transaction
*
* @param chainIdentifier Smart chain string identifier
* @param tx Serialized signed transaction
*/
deserializeSignedTransaction(chainIdentifier, tx) {
if (this.root._chains[chainIdentifier] == null)
throw new Error("Invalid chain identifier! Unknown chain: " + chainIdentifier);
return this.root._chains[chainIdentifier].chainInterface.deserializeSignedTx(tx);
}
}
exports.SwapperUtils = SwapperUtils;