Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Withdrawals #456

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions sdk/src/acre.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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(
`0x${accountEthereumAddress.identifierHex}`,
ethersProvider,
)
const signer = new VoidSigner(accountEthereumAddress, ethersProvider)

const contracts = getEthereumContracts(signer, ethereumNetwork)

Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions sdk/src/lib/bitcoin/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
throw new Error("Method not implemented.")
}
}
1 change: 1 addition & 0 deletions sdk/src/lib/bitcoin/providers/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ export interface BitcoinProvider {
* @returns Bitcoin address selected by the user.
*/
getAddress(): Promise<string>
getPublicKey(): Promise<string>
}
19 changes: 19 additions & 0 deletions sdk/src/lib/contracts/stbtc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ChainIdentifier } from "./chain-identifier"

export interface StBTC {
/**
* @returns The chain-specific identifier of this contract.
*/
getAddress(): ChainIdentifier

/**
* @returns Total tBTC amount under stBTC contract management in 1e18
* precision.
Expand All @@ -26,4 +31,18 @@ export interface StBTC {
* @returns Deposit fee.
*/
calculateDepositFee(amount: bigint): Promise<bigint>

/**
* 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<bigint>

encodeRedeemToBitcoinFunctionData(
stbtcAmount: bigint,
tbtcRedemptionData: string,
): string

convertToShares(amount: bigint): Promise<bigint>
}
36 changes: 33 additions & 3 deletions sdk/src/lib/ethereum/stbtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -35,6 +41,15 @@ class EthereumStBTC
}

super(config, artifact)

this.#bitcoinRedeemerContractAddress = bitcoinRedeemerContractAddress
}

/**
* @see {StBTC#previewRedeem}
*/
previewRedeem(sharesAmount: bigint): Promise<bigint> {
return this.instance.previewRedeem(sharesAmount)
}

/**
Expand All @@ -58,14 +73,18 @@ class EthereumStBTC
return this.instance.assetsBalanceOf(`0x${identifier.identifierHex}`)
}

convertToShares(amount: bigint): Promise<bigint> {
return this.instance.convertToShares(amount)
}

/**
* @see {StBTC#calculateDepositFee}
*/
async calculateDepositFee(amount: bigint): Promise<bigint> {
async calculateDepositFee(tbtcAmount: bigint): Promise<bigint> {
const entryFeeBasisPoints = await this.#getEntryFeeBasisPoints()

return (
(amount * entryFeeBasisPoints) /
(tbtcAmount * entryFeeBasisPoints) /
(entryFeeBasisPoints + this.#BASIS_POINT_SCALE)
)
}
Expand All @@ -79,6 +98,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
Expand Down
49 changes: 40 additions & 9 deletions sdk/src/modules/account.ts
Original file line number Diff line number Diff line change
@@ -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 { fromSatoshi, 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 = {
/**
Expand Down Expand Up @@ -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,
)
}

/**
Expand Down Expand Up @@ -102,6 +116,23 @@ export default class Account {
return new StakeInitialization(tbtcDeposit)
}

/**
* Initializes the withdrawal process.
* @param amount Bitcoin amount to withdraw in 1e8 satoshi precision.

* @returns Hash of the transaction that initiated the withdrawal.
*/
async initializeWithdrawal(
amount: bigint,
bitcoinSignMessageFn: (message: string) => Promise<string>,
): Promise<string> {
return this.#withdrawalService.initiateWithdrawal(
this.#bitcoinAddress,
fromSatoshi(amount),
bitcoinSignMessageFn,
)
}

/**
* @returns Balance of the account's stBTC shares (in 1e18 precision).
*/
Expand Down
35 changes: 34 additions & 1 deletion sdk/src/modules/tbtc/Tbtc.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -145,4 +150,32 @@ export default class Tbtc {
timestamp: deposit.createdAt,
}))
}

async initiateRedemption(
redeemerAddress: string,
destinationBitcoinAddress: string,
tbtcAmount: bigint,
handleRedemptionRequestFn: (redemptionData: string) => Promise<string>,
): Promise<string> {
const tbtcRedeemer: TbtcRedeemerProxy = {
redeemerAddress(): ChainIdentifier {
return EthereumAddress.from(redeemerAddress)
},
async requestRedemption(redemptionData: Hex): Promise<Hex> {
const txHash = await handleRedemptionRequestFn(
redemptionData.toPrefixedString(),
)
return Hex.from(txHash)
},
}

const { targetChainTxHash } =
await this.#tbtcSdk.redemptions.requestRedemptionWithProxy(
destinationBitcoinAddress,
tbtcAmount,
tbtcRedeemer,
)

return targetChainTxHash.toPrefixedString()
}
}
88 changes: 88 additions & 0 deletions sdk/src/modules/withdrawal-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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,
tbtcAmount: bigint,
bitcoinSignMessageFn: (message: string) => Promise<string>,
): Promise<string> {
const sharesAmount = await this.#contracts.stBTC.convertToShares(tbtcAmount)

return this.initiateRedemption(
destinationBitcoinAddress,
sharesAmount,
bitcoinSignMessageFn,
)
}

async initiateRedemption(
destinationBitcoinAddress: string,
sharesAmount: bigint,
bitcoinSignMessageFn: (message: string) => Promise<string>,
): Promise<string> {
// 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,
)

return transactionHash
}

return this.#tbtc.initiateRedemption(
this.#account.ethereumAddress,
destinationBitcoinAddress,
tbtcAmount,
requestRedemptionWithOrangeKit,
)
}
}
Loading