@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
207 lines (179 loc) • 7.69 kB
text/typescript
import {ToBTCDefinition, ToBTCWrapper} from "./ToBTCWrapper";
import {isIToBTCSwapInit, IToBTCSwap, IToBTCSwapInit} from "../IToBTCSwap";
import {SwapType} from "../../../../enums/SwapType";
import {ChainType, SwapData} from "@atomiqlabs/base";
import {Buffer} from "buffer";
import {IntermediaryError} from "../../../../errors/IntermediaryError";
import {toBigInt} from "../../../../utils/Utils";
import {TokenAmount, toTokenAmount} from "../../../../types/TokenAmount";
import {BitcoinTokens, BtcToken} from "../../../../types/Token";
import {getLogger, LoggerType} from "../../../../utils/Logger";
import {fromOutputScript} from "../../../../utils/BitcoinUtils";
export type ToBTCSwapInit<T extends SwapData> = IToBTCSwapInit<T> & {
address?: string;
amount?: bigint;
confirmationTarget: number;
satsPerVByte: number;
requiredConfirmations?: number;
nonce?: bigint;
};
export function isToBTCSwapInit<T extends SwapData>(obj: any): obj is ToBTCSwapInit<T> {
return (obj.address==null || typeof (obj.address) === "string") &&
(obj.amount==null || typeof(obj.amount) === "bigint") &&
typeof (obj.confirmationTarget) === "number" &&
typeof (obj.satsPerVByte) === "number" &&
(obj.requiredConfirmations==null || typeof (obj.requiredConfirmations) === "number") &&
(obj.nonce==null || typeof (obj.nonce) === "bigint") &&
isIToBTCSwapInit<T>(obj);
}
/**
* Escrow based (PrTLC) swap for Smart chains -> Bitcoin
*
* @category Swaps/Smart chain → Bitcoin
*/
export class ToBTCSwap<T extends ChainType = ChainType> extends IToBTCSwap<T, ToBTCDefinition<T>> {
protected readonly TYPE: SwapType.TO_BTC = SwapType.TO_BTC;
/**
* @internal
*/
protected readonly outputToken: BtcToken<false> = BitcoinTokens.BTC;
/**
* @internal
*/
protected readonly logger: LoggerType;
private address?: string;
private amount?: bigint;
private readonly confirmationTarget: number;
private readonly satsPerVByte: number;
private requiredConfirmations?: number;
private nonce?: bigint;
private txId?: string;
constructor(wrapper: ToBTCWrapper<T>, serializedObject: any);
constructor(wrapper: ToBTCWrapper<T>, init: ToBTCSwapInit<T["Data"]>);
constructor(
wrapper: ToBTCWrapper<T>,
initOrObject: ToBTCSwapInit<T["Data"]> | any
) {
if(isToBTCSwapInit(initOrObject) && initOrObject.url!=null) initOrObject.url += "/tobtc";
super(wrapper, initOrObject);
if(isToBTCSwapInit(initOrObject)) {
this.address = initOrObject.address;
this.amount = initOrObject.amount;
this.confirmationTarget = initOrObject.confirmationTarget;
this.satsPerVByte = initOrObject.satsPerVByte;
this.requiredConfirmations = initOrObject.requiredConfirmations;
this.nonce = initOrObject.nonce;
} else {
this.address = initOrObject.address;
this.amount = toBigInt(initOrObject.amount);
this.confirmationTarget = initOrObject.confirmationTarget;
this.satsPerVByte = initOrObject.satsPerVByte;
this.txId = initOrObject.txId;
this.requiredConfirmations = initOrObject.requiredConfirmations ?? this._data.getConfirmationsHint();
this.nonce = toBigInt(initOrObject.nonce) ?? this._data.getNonceHint();
}
this.logger = getLogger("ToBTC("+this.getIdentifierHashString()+"): ");
this.tryRecomputeSwapPrice();
}
/**
* @inheritDoc
* @internal
*/
async _setPaymentResult(result: { secret?: string; txId?: string }, check: boolean = false): Promise<boolean> {
if(result==null) return false;
if(result.txId==null) throw new IntermediaryError("No btc txId returned!");
if(check || this.address==null || this.amount==null || this.nonce==null || this.requiredConfirmations==null) {
const btcTx = await this.wrapper._btcRpc.getTransaction(result.txId);
if(btcTx==null) return false;
//Extract nonce from tx
const nonce = this.nonce ?? (BigInt(btcTx.ins[0].sequence) & 0x00FFFFFFn) | (BigInt(btcTx.locktime - 500_000_000) << 24n);
let requiredConfirmations = this.requiredConfirmations;
const foundVout = btcTx.outs.find(vout => {
if(requiredConfirmations!=null) {
return this._data.getClaimHash()===this._contract.getHashForOnchain(
Buffer.from(vout.scriptPubKey.hex, "hex"),
BigInt(vout.value),
requiredConfirmations,
nonce
).toString("hex");
} else {
for(let i=1;i<=20;i++) {
if(
this._data.getClaimHash()===this._contract.getHashForOnchain(
Buffer.from(vout.scriptPubKey.hex, "hex"),
BigInt(vout.value),
i,
nonce
).toString("hex")
) {
requiredConfirmations = i;
return true;
}
}
}
});
if(requiredConfirmations==null) this.logger.warn(`_setPaymentResult(): Tried to recover data from bitcoin transaction ${result.txId} data, but wasn't able to!`);
if(foundVout!=null) {
this.nonce = nonce;
this.address = fromOutputScript(this.wrapper._options.bitcoinNetwork, foundVout.scriptPubKey.hex);
this.amount = BigInt(foundVout.value);
this.requiredConfirmations = requiredConfirmations;
} else {
if(check) throw new IntermediaryError("Invalid btc txId returned");
}
}
this.txId = result.txId;
return true;
}
//////////////////////////////
//// Amounts & fees
/**
* @inheritDoc
*/
getOutputToken(): BtcToken<false> {
return BitcoinTokens.BTC;
}
/**
* @inheritDoc
*/
getOutput(): TokenAmount<BtcToken<false>> {
return toTokenAmount(this.amount ?? null, this.outputToken, this.wrapper._prices, this.pricingInfo);
}
//////////////////////////////
//// Getters & utils
/**
* @inheritDoc
*/
getOutputAddress(): string | null {
return this.address ?? null;
}
/**
* @inheritDoc
*/
getOutputTxId(): string | null {
return this.txId ?? null;
}
/**
* Returns fee rate of the output bitcoin transaction in sats/vB as reported by the intermediary (LP)
*/
getBitcoinFeeRate(): number {
return this.satsPerVByte;
}
//////////////////////////////
//// Storage
/**
* @inheritDoc
*/
serialize(): any {
return {
...super.serialize(),
address: this.address,
amount: this.amount==null ? null : this.amount.toString(10),
confirmationTarget: this.confirmationTarget,
satsPerVByte: this.satsPerVByte,
nonce: this.nonce==null ? null : this.nonce.toString(10),
requiredConfirmations: this.requiredConfirmations,
txId: this.txId
};
}
}