@atomiqlabs/sdk
Version:
atomiq labs SDK for cross-chain swaps between smart chains and bitcoin
321 lines (273 loc) • 11 kB
text/typescript
import {decode as bolt11Decode} from "@atomiqlabs/bolt11";
import {ToBTCLNDefinition, ToBTCLNWrapper} from "./ToBTCLNWrapper";
import {isIToBTCSwapInit, IToBTCSwap, IToBTCSwapInit} from "../IToBTCSwap";
import {SwapType} from "../../../../enums/SwapType";
import {ChainType, SwapData} from "@atomiqlabs/base";
import {Buffer} from "buffer";
import {sha256} from "@noble/hashes/sha2";
import {IntermediaryError} from "../../../../errors/IntermediaryError";
import {isLNURLPaySuccessAction, LNURL, LNURLPaySuccessAction} from "../../../../lnurl/LNURL";
import {TokenAmount, toTokenAmount} from "../../../../types/TokenAmount";
import {BitcoinTokens, BtcToken} from "../../../../types/Token";
import {getLogger, LoggerType} from "../../../../utils/Logger";
import {LNURLDecodedSuccessAction} from "../../../../types/lnurl/LNURLPay";
export type ToBTCLNSwapInit<T extends SwapData> = IToBTCSwapInit<T> & {
confidence: number;
pr?: string;
lnurl?: string;
successAction?: LNURLPaySuccessAction;
longExpiry?: boolean;
};
export function isToBTCLNSwapInit<T extends SwapData>(obj: any): obj is ToBTCLNSwapInit<T> {
return typeof (obj.confidence) === "number" &&
(obj.pr==null || typeof (obj.pr) === "string") &&
(obj.lnurl == null || typeof (obj.lnurl) === "string") &&
(obj.successAction == null || isLNURLPaySuccessAction(obj.successAction)) &&
(obj.longExpiry == null || typeof (obj.longExpiry) === "boolean") &&
isIToBTCSwapInit<T>(obj);
}
//Set of nodes which disallow probing, resulting in 0 confidence reported by the LP
const SNOWFLAKE_LIST: Set<string> = new Set([
"038f8f113c580048d847d6949371726653e02b928196bad310e3eda39ff61723f6",
"03a6ce61fcaacd38d31d4e3ce2d506602818e3856b4b44faff1dde9642ba705976"
]);
/**
* Escrow based (HTLC) swap for Smart chains -> Bitcoin lightning
*
* @category Swaps/Smart chain → Lightning
*/
export class ToBTCLNSwap<T extends ChainType = ChainType> extends IToBTCSwap<T, ToBTCLNDefinition<T>> {
protected readonly TYPE = SwapType.TO_BTCLN;
/**
* @internal
*/
protected readonly outputToken: BtcToken<true> = BitcoinTokens.BTCLN;
/**
* @internal
*/
protected readonly logger: LoggerType;
private readonly usesClaimHashAsId: boolean;
private readonly confidence: number;
private readonly longExpiry?: boolean;
private pr?: string;
private secret?: string;
private lnurl?: string;
private successAction?: LNURLPaySuccessAction;
/**
* Sets the LNURL data for the swap
*
* @internal
*/
_setLNURLData(lnurl: string, successAction?: LNURLPaySuccessAction) {
this.lnurl = lnurl;
this.successAction = successAction;
}
constructor(wrapper: ToBTCLNWrapper<T>, init: ToBTCLNSwapInit<T["Data"]>);
constructor(wrapper: ToBTCLNWrapper<T>, obj: any);
constructor(wrapper: ToBTCLNWrapper<T>, initOrObj: ToBTCLNSwapInit<T["Data"]> | any) {
if(isToBTCLNSwapInit(initOrObj) && initOrObj.url!=null) initOrObj.url += "/tobtcln";
super(wrapper, initOrObj);
if(isToBTCLNSwapInit(initOrObj)) {
this.confidence = initOrObj.confidence;
this.pr = initOrObj.pr;
this.lnurl = initOrObj.lnurl;
this.successAction = initOrObj.successAction;
this.longExpiry = initOrObj.longExpiry;
this.usesClaimHashAsId = true;
} else {
this.confidence = initOrObj.confidence;
this.pr = initOrObj.pr;
this.lnurl = initOrObj.lnurl;
this.successAction = initOrObj.successAction;
this.secret = initOrObj.secret;
this.longExpiry = initOrObj.longExpiry;
this.usesClaimHashAsId = initOrObj.usesClaimHashAsId ?? false;
}
this.logger = getLogger("ToBTCLN("+this.getIdentifierHashString()+"): ");
this.tryRecomputeSwapPrice();
}
/**
* @inheritDoc
* @internal
*/
_setPaymentResult(result: { secret?: string; txId?: string }, check: boolean = false): Promise<boolean> {
if(result==null) return Promise.resolve(false);
if(result.secret==null) throw new IntermediaryError("No payment secret returned!");
const secretBuffer = Buffer.from(result.secret, "hex");
const hash = Buffer.from(sha256(secretBuffer));
if(check) {
const claimHash = this._contract.getHashForHtlc(hash);
const expectedClaimHash = Buffer.from(this.getClaimHash(), "hex");
if(!claimHash.equals(expectedClaimHash)) throw new IntermediaryError("Invalid payment secret returned");
}
this.pr ??= hash.toString("hex");
this.secret = result.secret;
return Promise.resolve(true);
}
//////////////////////////////
//// Amounts & fees
/**
* @inheritDoc
*/
getOutputToken(): BtcToken<true> {
return BitcoinTokens.BTCLN;
}
/**
* @inheritDoc
*/
getOutput(): TokenAmount<BtcToken<true>> {
if(this.pr==null || !this.pr.toLowerCase().startsWith("ln"))
return toTokenAmount(null, this.outputToken, this.wrapper._prices, this.pricingInfo);
const parsedPR = bolt11Decode(this.pr);
if(parsedPR.millisatoshis==null) throw new Error("Swap invoice has no msat amount field!");
const amount = (BigInt(parsedPR.millisatoshis) + 999n) / 1000n;
return toTokenAmount(amount, this.outputToken, this.wrapper._prices, this.pricingInfo);
}
//////////////////////////////
//// Getters & utils
/**
* @inheritDoc
*/
getOutputTxId(): string | null {
const paymentHash = this.getPaymentHash();
if(paymentHash==null) return null;
return paymentHash.toString("hex");
}
/**
* @inheritDoc
*/
getOutputAddress(): string | null {
return this.lnurl ?? this.pr ?? null;
}
/**
* Returns payment secret (pre-image) as a proof of payment
*/
getSecret(): string | null {
return this.secret ?? null;
}
/**
* Returns the confidence of the intermediary that this payment will succeed.
*
* @returns Decimal value between 0 and 1, where 0 is not likely and 1 is very likely
*/
getConfidence(): number {
return this.confidence;
}
/**
* Checks whether a swap is likely to fail, based on the confidence as reported by the intermediary (LP)
*/
willLikelyFail(): boolean {
if(this.pr==null || !this.pr.toLowerCase().startsWith("ln")) return false;
const parsedRequest = bolt11Decode(this.pr);
if(parsedRequest.tagsObject.routing_info!=null) {
for (let route of parsedRequest.tagsObject.routing_info) {
if(SNOWFLAKE_LIST.has(route.pubkey)) {
return false;
}
}
}
return this.confidence===0;
}
/**
* Tries to detect if the target lightning invoice is a non-custodial mobile wallet, extract care must be taken
* for such a wallet **to be online** when attempting to make a swap sending to such a wallet
*/
isPayingToNonCustodialWallet(): boolean {
if(this.pr==null || !this.pr.toLowerCase().startsWith("ln")) return false;
const parsedRequest = bolt11Decode(this.pr);
if(parsedRequest.tagsObject.routing_info!=null) {
return parsedRequest.tagsObject.routing_info.length>0;
}
return false;
}
/**
* Returns whether the swap requires longer than usual HTLC expiration, this means in the case that the LP is not
* cooperative the user will have to wait longer for the unilateral refund to become available. The longer
* expiration times might be required when sending lightning payments to systems with long potential settlement
* times (like Arkade or Bark).
*/
hasLongExpiration(): boolean {
return this.longExpiry ?? false;
}
/**
* @inheritDoc
* @internal
*/
protected getIdentifierHash(): Buffer {
const idBuffer: Buffer = this.usesClaimHashAsId
? Buffer.from(this.getClaimHash(), "hex")
: this.getPaymentHash()!;
if(this._randomNonce==null) return idBuffer;
return Buffer.concat([idBuffer, Buffer.from(this._randomNonce, "hex")]);
}
/**
* @inheritDoc
* @internal
*/
protected getLpIdentifier(): string {
if(this.pr==null) return this._data.getEscrowHash();
if(this.pr.toLowerCase().startsWith("ln")) {
const parsed = bolt11Decode(this.pr);
if(parsed.tagsObject.payment_hash==null) throw new Error("Swap invoice has no payment hash field!");
return parsed.tagsObject.payment_hash;
}
return this.pr;
}
/**
* Returns the payment hash of the swap, i.e. a payment hash of the lightning network invoice that
* is about to be paid
*/
getPaymentHash(): Buffer | null {
if(this.pr==null) return null;
if(this.pr.toLowerCase().startsWith("ln")) {
const parsed = bolt11Decode(this.pr);
if(parsed.tagsObject.payment_hash==null) throw new Error("Swap invoice has no payment hash field!");
return Buffer.from(parsed.tagsObject.payment_hash, "hex");
}
return Buffer.from(this.pr, "hex");
}
//////////////////////////////
//// LNURL-pay
/**
* Whether this is an LNURL-pay swap
*/
isLNURL(): boolean {
return this.lnurl!=null;
}
/**
* Gets the used LNURL-pay link or `null` if this is not an LNURL-pay swap
*/
getLNURL(): string | null {
return this.lnurl ?? null;
}
/**
* Checks whether this LNURL-pay payment contains a success action
*/
hasSuccessAction(): boolean {
return this.successAction!=null;
}
/**
* Returns the success action after a successful payment, else `null`
*/
getSuccessAction(): LNURLDecodedSuccessAction | null {
return LNURL.decodeSuccessAction(this.successAction, this.secret);
}
//////////////////////////////
//// Storage
/**
* @inheritDoc
*/
serialize(): any {
return {
...super.serialize(),
paymentHash: this.getPaymentHash()?.toString("hex"),
pr: this.pr,
confidence: this.confidence,
secret: this.secret,
lnurl: this.lnurl,
successAction: this.successAction,
longExpiry: this.longExpiry,
usesClaimHashAsId: this.usesClaimHashAsId
};
}
}