From 0237d6a0cc1a01fc9a0aacd683b0f8d169259d61 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 7 Jun 2024 15:06:08 +0200 Subject: [PATCH] WIP: Integrate withdrawals with OrangeKit and tBTC SDK This branch expects two dependencies changes that are not yet merged: - @keep-network/tbtc-v2.ts: https://github.com/keep-network/tbtc-v2/pull/814 - @orangekit/sdk: https://github.com/thesis/orangekit/pull/83 --- sdk/src/acre.ts | 21 +++--- sdk/src/lib/bitcoin/address.ts | 2 + .../ledger-live-wallet-api-provider.ts | 5 ++ sdk/src/lib/bitcoin/providers/provider.ts | 1 + sdk/src/lib/contracts/stbtc.ts | 17 +++++ sdk/src/lib/ethereum/stbtc.ts | 32 +++++++- sdk/src/modules/account.ts | 47 ++++++++++-- sdk/src/modules/tbtc/Tbtc.ts | 35 ++++++++- sdk/src/modules/withdrawal-service.ts | 75 +++++++++++++++++++ 9 files changed, 211 insertions(+), 24 deletions(-) create mode 100644 sdk/src/modules/withdrawal-service.ts diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 9709c0485..7b734d31e 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -1,11 +1,7 @@ import { OrangeKitSdk } from "@orangekit/sdk" import { getDefaultProvider } from "ethers" import { AcreContracts } from "./lib/contracts" -import { - EthereumAddress, - EthereumNetwork, - getEthereumContracts, -} from "./lib/ethereum" +import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" import Account from "./modules/account" import Tbtc from "./modules/tbtc" import { VoidSigner } from "./lib/utils" @@ -71,14 +67,12 @@ class Acre { ) const accountBitcoinAddress = await bitcoinProvider.getAddress() - const accountEthereumAddress = EthereumAddress.from( - await orangeKit.predictAddress(accountBitcoinAddress), + const accountEthereumAddress = await orangeKit.predictAddress( + accountBitcoinAddress, ) + const accountPublicKey = await bitcoinProvider.getPublicKey() - const signer = new VoidSigner( - accountEthereumAddress.identifierHex, - ethersProvider, - ) + const signer = new VoidSigner(accountEthereumAddress, ethersProvider) const contracts = getEthereumContracts(signer, ethereumNetwork) @@ -88,15 +82,18 @@ class Acre { tbtcApiUrl, contracts.bitcoinDepositor, ) + const subgraph = new AcreSubgraphApi( // TODO: Set correct url based on the network "https://api.studio.thegraph.com/query/73600/acre/version/latest", ) - const account = new Account(contracts, tbtc, subgraph, { + const account = new Account(contracts, tbtc, subgraph, orangeKit, { bitcoinAddress: accountBitcoinAddress, ethereumAddress: accountEthereumAddress, + publicKey: accountPublicKey, }) + const protocol = new Protocol(contracts) return new Acre( diff --git a/sdk/src/lib/bitcoin/address.ts b/sdk/src/lib/bitcoin/address.ts index 884e210e0..ebdcb1698 100644 --- a/sdk/src/lib/bitcoin/address.ts +++ b/sdk/src/lib/bitcoin/address.ts @@ -4,6 +4,8 @@ import { BitcoinScriptUtils, } from "@keep-network/tbtc-v2.ts" +export type BitcoinAddress = string + /** * Checks if the address is of type P2PKH or P2WPKH. * @param address The address to be checked. diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index 4d0e1bc00..8b3a9d97d 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -97,4 +97,9 @@ export default class LedgerLiveWalletApiBitcoinProvider return address } + + // eslint-disable-next-line @typescript-eslint/require-await, class-methods-use-this + async getPublicKey(): Promise { + throw new Error("Method not implemented.") + } } diff --git a/sdk/src/lib/bitcoin/providers/provider.ts b/sdk/src/lib/bitcoin/providers/provider.ts index ef71d25f3..86a47a487 100644 --- a/sdk/src/lib/bitcoin/providers/provider.ts +++ b/sdk/src/lib/bitcoin/providers/provider.ts @@ -14,4 +14,5 @@ export interface BitcoinProvider { * @returns Bitcoin address selected by the user. */ getAddress(): Promise + getPublicKey(): Promise } diff --git a/sdk/src/lib/contracts/stbtc.ts b/sdk/src/lib/contracts/stbtc.ts index b94b71a34..db624a79d 100644 --- a/sdk/src/lib/contracts/stbtc.ts +++ b/sdk/src/lib/contracts/stbtc.ts @@ -1,6 +1,11 @@ import { ChainIdentifier } from "./chain-identifier" export interface StBTC { + /** + * @returns The chain-specific identifier of this contract. + */ + getAddress(): ChainIdentifier + /** * @param identifier The generic chain identifier. * @returns Value of the basis for calculating final BTC balance. @@ -20,4 +25,16 @@ export interface StBTC { * @returns Deposit fee. */ calculateDepositFee(amount: bigint): Promise + + /** + * Calculates the amount of tBTC that will be redeemed for the given amount + * of stBTC shares. + * @param amount Amount of stBTC shares to redeem. + */ + previewRedeem(amount: bigint): Promise + + encodeRedeemToBitcoinFunctionData( + stbtcAmount: bigint, + tbtcRedemptionData: string, + ): string } diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts index e1304529e..f3c72e913 100644 --- a/sdk/src/lib/ethereum/stbtc.ts +++ b/sdk/src/lib/ethereum/stbtc.ts @@ -18,11 +18,17 @@ class EthereumStBTC { readonly #BASIS_POINT_SCALE = BigInt(1e4) + readonly #bitcoinRedeemerContractAddress: string + #cache: { entryFeeBasisPoints?: bigint } = { entryFeeBasisPoints: undefined } - constructor(config: EthersContractConfig, network: EthereumNetwork) { + constructor( + config: EthersContractConfig, + network: EthereumNetwork, + bitcoinRedeemerContractAddress: string, + ) { let artifact: EthersContractDeployment switch (network) { @@ -35,6 +41,15 @@ class EthereumStBTC } super(config, artifact) + + this.#bitcoinRedeemerContractAddress = bitcoinRedeemerContractAddress + } + + /** + * @see {StBTC#previewRedeem} + */ + previewRedeem(sharesAmount: bigint): Promise { + return this.instance.previewRedeem(sharesAmount) } /** @@ -54,11 +69,11 @@ class EthereumStBTC /** * @see {StBTC#calculateDepositFee} */ - async calculateDepositFee(amount: bigint): Promise { + async calculateDepositFee(tbtcAmount: bigint): Promise { const entryFeeBasisPoints = await this.#getEntryFeeBasisPoints() return ( - (amount * entryFeeBasisPoints) / + (tbtcAmount * entryFeeBasisPoints) / (entryFeeBasisPoints + this.#BASIS_POINT_SCALE) ) } @@ -72,6 +87,17 @@ class EthereumStBTC return this.#cache.entryFeeBasisPoints } + + encodeRedeemToBitcoinFunctionData( + sharesAmount: bigint, + tbtcRedemptionData: string, + ): string { + return this.instance.interface.encodeFunctionData("approveAndCall", [ + this.#bitcoinRedeemerContractAddress, + sharesAmount, + tbtcRedemptionData, + ]) + } } // eslint-disable-next-line import/prefer-default-export diff --git a/sdk/src/modules/account.ts b/sdk/src/modules/account.ts index 0f7480748..99865ff99 100644 --- a/sdk/src/modules/account.ts +++ b/sdk/src/modules/account.ts @@ -1,14 +1,15 @@ -import { AcreContracts, ChainIdentifier } from "../lib/contracts" +import { OrangeKitSdk } from "@orangekit/sdk" +import { AcreContracts } from "../lib/contracts" import StakeInitialization from "./staking" import { toSatoshi } from "../lib/utils" import Tbtc from "./tbtc" import AcreSubgraphApi from "../lib/api/AcreSubgraphApi" import { DepositStatus } from "../lib/api/TbtcApi" - -export { DepositReceipt } from "./tbtc" +import WithdrawalService from "./withdrawal-service" +import { EthereumAddress } from "../lib/ethereum" /** - * Represents the deposit data. + * Represents the deposit data */ export type Deposit = { /** @@ -56,19 +57,32 @@ export default class Account { readonly #bitcoinAddress: string - readonly #ethereumAddress: ChainIdentifier + readonly #ethereumAddress: EthereumAddress + + readonly #withdrawalService: WithdrawalService constructor( contracts: AcreContracts, tbtc: Tbtc, acreSubgraphApi: AcreSubgraphApi, - account: { bitcoinAddress: string; ethereumAddress: ChainIdentifier }, + orangeKit: OrangeKitSdk, + accountData: { + bitcoinAddress: string + ethereumAddress: string + publicKey: string + }, ) { this.#contracts = contracts this.#tbtc = tbtc this.#acreSubgraphApi = acreSubgraphApi - this.#bitcoinAddress = account.bitcoinAddress - this.#ethereumAddress = account.ethereumAddress + this.#bitcoinAddress = accountData.bitcoinAddress + this.#ethereumAddress = EthereumAddress.from(accountData.ethereumAddress) + this.#withdrawalService = new WithdrawalService( + contracts, + tbtc, + orangeKit, + accountData, + ) } /** @@ -102,6 +116,23 @@ export default class Account { return new StakeInitialization(tbtcDeposit) } + /** + * Initializes the withdrawal process. + * @param referral Data used for referral program. + + * @returns Object represents the deposit process. + */ + async initializeWithdrawal( + sharesAmount: bigint, + bitcoinSignMessageFn: (message: string) => Promise, + ) { + return this.#withdrawalService.initiateWithdrawal( + this.#bitcoinAddress, + sharesAmount, + bitcoinSignMessageFn, + ) + } + /** * @returns Value of the basis for calculating final BTC balance in 1e18 * precision. diff --git a/sdk/src/modules/tbtc/Tbtc.ts b/sdk/src/modules/tbtc/Tbtc.ts index 371165f1c..0f67455a1 100644 --- a/sdk/src/modules/tbtc/Tbtc.ts +++ b/sdk/src/modules/tbtc/Tbtc.ts @@ -1,4 +1,9 @@ -import { ChainIdentifier, TBTC as TbtcSdk } from "@keep-network/tbtc-v2.ts" +import { + ChainIdentifier, + TBTC as TbtcSdk, + RedeemerProxy as TbtcRedeemerProxy, + EthereumAddress, +} from "@keep-network/tbtc-v2.ts" import { ethers } from "ethers" import TbtcApi, { DepositStatus } from "../../lib/api/TbtcApi" @@ -145,4 +150,32 @@ export default class Tbtc { timestamp: deposit.createdAt, })) } + + async initiateRedemption( + redeemerAddress: string, + destinationBitcoinAddress: string, + tbtcAmount: bigint, + handleRedemptionRequestFn: (redemptionData: string) => Promise, + ): Promise { + const tbtcRedeemer: TbtcRedeemerProxy = { + redeemerAddress(): ChainIdentifier { + return EthereumAddress.from(redeemerAddress) + }, + async requestRedemption(redemptionData: Hex): Promise { + const txHash = await handleRedemptionRequestFn( + redemptionData.toPrefixedString(), + ) + return Hex.from(txHash) + }, + } + + const { targetChainTxHash } = + await this.#tbtcSdk.redemptions.requestRedemptionWithProxy( + destinationBitcoinAddress, + tbtcAmount, + tbtcRedeemer, + ) + + return targetChainTxHash.toPrefixedString() + } } diff --git a/sdk/src/modules/withdrawal-service.ts b/sdk/src/modules/withdrawal-service.ts new file mode 100644 index 000000000..f62e3131d --- /dev/null +++ b/sdk/src/modules/withdrawal-service.ts @@ -0,0 +1,75 @@ +import { OrangeKitSdk } from "@orangekit/sdk" + +import { AcreContracts } from "../lib/contracts" + +import Tbtc from "./tbtc" + +export default class WithdrawalService { + readonly #contracts: AcreContracts + + readonly #tbtc: Tbtc + + #orangeKitSdk: OrangeKitSdk + + #account: { + bitcoinAddress: string + ethereumAddress: string + publicKey: string + } + + constructor( + contracts: AcreContracts, + tbtc: Tbtc, + orangeKitSdk: OrangeKitSdk, + account: { + bitcoinAddress: string + ethereumAddress: string + publicKey: string + }, + ) { + this.#contracts = contracts + this.#tbtc = tbtc + this.#orangeKitSdk = orangeKitSdk + this.#account = account + } + + async initiateWithdrawal( + destinationBitcoinAddress: string, + sharesAmount: bigint, + bitcoinSignMessageFn: (message: string) => Promise, + ): Promise { + // Estimate the amount of tBTC released after redeeming stBTC tokens. + // The value is required by the tBTC SDK to determine the tBTC Wallet from + // which the Bitcoin will be bridged to the depositor's Bitcoin address. + const tbtcAmount = await this.#contracts.stBTC.previewRedeem(sharesAmount) + + const requestRedemptionWithOrangeKit = async ( + tbtcRedemptionData: string, + ) => { + const safeTxData = + this.#contracts.stBTC.encodeRedeemToBitcoinFunctionData( + sharesAmount, + tbtcRedemptionData, + ) + + const transactionHash = await this.#orangeKitSdk.sendTransaction( + this.#contracts.stBTC.getAddress(), + "0x0", + safeTxData, + this.#account.bitcoinAddress, + this.#account.publicKey, + bitcoinSignMessageFn, + ) + + // TODO: CHANGE RESULT + return transactionHash + } + + return this.#tbtc.initiateRedemption( + this.#account.ethereumAddress, + destinationBitcoinAddress, + tbtcAmount, + requestRedemptionWithOrangeKit, + ) + } +}