UNPKG

@atomiqlabs/chain-starknet

Version:
595 lines (532 loc) 18.2 kB
import {SwapData, ChainSwapType} from "@atomiqlabs/base"; import {TimelockRefundHandler} from "./handlers/refund/TimelockRefundHandler"; import {BigNumberish, cairo, CairoOption, CairoOptionVariant, hash} from "starknet"; import {replaceBigInts, Serialized, toBigInt, toHex} from "../../utils/Utils"; import { StringToPrimitiveType } from "abi-wan-kanabi/dist/kanabi"; import {EscrowManagerAbi} from "./EscrowManagerAbi"; import {IClaimHandler} from "./handlers/claim/ClaimHandlers"; const FLAG_PAY_OUT: bigint = 0x01n; const FLAG_PAY_IN: bigint = 0x02n; const FLAG_REPUTATION: bigint = 0x04n; export type StarknetSwapDataType = StringToPrimitiveType<typeof EscrowManagerAbi, "escrow_manager::structs::escrow::EscrowData">; /** * Represents a success hook/action to be executed upon claim of the swap * * @category Swaps */ export type StarknetSuccessAction = { executionHash: string, executionExpiry: bigint, executionFee: bigint } function successActionEquals(a?: StarknetSuccessAction, b?: StarknetSuccessAction): boolean { if(a!=null && b!=null) { return a.executionHash.toLowerCase()===b.executionHash.toLowerCase() && a.executionExpiry === b.executionExpiry && a.executionFee === b.executionFee; } return a === b; } export type StarknetSwapDataCtorArgs = { offerer: string, claimer: string, token: string, refundHandler: string, claimHandler: string, payOut: boolean, payIn: boolean, reputation: boolean, sequence: bigint, claimData: string, refundData: string, amount: bigint, feeToken: string, securityDeposit: bigint, claimerBounty: bigint, kind: ChainSwapType, extraData?: string, successAction?: StarknetSuccessAction }; function isSerializedData(obj: any): obj is ({type: "strk"} & Serialized<StarknetSwapData>) { return obj.type==="strk"; } /** * Represents swap data for executing PrTLC (on-chain) or HTLC (lightning) based swaps * * @category Swaps */ export class StarknetSwapData extends SwapData { /** * * @param value * @private */ private static toFlags(value: number | bigint | string): {payOut: boolean, payIn: boolean, reputation: boolean, sequence: bigint} { const val = toBigInt(value); return { sequence: val >> 64n, payOut: (val & FLAG_PAY_OUT) === FLAG_PAY_OUT, payIn: (val & FLAG_PAY_IN) === FLAG_PAY_IN, reputation: (val & FLAG_REPUTATION) === FLAG_REPUTATION } } /** * * @private */ private getFlags(): bigint { return (this.sequence << 64n) + (this.payOut ? FLAG_PAY_OUT : 0n) + (this.payIn ? FLAG_PAY_IN : 0n) + (this.reputation ? FLAG_REPUTATION : 0n); } offerer: string; claimer: string; token: string; refundHandler: string; claimHandler: string; //Flags payOut: boolean; payIn: boolean; reputation: boolean; sequence: bigint; claimData: string; refundData: string; amount: bigint; feeToken: string; securityDeposit: bigint; claimerBounty: bigint; extraData?: string; successAction?: StarknetSuccessAction; kind: ChainSwapType; /** * Creates a new swap data based on the provided arguments * * @param args */ constructor(args: StarknetSwapDataCtorArgs); /** * Deserializes the spv vault data from its serialized implementation (returned from {@link StarknetSwapData.serialize}) * * @param data */ constructor(data: Serialized<StarknetSwapData> & {type: "strk"}); constructor( data: StarknetSwapDataCtorArgs | (Serialized<StarknetSwapData> & {type: "strk"}) ) { super(); if(!isSerializedData(data)) { this.offerer = data.offerer; this.claimer = data.claimer; this.token = data.token; this.refundHandler = data.refundHandler; this.claimHandler = data.claimHandler; this.payOut = data.payOut; this.payIn = data.payIn; this.reputation = data.reputation; this.sequence = data.sequence; this.claimData = data.claimData; this.refundData = data.refundData; this.amount = data.amount; this.feeToken = data.feeToken; this.securityDeposit = data.securityDeposit; this.claimerBounty = data.claimerBounty; this.kind = data.kind; this.extraData = data.extraData; this.successAction = data.successAction; } else { this.offerer = data.offerer; this.claimer = data.claimer; this.token = data.token; this.refundHandler = data.refundHandler; this.claimHandler = data.claimHandler; this.payOut = data.payOut; this.payIn = data.payIn; this.reputation = data.reputation; this.sequence = BigInt(data.sequence); this.claimData = data.claimData; this.refundData = data.refundData; this.amount = BigInt(data.amount); this.feeToken = data.feeToken; this.securityDeposit = BigInt(data.securityDeposit); this.claimerBounty = BigInt(data.claimerBounty); this.kind = data.kind; this.extraData = data.extraData; this.successAction = data.successAction==null || Array.isArray(data.successAction) ? undefined : { executionHash: data.successAction.executionHash, executionExpiry: BigInt(data.successAction.executionExpiry), executionFee: BigInt(data.successAction.executionFee), } } } /** * @inheritDoc */ getOfferer(): string { return this.offerer; } /** * @inheritDoc */ setOfferer(newOfferer: string) { this.offerer = newOfferer; this.payIn = true; } /** * @inheritDoc */ getClaimer(): string { return this.claimer; } /** * @inheritDoc */ setClaimer(newClaimer: string) { this.claimer = newClaimer; this.payIn = false; this.payOut = true; this.reputation = false; } /** * @inheritDoc */ serialize(): Serialized<StarknetSwapData> & {type: "strk"} { return { type: "strk", offerer: this.offerer, claimer: this.claimer, token: this.token, refundHandler: this.refundHandler, claimHandler: this.claimHandler, payOut: this.payOut, payIn: this.payIn, reputation: this.reputation, sequence: this.sequence?.toString(10), claimData: this.claimData, refundData: this.refundData, amount: this.amount?.toString(10), feeToken: this.feeToken, securityDeposit: this.securityDeposit?.toString(10), claimerBounty: this.claimerBounty?.toString(10), kind: this.kind, extraData: this.extraData, successAction: this.successAction==null ? undefined : { executionHash: this.successAction.executionHash, executionExpiry: this.successAction.executionExpiry.toString(10), executionFee: this.successAction.executionFee.toString(10) } } } /** * @inheritDoc */ getAmount(): bigint { return this.amount; } /** * @inheritDoc */ getToken(): string { return this.token; } /** * @inheritDoc */ isToken(token: string): boolean { return this.token.toLowerCase()===token.toLowerCase(); } /** * @inheritDoc */ getType(): ChainSwapType { return this.kind; } /** * @inheritDoc */ getExpiry(): bigint { return TimelockRefundHandler.getExpiry(this); } /** * @inheritDoc */ isPayIn(): boolean { return this.payIn; } /** * @inheritDoc */ isPayOut(): boolean { return this.payOut; } /** * @inheritDoc */ isTrackingReputation(): boolean { return this.reputation; } /** * @inheritDoc */ getEscrowHash(): string { const amountValue = cairo.uint256("0x"+this.amount.toString(16)); const securityDepositValue = cairo.uint256("0x"+this.securityDeposit.toString(16)); const claimerBountyValue = cairo.uint256("0x"+this.claimerBounty.toString(16)); const elements = [ this.offerer, this.claimer, this.token, this.refundHandler, this.claimHandler, this.getFlags(), this.claimData, this.refundData, amountValue.low, amountValue.high, this.feeToken, securityDepositValue.low, securityDepositValue.high, claimerBountyValue.low, claimerBountyValue.high ]; if(this.successAction!=null) { elements.push(this.successAction.executionHash); elements.push(this.successAction.executionExpiry); const feeValue = cairo.uint256("0x"+this.successAction.executionFee.toString(16)); elements.push(feeValue.low, feeValue.high); } let escrowHash = hash.computePoseidonHashOnElements(elements); if(escrowHash.startsWith("0x")) escrowHash = escrowHash.slice(2); return escrowHash.padStart(64, "0"); } /** * @inheritDoc */ getClaimHash(): string { let hash = this.claimData; if(hash.startsWith("0x")) hash = hash.slice(2); return hash.padStart(64, "0"); } /** * @inheritDoc */ getSequence(): bigint { return this.sequence; } /** * @inheritDoc */ getConfirmationsHint(): number | null { if(this.extraData==null) return null; if(this.extraData.length!=84) return null; return parseInt(this.extraData.slice(80), 16); } /** * @inheritDoc */ getNonceHint(): bigint | null { if(this.extraData==null) return null; if(this.extraData.length!=84) return null; return BigInt("0x"+this.extraData.slice(64, 80)); } /** * @inheritDoc */ getTxoHashHint(): string | null { if(this.extraData==null) return null; if(this.extraData.length!=84) return null; return this.extraData.slice(0, 64); } /** * @inheritDoc */ getExtraData(): string | null { return this.extraData ?? null; } /** * @inheritDoc */ setExtraData(extraData: string): void { this.extraData = extraData; } /** * @inheritDoc */ getSecurityDeposit() { return this.securityDeposit; } /** * @inheritDoc */ getClaimerBounty() { return this.claimerBounty; } /** * @inheritDoc */ getTotalDeposit() { return this.claimerBounty < this.securityDeposit ? this.securityDeposit : this.claimerBounty; } /** * @inheritDoc */ getDepositToken() { return this.feeToken; } /** * @inheritDoc */ isDepositToken(token: string): boolean { if(!token.startsWith("0x")) token = "0x"+token; return toHex(this.feeToken)===toHex(token); } /** * @inheritDoc */ isClaimer(address: string) { if(!address.startsWith("0x")) address = "0x"+address; return toHex(this.claimer)===toHex(address); } /** * @inheritDoc */ isOfferer(address: string) { if(!address.startsWith("0x")) address = "0x"+address; return toHex(this.offerer)===toHex(address); } /** * Checks whether the passed address is specified as a claim handler for the swap * * @param address */ isClaimHandler(address: string): boolean { if(!address.startsWith("0x")) address = "0x"+address; return toHex(this.claimHandler)===toHex(address); } /** * Checks if the passed data match the swap's claim data * * @param data */ isClaimData(data: string): boolean { if(!data.startsWith("0x")) data = "0x"+data; return toHex(this.claimData)===toHex(data); } /** * @inheritDoc */ equals(other: StarknetSwapData): boolean { return other.offerer.toLowerCase()===this.offerer.toLowerCase() && other.claimer.toLowerCase()===this.claimer.toLowerCase() && other.token.toLowerCase()===this.token.toLowerCase() && other.refundHandler.toLowerCase()===this.refundHandler.toLowerCase() && other.claimHandler.toLowerCase()===this.claimHandler.toLowerCase() && other.payIn===this.payIn && other.payOut===this.payOut && other.reputation===this.reputation && this.sequence === other.sequence && other.claimData.toLowerCase()===this.claimData.toLowerCase() && other.refundData.toLowerCase()===this.refundData.toLowerCase() && other.amount === this.amount && other.securityDeposit === this.securityDeposit && other.claimerBounty === this.claimerBounty && successActionEquals(other.successAction, this.successAction) } /** * Serializes the swap data into starknet.js struct representation */ toEscrowStruct(): StarknetSwapDataType { return { offerer: this.offerer, claimer: this.claimer, token: this.token, refund_handler: this.refundHandler, claim_handler: this.claimHandler, flags: this.getFlags(), claim_data: this.claimData, refund_data: this.refundData, amount: cairo.uint256(toBigInt(this.amount)), fee_token: this.feeToken, security_deposit: cairo.uint256(toBigInt(this.securityDeposit)), claimer_bounty: cairo.uint256(toBigInt(this.claimerBounty)), success_action: new CairoOption( this.successAction==null ? CairoOptionVariant.None : CairoOptionVariant.Some, this.successAction==null ? undefined : { hash: this.successAction.executionHash, expiry: this.successAction.executionExpiry, fee: cairo.uint256(this.successAction.executionFee) } ) as StarknetSwapDataType["success_action"] } } /** * Deserializes swap data from the provided felt252 array, * * @param span a felt252 array of length 16 or more * @param claimHandlerImpl Claim handler implementation to parse the swap type, this is checked * for internally and this throws an error if the passed `claimHandlerImpl` doesn't match the * claim handler address in the passed swap data */ static fromSerializedFeltArray(span: BigNumberish[], claimHandlerImpl: IClaimHandler<any, any>) { if(span.length < 16) throw new Error("Invalid length of serialized starknet swap data!"); const offerer = toHex(span.shift()!); const claimer = toHex(span.shift()!); const token = toHex(span.shift()!); const refundHandler = toHex(span.shift()!); const claimHandler = toHex(span.shift()!); const {payOut, payIn, reputation, sequence} = StarknetSwapData.toFlags(span.shift()!); const claimData = toHex(span.shift()!); const refundData = toHex(span.shift()!); const amount = toBigInt({low: span.shift()!, high: span.shift()!}); const feeToken = toHex(span.shift()!); const securityDeposit = toBigInt({low: span.shift()!, high: span.shift()!}); const claimerBounty = toBigInt({low: span.shift()!, high: span.shift()!}); const hasSuccessAction = toBigInt(span.shift()!) === 0n; let successAction: StarknetSuccessAction | undefined = undefined; if(hasSuccessAction) { if(span.length < 4) throw new Error("Invalid length of serialized starknet swap data!"); successAction = { executionHash: toHex(span.shift()!), executionExpiry: toBigInt(span.shift()!), executionFee: toBigInt({low: span.shift()!, high: span.shift()!}) } } const swapData = new StarknetSwapData({ offerer, claimer, token, refundHandler, claimHandler, payOut, payIn, reputation, sequence, claimData, refundData, amount, feeToken, securityDeposit, claimerBounty, kind: claimHandlerImpl.getType(), successAction }); if(!swapData.isClaimHandler(claimHandlerImpl.address)) throw new Error(`Invalid swap handler impl passed! Passed: ${claimHandlerImpl.address}, actual: ${swapData.claimHandler}`); return swapData; } /** * @inheritDoc */ hasSuccessAction(): boolean { return this.successAction != null; } /** * @inheritDoc */ getEscrowStruct(): any { return replaceBigInts(this.toEscrowStruct()); } } SwapData.deserializers["strk"] = StarknetSwapData;