From bea20a562bb299a4e964b6ed830b03c645a5a8a5 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jun 2024 15:07:19 +0200 Subject: [PATCH 01/18] Implement `AcreIdentifierResolver` Implement the `toAcreIdentifier` function that returns the deposit owner/user identifier in the Acre protocol based on the bitcoin address. The function saves calculated identifier in cache so we do not need to recalculate it. --- sdk/src/lib/identifier-resolver/index.ts | 39 ++++++++++ sdk/test/lib/identifier-resolver.test.ts | 90 ++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 sdk/src/lib/identifier-resolver/index.ts create mode 100644 sdk/test/lib/identifier-resolver.test.ts diff --git a/sdk/src/lib/identifier-resolver/index.ts b/sdk/src/lib/identifier-resolver/index.ts new file mode 100644 index 000000000..aebf23f61 --- /dev/null +++ b/sdk/src/lib/identifier-resolver/index.ts @@ -0,0 +1,39 @@ +import { OrangeKitSdk } from "@orangekit/sdk" +import { EthereumAddress } from "@keep-network/tbtc-v2.ts" +import { BitcoinProvider } from "../bitcoin" +import { ChainIdentifier } from "../contracts" + +type AcreUserIdentifier = ChainIdentifier + +type BitcoinAddress = string + +type AcreIdentifierCache = Map + +const cache: AcreIdentifierCache = new Map() + +async function toAcreIdentifier( + bitcoinProvider: BitcoinProvider, + orangeKit: OrangeKitSdk, +): Promise { + const bitcoinAddress = await bitcoinProvider.getAddress() + + const cachedIdentifier = cache.get(bitcoinAddress) + + if (cachedIdentifier) { + return cachedIdentifier + } + + const identifier = EthereumAddress.from( + await orangeKit.predictAddress(bitcoinAddress), + ) + + cache.set(bitcoinAddress, identifier) + + return identifier +} + +const AcreIdentifierResolver = { + toAcreIdentifier, +} + +export default AcreIdentifierResolver diff --git a/sdk/test/lib/identifier-resolver.test.ts b/sdk/test/lib/identifier-resolver.test.ts new file mode 100644 index 000000000..3e58dee89 --- /dev/null +++ b/sdk/test/lib/identifier-resolver.test.ts @@ -0,0 +1,90 @@ +import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" +import { MockOrangeKitSdk } from "../utils/mock-orangekit" +import { ChainIdentifier, EthereumAddress } from "../../src" +import AcreIdentifierResolver from "../../src/lib/identifier-resolver" + +describe("AcreIdentifierResolver", () => { + describe("toAcreIdentifier", () => { + const bitcoinProvider = new MockBitcoinProvider() + const orangeKit = new MockOrangeKitSdk() + + describe("when the identifier not yet cached", () => { + const bitcoinAddress = "mjc2zGTypwNyDi4ZxGbBNnUA84bfgiwYc" + const identifier = EthereumAddress.from( + "0x766C69E751460070b160aF93Cbec51ccd77ff4A2", + ) + let result: ChainIdentifier + + beforeAll(async () => { + bitcoinProvider.getAddress.mockResolvedValue(bitcoinAddress) + orangeKit.predictAddress = jest + .fn() + .mockResolvedValue(`0x${identifier.identifierHex}`) + + result = await AcreIdentifierResolver.toAcreIdentifier( + bitcoinProvider, + // @ts-expect-error Error: Property '#private' is missing in type + // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. + orangeKit, + ) + }) + + afterAll(() => { + bitcoinProvider.getAddress.mockClear() + }) + + it("should get the bitcoin address", () => { + expect(bitcoinProvider.getAddress).toHaveBeenCalled() + }) + + it("should call orangekit to predict the address", () => { + expect(orangeKit.predictAddress).toHaveBeenCalledWith(bitcoinAddress) + }) + + it("should return the correct identifier", () => { + expect(result.equals(identifier)).toBeTruthy() + }) + }) + + describe("when the identifier is already cached", () => { + const bitcoinAddress = "mwyc4iaVjyL9xif9MyG7RuCvD3qizEiChY" + const identifier = EthereumAddress.from( + "0x8fC860a219A401BCe1Fb97EB510Efb35f59c8211", + ) + let result: ChainIdentifier + + beforeAll(async () => { + bitcoinProvider.getAddress.mockResolvedValue(bitcoinAddress) + orangeKit.predictAddress = jest + .fn() + .mockResolvedValue(`0x${identifier.identifierHex}`) + + await AcreIdentifierResolver.toAcreIdentifier( + bitcoinProvider, + // @ts-expect-error Error: Property '#private' is missing in type + // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. + orangeKit, + ) + + result = await AcreIdentifierResolver.toAcreIdentifier( + bitcoinProvider, + // @ts-expect-error Error: Property '#private' is missing in type + // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. + orangeKit, + ) + }) + + it("should get the bitcoin address", () => { + expect(bitcoinProvider.getAddress).toHaveBeenCalledTimes(2) + }) + + it("should call the orangekit SDK only once", () => { + expect(orangeKit.predictAddress).toHaveReturnedTimes(1) + }) + + it("should return the identifier from cache", () => { + expect(result.equals(identifier)).toBeTruthy() + }) + }) + }) +}) From ba78e7d1f4ba1ca70989c8a00a93c25069c1d840 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jun 2024 15:10:11 +0200 Subject: [PATCH 02/18] Use the `AcreIdentifierResolver` in other modules And update the `sharesBalance` and `estimatedBitcoinBalance` function to rely on the bitcoin address only. The consumer should operate with a Bitcoin address only. SDK should handle conversion from Bitcoin to EVM identifier behind the scenes. --- sdk/src/acre.ts | 13 +++++---- sdk/src/modules/staking/index.ts | 47 +++++++++++++++++++++----------- sdk/test/modules/staking.test.ts | 39 ++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 4c074a39a..bc87b1111 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -8,6 +8,7 @@ import { VoidSigner } from "./lib/utils" import { BitcoinProvider, BitcoinNetwork } from "./lib/bitcoin" import { getChainIdByNetwork } from "./lib/ethereum/network" import AcreSubgraphApi from "./lib/api/AcreSubgraphApi" +import AcreIdentifierResolver from "./lib/identifier-resolver" class Acre { readonly #tbtc: Tbtc @@ -66,13 +67,13 @@ class Acre { ethereumRpcUrl, ) - // TODO: Should we store this address in context so that we do not to - // recalculate it when necessary? - const depositOwnerEvmAddress = await orangeKit.predictAddress( - await bitcoinProvider.getAddress(), - ) + const depositOwnerEvmAddress = + await AcreIdentifierResolver.toAcreIdentifier(bitcoinProvider, orangeKit) - const signer = new VoidSigner(depositOwnerEvmAddress, ethersProvider) + const signer = new VoidSigner( + depositOwnerEvmAddress.identifierHex, + ethersProvider, + ) const contracts = getEthereumContracts(signer, ethereumNetwork) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index cc9b18547..d317f7dcf 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -1,12 +1,17 @@ -import { ChainIdentifier, EthereumAddress } from "@keep-network/tbtc-v2.ts" import { OrangeKitSdk } from "@orangekit/sdk" -import { AcreContracts, DepositFees } from "../../lib/contracts" +import { + AcreContracts, + DepositFees, + ChainIdentifier, +} from "../../lib/contracts" import { StakeInitialization } from "./stake-initialization" import { fromSatoshi, toSatoshi } from "../../lib/utils" import Tbtc from "../tbtc" import { BitcoinProvider } from "../../lib/bitcoin/providers" import AcreSubgraphApi from "../../lib/api/AcreSubgraphApi" import { DepositStatus } from "../../lib/api/TbtcApi" +import AcreIdentifierResolver from "../../lib/identifier-resolver" +import { Acre } from "../../acre" export { DepositReceipt } from "../tbtc" @@ -111,9 +116,11 @@ class StakingModule { // can create `EVMChainIdentifier` class and use it as a type in `modules` // and `lib`. Currently we support only `Ethereum` so here we force to // `EthereumAddress`. - const depositOwnerEvmAddress = EthereumAddress.from( - await this.#orangeKit.predictAddress(depositOwnerBitcoinAddress), - ) + const depositOwnerEvmAddress = + await AcreIdentifierResolver.toAcreIdentifier( + this.#bitcoinProvider, + this.#orangeKit, + ) // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if // address is not supported. @@ -136,19 +143,27 @@ class StakingModule { } /** - * @param identifier The generic chain identifier. * @returns Value of the basis for calculating final BTC balance. */ - sharesBalance(identifier: ChainIdentifier) { - return this.#contracts.stBTC.balanceOf(identifier) + async sharesBalance() { + return this.#contracts.stBTC.balanceOf( + await AcreIdentifierResolver.toAcreIdentifier( + this.#bitcoinProvider, + this.#orangeKit, + ), + ) } /** - * @param identifier The generic chain identifier. * @returns Maximum withdraw value. */ - estimatedBitcoinBalance(identifier: ChainIdentifier) { - return this.#contracts.stBTC.assetsBalanceOf(identifier) + async estimatedBitcoinBalance() { + return this.#contracts.stBTC.assetsBalanceOf( + await AcreIdentifierResolver.toAcreIdentifier( + this.#bitcoinProvider, + this.#orangeKit, + ), + ) } /** @@ -199,11 +214,11 @@ class StakingModule { * and finalized. */ async getDeposits(): Promise { - const bitcoinAddress = await this.#bitcoinProvider.getAddress() - - const depositOwnerEvmAddress = EthereumAddress.from( - await this.#orangeKit.predictAddress(bitcoinAddress), - ) + const depositOwnerEvmAddress = + await AcreIdentifierResolver.toAcreIdentifier( + this.#bitcoinProvider, + this.#orangeKit, + ) const subgraphData = await this.#acreSubgraphApi.getDepositsByOwner( depositOwnerEvmAddress, diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index 4abe14287..d47c87a48 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -1,5 +1,6 @@ import { BitcoinTxHash } from "@keep-network/tbtc-v2.ts" import { ethers } from "ethers" +import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts, StakingModule, @@ -9,6 +10,8 @@ import { DepositFees, DepositFee, DepositStatus, + ChainIdentifier, + BitcoinProvider, } from "../../src" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" @@ -17,6 +20,7 @@ import { MockTbtc } from "../utils/mock-tbtc" import { DepositReceipt } from "../../src/modules/tbtc" import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" import AcreSubgraphApi from "../../src/lib/api/AcreSubgraphApi" +import AcreIdentifierResolver from "../../src/lib/identifier-resolver" const stakingModuleData: { initializeDeposit: { @@ -337,11 +341,26 @@ describe("Staking", () => { const depositor = EthereumAddress.from(ethers.Wallet.createRandom().address) const expectedResult = 4294967295n + let spyOnAcreIdentifierResolver: jest.SpyInstance< + Promise, + [bitcoinProvider: BitcoinProvider, orangeKit: OrangeKitSdk] + > + let result: bigint beforeAll(async () => { contracts.stBTC.balanceOf = jest.fn().mockResolvedValue(expectedResult) - result = await staking.sharesBalance(depositor) + spyOnAcreIdentifierResolver = jest + .spyOn(AcreIdentifierResolver, "toAcreIdentifier") + .mockResolvedValueOnce(depositor) + result = await staking.sharesBalance() + }) + + it("should resolve the acre identifier", () => { + expect(spyOnAcreIdentifierResolver).toHaveBeenCalledWith( + bitcoinProvider, + orangeKit, + ) }) it("should get balance of stBTC", () => { @@ -357,11 +376,27 @@ describe("Staking", () => { const expectedResult = 4294967295n const depositor = EthereumAddress.from(ethers.Wallet.createRandom().address) let result: bigint + let spyOnAcreIdentifierResolver: jest.SpyInstance< + Promise, + [bitcoinProvider: BitcoinProvider, orangeKit: OrangeKitSdk] + > + beforeAll(async () => { contracts.stBTC.assetsBalanceOf = jest .fn() .mockResolvedValue(expectedResult) - result = await staking.estimatedBitcoinBalance(depositor) + spyOnAcreIdentifierResolver = jest + .spyOn(AcreIdentifierResolver, "toAcreIdentifier") + .mockResolvedValueOnce(depositor) + + result = await staking.estimatedBitcoinBalance() + }) + + it("should resolve the acre identifier", () => { + expect(spyOnAcreIdentifierResolver).toHaveBeenCalledWith( + bitcoinProvider, + orangeKit, + ) }) it("should get staker's balance of tBTC tokens in vault ", () => { From a27504b5710597678f6deeb652adbd540434c766 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jun 2024 15:30:08 +0200 Subject: [PATCH 03/18] Update the `useFetchBTCBalance` hook We no longer need to pass the chain identifier to the `sharesBalance` and `estimatedBitcoinBalance` - the SDK relies on the bitcoin provider implementation passed during the SDK initialization. --- dapp/src/hooks/sdk/useFetchBTCBalance.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/dapp/src/hooks/sdk/useFetchBTCBalance.ts b/dapp/src/hooks/sdk/useFetchBTCBalance.ts index 81d2708b2..606baa870 100644 --- a/dapp/src/hooks/sdk/useFetchBTCBalance.ts +++ b/dapp/src/hooks/sdk/useFetchBTCBalance.ts @@ -1,9 +1,7 @@ import { useEffect } from "react" -import { EthereumAddress } from "@acre-btc/sdk" import { useAcreContext } from "#/acre-react/hooks" import { logPromiseFailure } from "#/utils" import { setEstimatedBtcBalance, setSharesBalance } from "#/store/btc" -import { ZeroAddress } from "ethers" import { useAppDispatch } from "../store/useAppDispatch" export function useFetchBTCBalance() { @@ -14,11 +12,9 @@ export function useFetchBTCBalance() { const getBtcBalance = async () => { if (!isInitialized || !acre) return - // TODO: We should pass the Bitcoin address here once we update the SDK. - const chainIdentifier = EthereumAddress.from(ZeroAddress) - const sharesBalance = await acre.staking.sharesBalance(chainIdentifier) + const sharesBalance = await acre.staking.sharesBalance() const estimatedBitcoinBalance = - await acre.staking.estimatedBitcoinBalance(chainIdentifier) + await acre.staking.estimatedBitcoinBalance() dispatch(setSharesBalance(sharesBalance)) dispatch(setEstimatedBtcBalance(estimatedBitcoinBalance)) From daf1ce5571f7c56b4bac5a889b98f4409560f695 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 4 Jun 2024 16:08:29 +0200 Subject: [PATCH 04/18] Update the root `index.ts` file in SDK Do not export the `ethereum` lib components from the root `index.ts` file - we should keep the Ethereum-related stuff under the hood inside the SDK. --- sdk/src/index.ts | 1 - sdk/src/lib/identifier-resolver/index.ts | 2 +- sdk/test/lib/ethereum/data.ts | 2 +- sdk/test/lib/ethereum/stbtc.test.ts | 2 +- sdk/test/lib/ethereum/tbtc-depositor.test.ts | 4 ++-- sdk/test/lib/identifier-resolver.test.ts | 3 ++- sdk/test/modules/staking.test.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/src/index.ts b/sdk/src/index.ts index ae4eff2a0..bddaffa54 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -1,6 +1,5 @@ export * from "./lib/bitcoin" export * from "./lib/contracts" -export * from "./lib/ethereum" export * from "./lib/utils" export { DepositStatus } from "./lib/api/TbtcApi" diff --git a/sdk/src/lib/identifier-resolver/index.ts b/sdk/src/lib/identifier-resolver/index.ts index aebf23f61..95d69f75f 100644 --- a/sdk/src/lib/identifier-resolver/index.ts +++ b/sdk/src/lib/identifier-resolver/index.ts @@ -1,5 +1,5 @@ import { OrangeKitSdk } from "@orangekit/sdk" -import { EthereumAddress } from "@keep-network/tbtc-v2.ts" +import { EthereumAddress } from "../ethereum" import { BitcoinProvider } from "../bitcoin" import { ChainIdentifier } from "../contracts" diff --git a/sdk/test/lib/ethereum/data.ts b/sdk/test/lib/ethereum/data.ts index b4deab283..02eda08e9 100644 --- a/sdk/test/lib/ethereum/data.ts +++ b/sdk/test/lib/ethereum/data.ts @@ -1,4 +1,4 @@ -import { EthereumAddress } from "../../../src" +import { EthereumAddress } from "../../../src/lib/ethereum" // eslint-disable-next-line import/prefer-default-export export const extraDataValidTestData: { diff --git a/sdk/test/lib/ethereum/stbtc.test.ts b/sdk/test/lib/ethereum/stbtc.test.ts index 08c7dddc0..5ce10f31e 100644 --- a/sdk/test/lib/ethereum/stbtc.test.ts +++ b/sdk/test/lib/ethereum/stbtc.test.ts @@ -1,6 +1,6 @@ import ethers, { Contract } from "ethers" import { EthereumStBTC } from "../../../src/lib/ethereum/stbtc" -import { EthereumAddress, EthereumSigner } from "../../../src" +import { EthereumAddress, EthereumSigner } from "../../../src/lib/ethereum" jest.mock("ethers", (): object => ({ Contract: jest.fn(), diff --git a/sdk/test/lib/ethereum/tbtc-depositor.test.ts b/sdk/test/lib/ethereum/tbtc-depositor.test.ts index 8a2ca326e..6a1d502c3 100644 --- a/sdk/test/lib/ethereum/tbtc-depositor.test.ts +++ b/sdk/test/lib/ethereum/tbtc-depositor.test.ts @@ -3,8 +3,8 @@ import { EthereumBitcoinDepositor, EthereumAddress, EthereumSigner, - DepositFees, -} from "../../../src" +} from "../../../src/lib/ethereum" +import { DepositFees } from "../../../src" import { extraDataValidTestData } from "./data" jest.mock("ethers", (): object => ({ diff --git a/sdk/test/lib/identifier-resolver.test.ts b/sdk/test/lib/identifier-resolver.test.ts index 3e58dee89..8a780882c 100644 --- a/sdk/test/lib/identifier-resolver.test.ts +++ b/sdk/test/lib/identifier-resolver.test.ts @@ -1,6 +1,7 @@ import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" import { MockOrangeKitSdk } from "../utils/mock-orangekit" -import { ChainIdentifier, EthereumAddress } from "../../src" +import { EthereumAddress } from "../../src/lib/ethereum" +import { ChainIdentifier } from "../../src/lib/contracts/chain-identifier" import AcreIdentifierResolver from "../../src/lib/identifier-resolver" describe("AcreIdentifierResolver", () => { diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index 9ec8cc5b9..97f20fbdb 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -6,13 +6,13 @@ import { StakingModule, Hex, StakeInitialization, - EthereumAddress, DepositFees, DepositFee, DepositStatus, ChainIdentifier, BitcoinProvider, } from "../../src" +import { EthereumAddress } from "../../src/lib/ethereum" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" import { MockOrangeKitSdk } from "../utils/mock-orangekit" From 6c9a9de00cb0fee548e73a86ebd1d2cb4d9a7653 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 5 Jun 2024 10:06:10 +0200 Subject: [PATCH 05/18] Remove the `EthereumNetwork` type from dapp The `EthereumNetwork` is no longer available in the `@acre/sdk` package. The dapp should rely on the Bitcoin network only. --- dapp/src/constants/chains.ts | 5 +---- dapp/src/constants/currency.ts | 10 ---------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/dapp/src/constants/chains.ts b/dapp/src/constants/chains.ts index 7574621b9..03b0e326e 100644 --- a/dapp/src/constants/chains.ts +++ b/dapp/src/constants/chains.ts @@ -1,5 +1,5 @@ import { Chain } from "#/types" -import { EthereumNetwork, BitcoinNetwork } from "@acre-btc/sdk" +import { BitcoinNetwork } from "@acre-btc/sdk" const BLOCK_EXPLORER_TESTNET = { ethereum: { title: "Etherscan", url: "https://sepolia.etherscan.io" }, @@ -16,9 +16,6 @@ export const BLOCK_EXPLORER: Record = ? BLOCK_EXPLORER_TESTNET : BLOCK_EXPLORER_MAINNET -export const ETHEREUM_NETWORK: EthereumNetwork = - import.meta.env.VITE_USE_TESTNET === "true" ? "sepolia" : "mainnet" - export const BITCOIN_NETWORK: BitcoinNetwork = import.meta.env.VITE_USE_TESTNET === "true" ? BitcoinNetwork.Testnet diff --git a/dapp/src/constants/currency.ts b/dapp/src/constants/currency.ts index 483da1dbd..2b54fcbb8 100644 --- a/dapp/src/constants/currency.ts +++ b/dapp/src/constants/currency.ts @@ -1,6 +1,4 @@ import { Currency, CurrencyType } from "#/types" -import { EthereumNetwork } from "@acre-btc/sdk" -import { ETHEREUM_NETWORK } from "./chains" export const BITCOIN: Currency = { name: "Bitcoin", @@ -30,17 +28,9 @@ export const USD: Currency = { desiredDecimals: 2, } -const CURRENCY_ID_BY_ETHEREUM_NETWORK: Record = { - mainnet: "ethereum", - sepolia: "ethereum_sepolia", -} - export const CURRENCY_ID_BITCOIN = import.meta.env.VITE_USE_TESTNET === "true" ? "bitcoin_testnet" : "bitcoin" -export const CURRENCY_ID_ETHEREUM = - CURRENCY_ID_BY_ETHEREUM_NETWORK[ETHEREUM_NETWORK] - export const CURRENCIES_BY_TYPE: Record = { bitcoin: BITCOIN, ethereum: ETHEREUM, From 39c722ca4a867c8faeee16d393cfe96673a59800 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 5 Jun 2024 16:04:35 +0200 Subject: [PATCH 06/18] Rename the `staking` module to `account` In the `Account` module we want to expose the features related to a given bitcoin account. --- sdk/src/acre.ts | 30 ++- sdk/src/index.ts | 5 +- sdk/src/modules/account/index.ts | 234 ++++++++++++++++ sdk/src/modules/staking/index.ts | 250 +++--------------- .../modules/staking/stake-initialization.ts | 52 ---- .../{staking.test.ts => account.test.ts} | 80 ++---- 6 files changed, 314 insertions(+), 337 deletions(-) create mode 100644 sdk/src/modules/account/index.ts delete mode 100644 sdk/src/modules/staking/stake-initialization.ts rename sdk/test/modules/{staking.test.ts => account.test.ts} (87%) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index bc87b1111..deefbfe5e 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -2,7 +2,7 @@ import { OrangeKitSdk } from "@orangekit/sdk" import { getDefaultProvider } from "ethers" import { AcreContracts } from "./lib/contracts" import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" -import { StakingModule } from "./modules/staking" +import Account from "./modules/account" import Tbtc from "./modules/tbtc" import { VoidSigner } from "./lib/utils" import { BitcoinProvider, BitcoinNetwork } from "./lib/bitcoin" @@ -19,7 +19,7 @@ class Acre { public readonly contracts: AcreContracts - public readonly staking: StakingModule + public readonly account: Account readonly #acreSubgraph: AcreSubgraphApi @@ -29,19 +29,14 @@ class Acre { orangeKit: OrangeKitSdk, tbtc: Tbtc, acreSubgraphApi: AcreSubgraphApi, + account: Account, ) { this.contracts = contracts this.#tbtc = tbtc this.#orangeKit = orangeKit this.#acreSubgraph = acreSubgraphApi this.#bitcoinProvider = bitcoinProvider - this.staking = new StakingModule( - this.contracts, - this.#bitcoinProvider, - this.#orangeKit, - this.#tbtc, - this.#acreSubgraph, - ) + this.account = account } static async initialize( @@ -88,7 +83,22 @@ class Acre { "https://api.studio.thegraph.com/query/73600/acre/version/latest", ) - return new Acre(contracts, bitcoinProvider, orangeKit, tbtc, subgraph) + const account = await Account.initialize( + contracts, + bitcoinProvider, + orangeKit, + tbtc, + subgraph, + ) + + return new Acre( + contracts, + bitcoinProvider, + orangeKit, + tbtc, + subgraph, + account, + ) } } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index bddaffa54..a2769f1d7 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -3,6 +3,9 @@ export * from "./lib/contracts" export * from "./lib/utils" export { DepositStatus } from "./lib/api/TbtcApi" -export * from "./modules/staking" +export * from "./modules/account" +export { default as Account } from "./modules/account" + +export { default as StakeInitialization } from "./modules/staking" export * from "./acre" diff --git a/sdk/src/modules/account/index.ts b/sdk/src/modules/account/index.ts new file mode 100644 index 000000000..03f315c27 --- /dev/null +++ b/sdk/src/modules/account/index.ts @@ -0,0 +1,234 @@ +import { OrangeKitSdk } from "@orangekit/sdk" +import { + AcreContracts, + DepositFees, + ChainIdentifier, +} from "../../lib/contracts" +import StakeInitialization from "../staking" +import { fromSatoshi, toSatoshi } from "../../lib/utils" +import Tbtc from "../tbtc" +import { BitcoinProvider } from "../../lib/bitcoin/providers" +import AcreSubgraphApi from "../../lib/api/AcreSubgraphApi" +import { DepositStatus } from "../../lib/api/TbtcApi" + +import { EthereumAddress } from "../../lib/ethereum" + +export { DepositReceipt } from "../tbtc" + +/** + * Represents all total deposit fees grouped by network. + */ +export type DepositFee = { + tbtc: bigint + acre: bigint + total: bigint +} + +/** + * Represents the deposit data. + */ +export type Deposit = { + /** + * Unique deposit identifier represented as + * `keccak256(bitcoinFundingTxHash | fundingOutputIndex)`. + */ + id: string + /** + * Bitcoin transaction hash (or transaction ID) in the same byte order as + * used by the Bitcoin block explorers. + */ + txHash: string + /** + * Amount of Bitcoin funding transaction. + */ + amount: bigint + /** + * Status of the deposit. + */ + status: DepositStatus + /** + * Timestamp when the deposit was initialized. + */ + timestamp: number +} + +type AcreAccountIdentifier = ChainIdentifier + +/** + * Module exposing features related to the account. + */ +export default class Account { + /** + * Acre contracts. + */ + readonly #contracts: AcreContracts + + /** + * tBTC Module. + */ + readonly #tbtc: Tbtc + + /** + * Acre subgraph api. + */ + readonly #acreSubgraphApi: AcreSubgraphApi + + readonly #bitcoinAddress: string + + readonly #acreIdentifier: AcreAccountIdentifier + + static async initialize( + contracts: AcreContracts, + bitcoinProvider: BitcoinProvider, + orangeKit: OrangeKitSdk, + tbtc: Tbtc, + acreSubgraphApi: AcreSubgraphApi, + ) { + const bitcoinAddress = await bitcoinProvider.getAddress() + + const identifier = EthereumAddress.from( + await orangeKit.predictAddress(bitcoinAddress), + ) + + return new Account( + contracts, + tbtc, + acreSubgraphApi, + bitcoinAddress, + identifier, + ) + } + + private constructor( + contracts: AcreContracts, + tbtc: Tbtc, + acreSubgraphApi: AcreSubgraphApi, + bitcoinAddress: string, + acreIdentifier: AcreAccountIdentifier, + ) { + this.#contracts = contracts + this.#tbtc = tbtc + this.#acreSubgraphApi = acreSubgraphApi + this.#bitcoinAddress = bitcoinAddress + this.#acreIdentifier = acreIdentifier + } + + /** + * Initializes the Acre deposit process. + * @param referral Data used for referral program. + * @param bitcoinRecoveryAddress `P2PKH` or `P2WPKH` Bitcoin address that can + * be used for emergency recovery of the deposited funds. If + * `undefined` the bitcoin address from bitcoin provider is used as + * bitcoin recovery address - note that an address returned by bitcoin + * provider must then be `P2WPKH` or `P2PKH`. This property is + * available to let the consumer use `P2SH-P2WPKH` as the deposit owner + * and another tBTC-supported type (`P2WPKH`, `P2PKH`) address as the + * tBTC Bridge recovery address. + * @returns Object represents the deposit process. + */ + async initializeStake(referral: number, bitcoinRecoveryAddress?: string) { + // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if + // address is not supported. + const finalBitcoinRecoveryAddress = + bitcoinRecoveryAddress ?? this.#bitcoinAddress + + const tbtcDeposit = await this.#tbtc.initiateDeposit( + this.#acreIdentifier, + finalBitcoinRecoveryAddress, + referral, + ) + + return new StakeInitialization(tbtcDeposit) + } + + /** + * @returns Value of the basis for calculating final BTC balance. + */ + async sharesBalance() { + return this.#contracts.stBTC.balanceOf(this.#acreIdentifier) + } + + /** + * @returns Maximum withdraw value. + */ + async estimatedBitcoinBalance() { + return this.#contracts.stBTC.assetsBalanceOf(this.#acreIdentifier) + } + + /** + * Estimates the deposit fee based on the provided amount. + * @param amount Amount to deposit in satoshi. + * @returns Deposit fee grouped by tBTC and Acre networks in 1e8 satoshi + * precision and total deposit fee value. + */ + async estimateDepositFee(amount: bigint): Promise { + const amountInTokenPrecision = fromSatoshi(amount) + + const { acre: acreFees, tbtc: tbtcFees } = + await this.#contracts.bitcoinDepositor.calculateDepositFee( + amountInTokenPrecision, + ) + const depositFee = await this.#contracts.stBTC.calculateDepositFee( + amountInTokenPrecision, + ) + + const sumFeesByProtocol = < + T extends DepositFees["tbtc"] | DepositFees["acre"], + >( + fees: T, + ) => Object.values(fees).reduce((reducer, fee) => reducer + fee, 0n) + + const tbtc = toSatoshi(sumFeesByProtocol(tbtcFees)) + + const acre = toSatoshi(sumFeesByProtocol(acreFees)) + toSatoshi(depositFee) + + return { + tbtc, + acre, + total: tbtc + acre, + } + } + + /** + * @returns Minimum deposit amount in 1e8 satoshi precision. + */ + async minDepositAmount() { + const value = await this.#contracts.bitcoinDepositor.minDepositAmount() + return toSatoshi(value) + } + + /** + * @returns All deposits associated with the Bitcoin address that returns the + * Bitcoin Provider. They include all deposits: queued, initialized + * and finalized. + */ + async getDeposits(): Promise { + const subgraphData = await this.#acreSubgraphApi.getDepositsByOwner( + this.#acreIdentifier, + ) + + const initializedOrFinalizedDepositsMap = new Map( + subgraphData.map((data) => [data.depositKey, data]), + ) + + const tbtcData = await this.#tbtc.getDepositsByOwner(this.#acreIdentifier) + + return tbtcData.map((deposit) => { + const depositFromSubgraph = initializedOrFinalizedDepositsMap.get( + deposit.depositKey, + ) + + const amount = toSatoshi( + depositFromSubgraph?.initialAmount ?? deposit.initialAmount, + ) + + return { + id: deposit.depositKey, + txHash: deposit.txHash, + amount, + status: deposit.status, + timestamp: deposit.timestamp, + } + }) + } +} diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index e26efa0ad..b7507304b 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -1,242 +1,52 @@ -import { OrangeKitSdk } from "@orangekit/sdk" -import { AcreContracts, DepositFees } from "../../lib/contracts" -import StakeInitialization from "./stake-initialization" -import { fromSatoshi, toSatoshi } from "../../lib/utils" -import Tbtc from "../tbtc" -import { BitcoinProvider } from "../../lib/bitcoin/providers" -import AcreSubgraphApi from "../../lib/api/AcreSubgraphApi" -import { DepositStatus } from "../../lib/api/TbtcApi" -import AcreIdentifierResolver from "../../lib/identifier-resolver" +import TbtcDeposit from "../tbtc/Deposit" -export { DepositReceipt } from "../tbtc" +import type { DepositReceipt } from "../account" /** - * Represents all total deposit fees grouped by network. + * Represents an instance of the staking flow. Staking flow requires a few steps + * which should be done to stake BTC. */ -export type DepositFee = { - tbtc: bigint - acre: bigint - total: bigint -} - -/** - * Represents the deposit data. - */ -export type Deposit = { - /** - * Unique deposit identifier represented as - * `keccak256(bitcoinFundingTxHash | fundingOutputIndex)`. - */ - id: string - /** - * Bitcoin transaction hash (or transaction ID) in the same byte order as - * used by the Bitcoin block explorers. - */ - txHash: string +export default class StakeInitialization { /** - * Amount of Bitcoin funding transaction. + * Component representing an instance of the tBTC deposit process. */ - amount: bigint - /** - * Status of the deposit. - */ - status: DepositStatus - /** - * Timestamp when the deposit was initialized. - */ - timestamp: number -} + readonly #tbtcDeposit: TbtcDeposit -/** - * Module exposing features related to the staking. - */ -class StakingModule { - /** - * Acre contracts. - */ - readonly #contracts: AcreContracts - - /** - * Bitcoin provider to communicate with the wallet. - */ - readonly #bitcoinProvider: BitcoinProvider - - /** - * tBTC Module. - */ - readonly #tbtc: Tbtc - - /** - * OrangeKit SDK. - */ - readonly #orangeKit: OrangeKitSdk - - /** - * Acre subgraph api. - */ - readonly #acreSubgraphApi: AcreSubgraphApi - - constructor( - contracts: AcreContracts, - bitcoinProvider: BitcoinProvider, - orangeKit: OrangeKitSdk, - tbtc: Tbtc, - acreSubgraphApi: AcreSubgraphApi, - ) { - this.#contracts = contracts - this.#bitcoinProvider = bitcoinProvider - this.#tbtc = tbtc - this.#orangeKit = orangeKit - this.#acreSubgraphApi = acreSubgraphApi - } - - /** - * Initializes the Acre deposit process. - * @param referral Data used for referral program. - * @param bitcoinRecoveryAddress `P2PKH` or `P2WPKH` Bitcoin address that can - * be used for emergency recovery of the deposited funds. If - * `undefined` the bitcoin address from bitcoin provider is used as - * bitcoin recovery address - note that an address returned by bitcoin - * provider must then be `P2WPKH` or `P2PKH`. This property is - * available to let the consumer use `P2SH-P2WPKH` as the deposit owner - * and another tBTC-supported type (`P2WPKH`, `P2PKH`) address as the - * tBTC Bridge recovery address. - * @returns Object represents the deposit process. - */ - async initializeStake(referral: number, bitcoinRecoveryAddress?: string) { - const depositOwnerBitcoinAddress = await this.#bitcoinProvider.getAddress() - - // TODO: If we want to handle other chains we should create the wrapper for - // OrangeKit SDK to return `ChainIdentifier` from `predictAddress` fn. Or we - // can create `EVMChainIdentifier` class and use it as a type in `modules` - // and `lib`. Currently we support only `Ethereum` so here we force to - // `EthereumAddress`. - const depositOwnerEvmAddress = - await AcreIdentifierResolver.toAcreIdentifier( - this.#bitcoinProvider, - this.#orangeKit, - ) - - // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if - // address is not supported. - const finalBitcoinRecoveryAddress = - bitcoinRecoveryAddress ?? depositOwnerBitcoinAddress - - const tbtcDeposit = await this.#tbtc.initiateDeposit( - depositOwnerEvmAddress, - finalBitcoinRecoveryAddress, - referral, - ) - - return new StakeInitialization(tbtcDeposit) - } - - /** - * @returns Value of the basis for calculating final BTC balance. - */ - async sharesBalance() { - return this.#contracts.stBTC.balanceOf( - await AcreIdentifierResolver.toAcreIdentifier( - this.#bitcoinProvider, - this.#orangeKit, - ), - ) - } - - /** - * @returns Maximum withdraw value. - */ - async estimatedBitcoinBalance() { - return this.#contracts.stBTC.assetsBalanceOf( - await AcreIdentifierResolver.toAcreIdentifier( - this.#bitcoinProvider, - this.#orangeKit, - ), - ) + constructor(tbtcDeposit: TbtcDeposit) { + this.#tbtcDeposit = tbtcDeposit } /** - * Estimates the deposit fee based on the provided amount. - * @param amount Amount to deposit in satoshi. - * @returns Deposit fee grouped by tBTC and Acre networks in 1e8 satoshi - * precision and total deposit fee value. + * @dev It should be used as a first step of the staking flow and user should + * send BTC to returned Bitcoin address. + * @returns Bitcoin address corresponding to this deposit. */ - async estimateDepositFee(amount: bigint): Promise { - const amountInTokenPrecision = fromSatoshi(amount) - - const { acre: acreFees, tbtc: tbtcFees } = - await this.#contracts.bitcoinDepositor.calculateDepositFee( - amountInTokenPrecision, - ) - const depositFee = await this.#contracts.stBTC.calculateDepositFee( - amountInTokenPrecision, - ) - - const sumFeesByProtocol = < - T extends DepositFees["tbtc"] | DepositFees["acre"], - >( - fees: T, - ) => Object.values(fees).reduce((reducer, fee) => reducer + fee, 0n) - - const tbtc = toSatoshi(sumFeesByProtocol(tbtcFees)) - - const acre = toSatoshi(sumFeesByProtocol(acreFees)) + toSatoshi(depositFee) - - return { - tbtc, - acre, - total: tbtc + acre, - } + async getBitcoinAddress(): Promise { + return this.#tbtcDeposit.getBitcoinAddress() } /** - * @returns Minimum deposit amount in 1e8 satoshi precision. + * @returns Receipt corresponding to the tbtc deposit. */ - async minDepositAmount() { - const value = await this.#contracts.bitcoinDepositor.minDepositAmount() - return toSatoshi(value) + getDepositReceipt(): DepositReceipt { + return this.#tbtcDeposit.getReceipt() } /** - * @returns All deposits associated with the Bitcoin address that returns the - * Bitcoin Provider. They include all deposits: queued, initialized - * and finalized. + * Stakes BTC based on the Bitcoin funding transaction via BitcoinDepositor + * contract. It requires signed staking message, which means `stake` should be + * called after message signing. By default, it detects and uses the outpoint + * of the recent Bitcoin funding transaction and throws if such a transaction + * does not exist. + * @dev Use it as the last step of the staking flow. It requires signed + * staking message otherwise throws an error. + * @param options Optional options parameters to initialize stake. + * @see StakeOptions for more details. + * @returns Transaction hash of the stake initiation transaction. */ - async getDeposits(): Promise { - const depositOwnerEvmAddress = - await AcreIdentifierResolver.toAcreIdentifier( - this.#bitcoinProvider, - this.#orangeKit, - ) - - const subgraphData = await this.#acreSubgraphApi.getDepositsByOwner( - depositOwnerEvmAddress, - ) - - const initializedOrFinalizedDepositsMap = new Map( - subgraphData.map((data) => [data.depositKey, data]), - ) - - const tbtcData = await this.#tbtc.getDepositsByOwner(depositOwnerEvmAddress) + async stake(options = { retries: 5, backoffStepMs: 5_000 }): Promise { + await this.#tbtcDeposit.waitForFunding(options) - return tbtcData.map((deposit) => { - const depositFromSubgraph = initializedOrFinalizedDepositsMap.get( - deposit.depositKey, - ) - - const amount = toSatoshi( - depositFromSubgraph?.initialAmount ?? deposit.initialAmount, - ) - - return { - id: deposit.depositKey, - txHash: deposit.txHash, - amount, - status: deposit.status, - timestamp: deposit.timestamp, - } - }) + return this.#tbtcDeposit.createDeposit() } } - -export { StakingModule, StakeInitialization } diff --git a/sdk/src/modules/staking/stake-initialization.ts b/sdk/src/modules/staking/stake-initialization.ts deleted file mode 100644 index cd787d349..000000000 --- a/sdk/src/modules/staking/stake-initialization.ts +++ /dev/null @@ -1,52 +0,0 @@ -import TbtcDeposit from "../tbtc/Deposit" - -import type { DepositReceipt } from "." - -/** - * Represents an instance of the staking flow. Staking flow requires a few steps - * which should be done to stake BTC. - */ -export default class StakeInitialization { - /** - * Component representing an instance of the tBTC deposit process. - */ - readonly #tbtcDeposit: TbtcDeposit - - constructor(tbtcDeposit: TbtcDeposit) { - this.#tbtcDeposit = tbtcDeposit - } - - /** - * @dev It should be used as a first step of the staking flow and user should - * send BTC to returned Bitcoin address. - * @returns Bitcoin address corresponding to this deposit. - */ - async getBitcoinAddress(): Promise { - return this.#tbtcDeposit.getBitcoinAddress() - } - - /** - * @returns Receipt corresponding to the tbtc deposit. - */ - getDepositReceipt(): DepositReceipt { - return this.#tbtcDeposit.getReceipt() - } - - /** - * Stakes BTC based on the Bitcoin funding transaction via BitcoinDepositor - * contract. It requires signed staking message, which means `stake` should be - * called after message signing. By default, it detects and uses the outpoint - * of the recent Bitcoin funding transaction and throws if such a transaction - * does not exist. - * @dev Use it as the last step of the staking flow. It requires signed - * staking message otherwise throws an error. - * @param options Optional options parameters to initialize stake. - * @see StakeOptions for more details. - * @returns Transaction hash of the stake initiation transaction. - */ - async stake(options = { retries: 5, backoffStepMs: 5_000 }): Promise { - await this.#tbtcDeposit.waitForFunding(options) - - return this.#tbtcDeposit.createDeposit() - } -} diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/account.test.ts similarity index 87% rename from sdk/test/modules/staking.test.ts rename to sdk/test/modules/account.test.ts index 97f20fbdb..2516d0798 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/account.test.ts @@ -1,16 +1,12 @@ import { BitcoinTxHash } from "@keep-network/tbtc-v2.ts" -import { ethers } from "ethers" -import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts, - StakingModule, Hex, - StakeInitialization, + Account, DepositFees, DepositFee, DepositStatus, - ChainIdentifier, - BitcoinProvider, + StakeInitialization, } from "../../src" import { EthereumAddress } from "../../src/lib/ethereum" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" @@ -20,7 +16,6 @@ import { MockTbtc } from "../utils/mock-tbtc" import { DepositReceipt } from "../../src/modules/tbtc" import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" import AcreSubgraphApi from "../../src/lib/api/AcreSubgraphApi" -import AcreIdentifierResolver from "../../src/lib/identifier-resolver" const stakingModuleData: { initializeDeposit: { @@ -107,7 +102,7 @@ const stakingInitializationData: { }, } -describe("Staking", () => { +describe("Account", () => { const contracts: AcreContracts = new MockAcreContracts() const bitcoinProvider = new MockBitcoinProvider() const orangeKit = new MockOrangeKitSdk() @@ -125,15 +120,19 @@ describe("Staking", () => { .fn() .mockResolvedValue(`0x${predictedEthereumDepositorAddress.identifierHex}`) - const staking: StakingModule = new StakingModule( - contracts, - bitcoinProvider, - // @ts-expect-error Error: Property '#private' is missing in type - // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. - orangeKit, - tbtc, - acreSubgraph, - ) + let account: Account + + beforeEach(async () => { + account = await Account.initialize( + contracts, + bitcoinProvider, + // @ts-expect-error Error: Property '#private' is missing in type + // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. + orangeKit, + tbtc, + acreSubgraph, + ) + }) describe("initializeStake", () => { const { mockedDepositBTCAddress, referral, extraData } = @@ -179,7 +178,7 @@ describe("Staking", () => { tbtc.initiateDeposit = jest.fn().mockReturnValue(mockedDeposit) - result = await staking.initializeStake( + result = await account.initializeStake( referral, bitcoinRecoveryAddress, ) @@ -265,29 +264,16 @@ describe("Staking", () => { }) describe("sharesBalance", () => { - const depositor = EthereumAddress.from(ethers.Wallet.createRandom().address) + const depositor = predictedEthereumDepositorAddress const expectedResult = 4294967295n - let spyOnAcreIdentifierResolver: jest.SpyInstance< - Promise, - [bitcoinProvider: BitcoinProvider, orangeKit: OrangeKitSdk] - > let result: bigint beforeAll(async () => { contracts.stBTC.balanceOf = jest.fn().mockResolvedValue(expectedResult) - spyOnAcreIdentifierResolver = jest - .spyOn(AcreIdentifierResolver, "toAcreIdentifier") - .mockResolvedValueOnce(depositor) - result = await staking.sharesBalance() - }) - it("should resolve the acre identifier", () => { - expect(spyOnAcreIdentifierResolver).toHaveBeenCalledWith( - bitcoinProvider, - orangeKit, - ) + result = await account.sharesBalance() }) it("should get balance of stBTC", () => { @@ -301,32 +287,18 @@ describe("Staking", () => { describe("estimatedBitcoinBalance", () => { const expectedResult = 4294967295n - const depositor = EthereumAddress.from(ethers.Wallet.createRandom().address) + const depositor = predictedEthereumDepositorAddress let result: bigint - let spyOnAcreIdentifierResolver: jest.SpyInstance< - Promise, - [bitcoinProvider: BitcoinProvider, orangeKit: OrangeKitSdk] - > beforeAll(async () => { contracts.stBTC.assetsBalanceOf = jest .fn() .mockResolvedValue(expectedResult) - spyOnAcreIdentifierResolver = jest - .spyOn(AcreIdentifierResolver, "toAcreIdentifier") - .mockResolvedValueOnce(depositor) - result = await staking.estimatedBitcoinBalance() - }) - - it("should resolve the acre identifier", () => { - expect(spyOnAcreIdentifierResolver).toHaveBeenCalledWith( - bitcoinProvider, - orangeKit, - ) + result = await account.estimatedBitcoinBalance() }) - it("should get staker's balance of tBTC tokens in vault ", () => { + it("should get staker's balance of tBTC tokens in vault", () => { expect(contracts.stBTC.assetsBalanceOf).toHaveBeenCalledWith(depositor) }) @@ -359,7 +331,7 @@ describe("Staking", () => { .fn() .mockResolvedValue(stBTCDepositFee) - result = await staking.estimateDepositFee(amount) + result = await account.estimateDepositFee(amount) }) it("should convert provided amount from satoshi to token precision", () => { @@ -413,7 +385,7 @@ describe("Staking", () => { contracts.bitcoinDepositor.minDepositAmount = jest .fn() .mockResolvedValue(mockedResult) - result = await staking.minDepositAmount() + result = await account.minDepositAmount() }) it("should convert value to 1e8 satoshi precision", () => { @@ -483,14 +455,14 @@ describe("Staking", () => { }, ] - let result: Awaited> + let result: Awaited> beforeAll(async () => { tbtc.getDepositsByOwner = jest .fn() .mockResolvedValue([queuedDeposit, finalizedDeposit.tbtc]) - result = await staking.getDeposits() + result = await account.getDeposits() }) it("should get the bitcoin address from bitcoin provider", () => { From 5eeb31be8e8c71abb7630e7ee270137eb2d8b7a9 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 11:10:43 +0200 Subject: [PATCH 07/18] Simplify the `Account` module Require the bitcoin address and acre identifier in consructor. We can calculate the acre identifier only once in `Acre.initialize` function and pass it to the module. --- dapp/src/acre-react/hooks/useStakeFlow.ts | 2 +- dapp/src/hooks/sdk/useFetchBTCBalance.ts | 4 +- dapp/src/hooks/sdk/useFetchDeposits.ts | 2 +- .../src/hooks/sdk/useFetchMinDepositAmount.ts | 2 +- dapp/src/hooks/useTransactionFee.ts | 2 +- sdk/src/acre.ts | 18 +++---- sdk/src/lib/identifier-resolver/index.ts | 21 ++++++--- sdk/src/modules/account/index.ts | 44 +++-------------- sdk/test/lib/identifier-resolver.test.ts | 17 ++++--- sdk/test/modules/account.test.ts | 47 ++----------------- 10 files changed, 47 insertions(+), 112 deletions(-) diff --git a/dapp/src/acre-react/hooks/useStakeFlow.ts b/dapp/src/acre-react/hooks/useStakeFlow.ts index b5f61c295..af1262326 100644 --- a/dapp/src/acre-react/hooks/useStakeFlow.ts +++ b/dapp/src/acre-react/hooks/useStakeFlow.ts @@ -27,7 +27,7 @@ export function useStakeFlow(): UseStakeFlowReturn { async (referral: number, bitcoinRecoveryAddress?: string) => { if (!acre || !isInitialized) throw new Error("Acre SDK not defined") - const initializedStakeFlow = await acre.staking.initializeStake( + const initializedStakeFlow = await acre.account.initializeStake( referral, bitcoinRecoveryAddress, ) diff --git a/dapp/src/hooks/sdk/useFetchBTCBalance.ts b/dapp/src/hooks/sdk/useFetchBTCBalance.ts index 606baa870..615220cc4 100644 --- a/dapp/src/hooks/sdk/useFetchBTCBalance.ts +++ b/dapp/src/hooks/sdk/useFetchBTCBalance.ts @@ -12,9 +12,9 @@ export function useFetchBTCBalance() { const getBtcBalance = async () => { if (!isInitialized || !acre) return - const sharesBalance = await acre.staking.sharesBalance() + const sharesBalance = await acre.account.sharesBalance() const estimatedBitcoinBalance = - await acre.staking.estimatedBitcoinBalance() + await acre.account.estimatedBitcoinBalance() dispatch(setSharesBalance(sharesBalance)) dispatch(setEstimatedBtcBalance(estimatedBitcoinBalance)) diff --git a/dapp/src/hooks/sdk/useFetchDeposits.ts b/dapp/src/hooks/sdk/useFetchDeposits.ts index 54570285e..af97dff18 100644 --- a/dapp/src/hooks/sdk/useFetchDeposits.ts +++ b/dapp/src/hooks/sdk/useFetchDeposits.ts @@ -12,7 +12,7 @@ export function useFetchDeposits() { return useCallback(async () => { if (!acre) return - const result: Activity[] = (await acre.staking.getDeposits()).map( + const result: Activity[] = (await acre.account.getDeposits()).map( (deposit) => ({ ...deposit, status: diff --git a/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts b/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts index 835de9918..1c8f5b24e 100644 --- a/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts +++ b/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts @@ -12,7 +12,7 @@ export function useFetchMinDepositAmount() { if (!isInitialized || !acre) return const fetchMinDepositAmount = async () => { - const minDepositAmount = await acre.staking.minDepositAmount() + const minDepositAmount = await acre.account.minDepositAmount() dispatch(setMinDepositAmount(minDepositAmount)) } diff --git a/dapp/src/hooks/useTransactionFee.ts b/dapp/src/hooks/useTransactionFee.ts index b650d7fbe..1cc8b4e08 100644 --- a/dapp/src/hooks/useTransactionFee.ts +++ b/dapp/src/hooks/useTransactionFee.ts @@ -21,7 +21,7 @@ export function useTransactionFee(amount?: bigint) { } else { const getEstimatedDepositFee = async () => { if (!acre) return - const fee = await acre.staking.estimateDepositFee(amount) + const fee = await acre.account.estimateDepositFee(amount) setDepositFee(fee) } diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index deefbfe5e..619170284 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -62,13 +62,10 @@ class Acre { ethereumRpcUrl, ) - const depositOwnerEvmAddress = + const { identifier: acreIdentifier, associatedBitcoinAddress } = await AcreIdentifierResolver.toAcreIdentifier(bitcoinProvider, orangeKit) - const signer = new VoidSigner( - depositOwnerEvmAddress.identifierHex, - ethersProvider, - ) + const signer = new VoidSigner(acreIdentifier.identifierHex, ethersProvider) const contracts = getEthereumContracts(signer, ethereumNetwork) @@ -83,13 +80,10 @@ class Acre { "https://api.studio.thegraph.com/query/73600/acre/version/latest", ) - const account = await Account.initialize( - contracts, - bitcoinProvider, - orangeKit, - tbtc, - subgraph, - ) + const account = new Account(contracts, tbtc, subgraph, { + bitcoinAddress: associatedBitcoinAddress, + acreIdentifier, + }) return new Acre( contracts, diff --git a/sdk/src/lib/identifier-resolver/index.ts b/sdk/src/lib/identifier-resolver/index.ts index 95d69f75f..5f443634d 100644 --- a/sdk/src/lib/identifier-resolver/index.ts +++ b/sdk/src/lib/identifier-resolver/index.ts @@ -3,24 +3,33 @@ import { EthereumAddress } from "../ethereum" import { BitcoinProvider } from "../bitcoin" import { ChainIdentifier } from "../contracts" -type AcreUserIdentifier = ChainIdentifier +export type AcreAccountIdentifier = ChainIdentifier type BitcoinAddress = string -type AcreIdentifierCache = Map +type AcreIdentifierCache = Map -const cache: AcreIdentifierCache = new Map() +const cache: AcreIdentifierCache = new Map< + BitcoinAddress, + AcreAccountIdentifier +>() async function toAcreIdentifier( bitcoinProvider: BitcoinProvider, orangeKit: OrangeKitSdk, -): Promise { +): Promise<{ + identifier: AcreAccountIdentifier + associatedBitcoinAddress: BitcoinAddress +}> { const bitcoinAddress = await bitcoinProvider.getAddress() const cachedIdentifier = cache.get(bitcoinAddress) if (cachedIdentifier) { - return cachedIdentifier + return { + identifier: cachedIdentifier, + associatedBitcoinAddress: bitcoinAddress, + } } const identifier = EthereumAddress.from( @@ -29,7 +38,7 @@ async function toAcreIdentifier( cache.set(bitcoinAddress, identifier) - return identifier + return { identifier, associatedBitcoinAddress: bitcoinAddress } } const AcreIdentifierResolver = { diff --git a/sdk/src/modules/account/index.ts b/sdk/src/modules/account/index.ts index 03f315c27..53b0f164d 100644 --- a/sdk/src/modules/account/index.ts +++ b/sdk/src/modules/account/index.ts @@ -1,17 +1,10 @@ -import { OrangeKitSdk } from "@orangekit/sdk" -import { - AcreContracts, - DepositFees, - ChainIdentifier, -} from "../../lib/contracts" +import { AcreContracts, DepositFees } from "../../lib/contracts" import StakeInitialization from "../staking" import { fromSatoshi, toSatoshi } from "../../lib/utils" import Tbtc from "../tbtc" -import { BitcoinProvider } from "../../lib/bitcoin/providers" import AcreSubgraphApi from "../../lib/api/AcreSubgraphApi" import { DepositStatus } from "../../lib/api/TbtcApi" - -import { EthereumAddress } from "../../lib/ethereum" +import { AcreAccountIdentifier } from "../../lib/identifier-resolver" export { DepositReceipt } from "../tbtc" @@ -52,8 +45,6 @@ export type Deposit = { timestamp: number } -type AcreAccountIdentifier = ChainIdentifier - /** * Module exposing features related to the account. */ @@ -77,40 +68,17 @@ export default class Account { readonly #acreIdentifier: AcreAccountIdentifier - static async initialize( - contracts: AcreContracts, - bitcoinProvider: BitcoinProvider, - orangeKit: OrangeKitSdk, - tbtc: Tbtc, - acreSubgraphApi: AcreSubgraphApi, - ) { - const bitcoinAddress = await bitcoinProvider.getAddress() - - const identifier = EthereumAddress.from( - await orangeKit.predictAddress(bitcoinAddress), - ) - - return new Account( - contracts, - tbtc, - acreSubgraphApi, - bitcoinAddress, - identifier, - ) - } - - private constructor( + constructor( contracts: AcreContracts, tbtc: Tbtc, acreSubgraphApi: AcreSubgraphApi, - bitcoinAddress: string, - acreIdentifier: AcreAccountIdentifier, + account: { bitcoinAddress: string; acreIdentifier: AcreAccountIdentifier }, ) { this.#contracts = contracts this.#tbtc = tbtc this.#acreSubgraphApi = acreSubgraphApi - this.#bitcoinAddress = bitcoinAddress - this.#acreIdentifier = acreIdentifier + this.#bitcoinAddress = account.bitcoinAddress + this.#acreIdentifier = account.acreIdentifier } /** diff --git a/sdk/test/lib/identifier-resolver.test.ts b/sdk/test/lib/identifier-resolver.test.ts index 8a780882c..a2f72224a 100644 --- a/sdk/test/lib/identifier-resolver.test.ts +++ b/sdk/test/lib/identifier-resolver.test.ts @@ -1,7 +1,6 @@ import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" import { MockOrangeKitSdk } from "../utils/mock-orangekit" import { EthereumAddress } from "../../src/lib/ethereum" -import { ChainIdentifier } from "../../src/lib/contracts/chain-identifier" import AcreIdentifierResolver from "../../src/lib/identifier-resolver" describe("AcreIdentifierResolver", () => { @@ -14,7 +13,9 @@ describe("AcreIdentifierResolver", () => { const identifier = EthereumAddress.from( "0x766C69E751460070b160aF93Cbec51ccd77ff4A2", ) - let result: ChainIdentifier + let result: Awaited< + ReturnType + > beforeAll(async () => { bitcoinProvider.getAddress.mockResolvedValue(bitcoinAddress) @@ -43,7 +44,8 @@ describe("AcreIdentifierResolver", () => { }) it("should return the correct identifier", () => { - expect(result.equals(identifier)).toBeTruthy() + expect(result.identifier.equals(identifier)).toBeTruthy() + expect(result.associatedBitcoinAddress).toBe(bitcoinAddress) }) }) @@ -52,7 +54,9 @@ describe("AcreIdentifierResolver", () => { const identifier = EthereumAddress.from( "0x8fC860a219A401BCe1Fb97EB510Efb35f59c8211", ) - let result: ChainIdentifier + let result: Awaited< + ReturnType + > beforeAll(async () => { bitcoinProvider.getAddress.mockResolvedValue(bitcoinAddress) @@ -83,8 +87,9 @@ describe("AcreIdentifierResolver", () => { expect(orangeKit.predictAddress).toHaveReturnedTimes(1) }) - it("should return the identifier from cache", () => { - expect(result.equals(identifier)).toBeTruthy() + it("should return the correct identifier", () => { + expect(result.identifier.equals(identifier)).toBeTruthy() + expect(result.associatedBitcoinAddress).toBe(bitcoinAddress) }) }) }) diff --git a/sdk/test/modules/account.test.ts b/sdk/test/modules/account.test.ts index 2516d0798..f532a43cf 100644 --- a/sdk/test/modules/account.test.ts +++ b/sdk/test/modules/account.test.ts @@ -11,10 +11,8 @@ import { import { EthereumAddress } from "../../src/lib/ethereum" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" -import { MockOrangeKitSdk } from "../utils/mock-orangekit" import { MockTbtc } from "../utils/mock-tbtc" import { DepositReceipt } from "../../src/modules/tbtc" -import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" import AcreSubgraphApi from "../../src/lib/api/AcreSubgraphApi" const stakingModuleData: { @@ -104,34 +102,15 @@ const stakingInitializationData: { describe("Account", () => { const contracts: AcreContracts = new MockAcreContracts() - const bitcoinProvider = new MockBitcoinProvider() - const orangeKit = new MockOrangeKitSdk() const tbtc = new MockTbtc() const acreSubgraph = new AcreSubgraphApi("test") const { bitcoinDepositorAddress, predictedEthereumDepositorAddress } = stakingModuleData.initializeDeposit - bitcoinProvider.getAddress.mockResolvedValue( - stakingModuleData.initializeDeposit.bitcoinDepositorAddress, - ) - - orangeKit.predictAddress = jest - .fn() - .mockResolvedValue(`0x${predictedEthereumDepositorAddress.identifierHex}`) - - let account: Account - - beforeEach(async () => { - account = await Account.initialize( - contracts, - bitcoinProvider, - // @ts-expect-error Error: Property '#private' is missing in type - // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. - orangeKit, - tbtc, - acreSubgraph, - ) + const account: Account = new Account(contracts, tbtc, acreSubgraph, { + bitcoinAddress: bitcoinDepositorAddress, + acreIdentifier: predictedEthereumDepositorAddress, }) describe("initializeStake", () => { @@ -184,16 +163,6 @@ describe("Account", () => { ) }) - it("should get the bitcoin address from bitcoin provider", () => { - expect(bitcoinProvider.getAddress).toHaveBeenCalled() - }) - - it("should get Ethereum depositor owner address", () => { - expect(orangeKit.predictAddress).toHaveBeenCalledWith( - bitcoinDepositorAddress, - ) - }) - it("should initiate tBTC deposit", () => { expect(tbtc.initiateDeposit).toHaveBeenCalledWith( predictedEthereumDepositorAddress, @@ -465,16 +434,6 @@ describe("Account", () => { result = await account.getDeposits() }) - it("should get the bitcoin address from bitcoin provider", () => { - expect(bitcoinProvider.getAddress).toHaveBeenCalled() - }) - - it("should get Ethereum depositor owner address", () => { - expect(orangeKit.predictAddress).toHaveBeenCalledWith( - bitcoinDepositorAddress, - ) - }) - it("should get deposits from subgraph", () => { expect(spyOnSubgraphGetDeposits).toHaveBeenCalledWith( predictedEthereumDepositorAddress, From c9e50525a2051233738461499667cfb0cd381eed Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 11:39:11 +0200 Subject: [PATCH 08/18] Create `Protocol` module Extract general protocol functions to a separate module. This module should provide general functions related to the Acre protocol such as: fees, min deposit/withdrawal amount, tvl etc. --- .../src/hooks/sdk/useFetchMinDepositAmount.ts | 2 +- dapp/src/hooks/useTransactionFee.ts | 2 +- sdk/src/acre.ts | 9 +- sdk/src/index.ts | 3 + sdk/src/modules/account/index.ts | 55 +------- sdk/src/modules/protocol/index.ts | 67 +++++++++ sdk/test/modules/account.test.ts | 128 ----------------- sdk/test/modules/protocol.test.ts | 131 ++++++++++++++++++ 8 files changed, 213 insertions(+), 184 deletions(-) create mode 100644 sdk/src/modules/protocol/index.ts create mode 100644 sdk/test/modules/protocol.test.ts diff --git a/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts b/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts index 1c8f5b24e..050878657 100644 --- a/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts +++ b/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts @@ -12,7 +12,7 @@ export function useFetchMinDepositAmount() { if (!isInitialized || !acre) return const fetchMinDepositAmount = async () => { - const minDepositAmount = await acre.account.minDepositAmount() + const minDepositAmount = await acre.protocol.minDepositAmount() dispatch(setMinDepositAmount(minDepositAmount)) } diff --git a/dapp/src/hooks/useTransactionFee.ts b/dapp/src/hooks/useTransactionFee.ts index 1cc8b4e08..b69ae971c 100644 --- a/dapp/src/hooks/useTransactionFee.ts +++ b/dapp/src/hooks/useTransactionFee.ts @@ -21,7 +21,7 @@ export function useTransactionFee(amount?: bigint) { } else { const getEstimatedDepositFee = async () => { if (!acre) return - const fee = await acre.account.estimateDepositFee(amount) + const fee = await acre.protocol.estimateDepositFee(amount) setDepositFee(fee) } diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 619170284..98118beb6 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -9,6 +9,7 @@ import { BitcoinProvider, BitcoinNetwork } from "./lib/bitcoin" import { getChainIdByNetwork } from "./lib/ethereum/network" import AcreSubgraphApi from "./lib/api/AcreSubgraphApi" import AcreIdentifierResolver from "./lib/identifier-resolver" +import Protocol from "./modules/protocol" class Acre { readonly #tbtc: Tbtc @@ -17,11 +18,13 @@ class Acre { readonly #bitcoinProvider: BitcoinProvider + readonly #acreSubgraph: AcreSubgraphApi + public readonly contracts: AcreContracts public readonly account: Account - readonly #acreSubgraph: AcreSubgraphApi + public readonly protocol: Protocol private constructor( contracts: AcreContracts, @@ -30,6 +33,7 @@ class Acre { tbtc: Tbtc, acreSubgraphApi: AcreSubgraphApi, account: Account, + protocol: Protocol, ) { this.contracts = contracts this.#tbtc = tbtc @@ -37,6 +41,7 @@ class Acre { this.#acreSubgraph = acreSubgraphApi this.#bitcoinProvider = bitcoinProvider this.account = account + this.protocol = protocol } static async initialize( @@ -84,6 +89,7 @@ class Acre { bitcoinAddress: associatedBitcoinAddress, acreIdentifier, }) + const protocol = new Protocol(contracts) return new Acre( contracts, @@ -92,6 +98,7 @@ class Acre { tbtc, subgraph, account, + protocol, ) } } diff --git a/sdk/src/index.ts b/sdk/src/index.ts index a2769f1d7..4c1f4f7ff 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -8,4 +8,7 @@ export { default as Account } from "./modules/account" export { default as StakeInitialization } from "./modules/staking" +export { default as Protocol } from "./modules/protocol" +export * from "./modules/protocol" + export * from "./acre" diff --git a/sdk/src/modules/account/index.ts b/sdk/src/modules/account/index.ts index 53b0f164d..6d5e2c90a 100644 --- a/sdk/src/modules/account/index.ts +++ b/sdk/src/modules/account/index.ts @@ -1,6 +1,6 @@ -import { AcreContracts, DepositFees } from "../../lib/contracts" +import { AcreContracts } from "../../lib/contracts" import StakeInitialization from "../staking" -import { fromSatoshi, toSatoshi } from "../../lib/utils" +import { toSatoshi } from "../../lib/utils" import Tbtc from "../tbtc" import AcreSubgraphApi from "../../lib/api/AcreSubgraphApi" import { DepositStatus } from "../../lib/api/TbtcApi" @@ -8,15 +8,6 @@ import { AcreAccountIdentifier } from "../../lib/identifier-resolver" export { DepositReceipt } from "../tbtc" -/** - * Represents all total deposit fees grouped by network. - */ -export type DepositFee = { - tbtc: bigint - acre: bigint - total: bigint -} - /** * Represents the deposit data. */ @@ -123,48 +114,6 @@ export default class Account { return this.#contracts.stBTC.assetsBalanceOf(this.#acreIdentifier) } - /** - * Estimates the deposit fee based on the provided amount. - * @param amount Amount to deposit in satoshi. - * @returns Deposit fee grouped by tBTC and Acre networks in 1e8 satoshi - * precision and total deposit fee value. - */ - async estimateDepositFee(amount: bigint): Promise { - const amountInTokenPrecision = fromSatoshi(amount) - - const { acre: acreFees, tbtc: tbtcFees } = - await this.#contracts.bitcoinDepositor.calculateDepositFee( - amountInTokenPrecision, - ) - const depositFee = await this.#contracts.stBTC.calculateDepositFee( - amountInTokenPrecision, - ) - - const sumFeesByProtocol = < - T extends DepositFees["tbtc"] | DepositFees["acre"], - >( - fees: T, - ) => Object.values(fees).reduce((reducer, fee) => reducer + fee, 0n) - - const tbtc = toSatoshi(sumFeesByProtocol(tbtcFees)) - - const acre = toSatoshi(sumFeesByProtocol(acreFees)) + toSatoshi(depositFee) - - return { - tbtc, - acre, - total: tbtc + acre, - } - } - - /** - * @returns Minimum deposit amount in 1e8 satoshi precision. - */ - async minDepositAmount() { - const value = await this.#contracts.bitcoinDepositor.minDepositAmount() - return toSatoshi(value) - } - /** * @returns All deposits associated with the Bitcoin address that returns the * Bitcoin Provider. They include all deposits: queued, initialized diff --git a/sdk/src/modules/protocol/index.ts b/sdk/src/modules/protocol/index.ts new file mode 100644 index 000000000..d4b1c9939 --- /dev/null +++ b/sdk/src/modules/protocol/index.ts @@ -0,0 +1,67 @@ +import { AcreContracts, DepositFees } from "../../lib/contracts" +import { fromSatoshi, toSatoshi } from "../../lib/utils" + +/** + * Represents all total deposit fees grouped by network. + */ +export type DepositFee = { + tbtc: bigint + acre: bigint + total: bigint +} + +/** + * Module exposing general functions related to the Acre protocol. + */ +export default class Protocol { + /** + * Acre contracts. + */ + readonly #contracts: AcreContracts + + constructor(contracts: AcreContracts) { + this.#contracts = contracts + } + + /** + * Estimates the deposit fee based on the provided amount. + * @param amount Amount to deposit in satoshi. + * @returns Deposit fee grouped by tBTC and Acre networks in 1e8 satoshi + * precision and total deposit fee value. + */ + async estimateDepositFee(amount: bigint): Promise { + const amountInTokenPrecision = fromSatoshi(amount) + + const { acre: acreFees, tbtc: tbtcFees } = + await this.#contracts.bitcoinDepositor.calculateDepositFee( + amountInTokenPrecision, + ) + const depositFee = await this.#contracts.stBTC.calculateDepositFee( + amountInTokenPrecision, + ) + + const sumFeesByProtocol = < + T extends DepositFees["tbtc"] | DepositFees["acre"], + >( + fees: T, + ) => Object.values(fees).reduce((reducer, fee) => reducer + fee, 0n) + + const tbtc = toSatoshi(sumFeesByProtocol(tbtcFees)) + + const acre = toSatoshi(sumFeesByProtocol(acreFees)) + toSatoshi(depositFee) + + return { + tbtc, + acre, + total: tbtc + acre, + } + } + + /** + * @returns Minimum deposit amount in 1e8 satoshi precision. + */ + async minDepositAmount() { + const value = await this.#contracts.bitcoinDepositor.minDepositAmount() + return toSatoshi(value) + } +} diff --git a/sdk/test/modules/account.test.ts b/sdk/test/modules/account.test.ts index f532a43cf..05e4ae688 100644 --- a/sdk/test/modules/account.test.ts +++ b/sdk/test/modules/account.test.ts @@ -3,13 +3,10 @@ import { AcreContracts, Hex, Account, - DepositFees, - DepositFee, DepositStatus, StakeInitialization, } from "../../src" import { EthereumAddress } from "../../src/lib/ethereum" -import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" import { MockTbtc } from "../utils/mock-tbtc" import { DepositReceipt } from "../../src/modules/tbtc" @@ -23,13 +20,6 @@ const stakingModuleData: { bitcoinDepositorAddress: string predictedEthereumDepositorAddress: EthereumAddress } - estimateDepositFees: { - amount: bigint - amountIn1e18: bigint - mockedDepositFees: DepositFees - stBTCDepositFee: bigint - expectedDepositFeesInSatoshi: DepositFee - } } = { initializeDeposit: { referral: 1, @@ -44,32 +34,6 @@ const stakingModuleData: { "0x9996baD9C879B1643Ac921454815F93BadA090AB", ), }, - estimateDepositFees: { - amount: 10_000_000n, // 0.1 BTC, - // 0.1 tBTC in 1e18 precision. - amountIn1e18: 100000000000000000n, - mockedDepositFees: { - tbtc: { - // 0.00005 tBTC in 1e18 precision. - treasuryFee: 50000000000000n, - // 0.001 tBTC in 1e18 precision. - depositTxMaxFee: 1000000000000000n, - // 0.0001999 tBTC in 1e18 precision. - optimisticMintingFee: 199900000000000n, - }, - acre: { - // 0.0001 tBTC in 1e18 precision. - bitcoinDepositorFee: 100000000000000n, - }, - }, - // 0.001 in 1e18 precison - stBTCDepositFee: 1000000000000000n, - expectedDepositFeesInSatoshi: { - tbtc: 124990n, - acre: 110000n, - total: 234990n, - }, - }, } const stakingInitializationData: { @@ -276,98 +240,6 @@ describe("Account", () => { }) }) - describe("estimateDepositFees", () => { - const { - estimateDepositFees: { - amount, - amountIn1e18, - mockedDepositFees, - expectedDepositFeesInSatoshi, - stBTCDepositFee, - }, - } = stakingModuleData - - let result: DepositFee - const spyOnToSatoshi = jest.spyOn(satoshiConverter, "toSatoshi") - const spyOnFromSatoshi = jest.spyOn(satoshiConverter, "fromSatoshi") - - beforeAll(async () => { - contracts.bitcoinDepositor.calculateDepositFee = jest - .fn() - .mockResolvedValue(mockedDepositFees) - - contracts.stBTC.calculateDepositFee = jest - .fn() - .mockResolvedValue(stBTCDepositFee) - - result = await account.estimateDepositFee(amount) - }) - - it("should convert provided amount from satoshi to token precision", () => { - expect(spyOnFromSatoshi).toHaveBeenNthCalledWith(1, amount) - }) - - it("should get the deposit fees from Acre Bitcoin Depositor contract handle", () => { - expect( - contracts.bitcoinDepositor.calculateDepositFee, - ).toHaveBeenCalledWith(amountIn1e18) - }) - - it("should get the stBTC deposit fee", () => { - expect(contracts.stBTC.calculateDepositFee).toHaveBeenCalledWith( - amountIn1e18, - ) - }) - - it("should convert tBTC network fees to satoshi", () => { - const { - tbtc: { depositTxMaxFee, treasuryFee, optimisticMintingFee }, - } = mockedDepositFees - const totalTbtcFees = depositTxMaxFee + treasuryFee + optimisticMintingFee - - expect(spyOnToSatoshi).toHaveBeenNthCalledWith(1, totalTbtcFees) - }) - - it("should convert Acre network fees to satoshi", () => { - const { - acre: { bitcoinDepositorFee }, - } = mockedDepositFees - const totalAcreFees = bitcoinDepositorFee - - expect(spyOnToSatoshi).toHaveBeenNthCalledWith(2, totalAcreFees) - }) - - it("should return the deposit fees in satoshi precision", () => { - expect(result).toMatchObject(expectedDepositFeesInSatoshi) - }) - }) - - describe("minDepositAmount", () => { - describe("should return minimum deposit amount", () => { - const spyOnToSatoshi = jest.spyOn(satoshiConverter, "toSatoshi") - const mockedResult = BigInt(0.015 * 1e18) - // The returned result should be in satoshi precision - const expectedResult = BigInt(0.015 * 1e8) - let result: bigint - - beforeAll(async () => { - contracts.bitcoinDepositor.minDepositAmount = jest - .fn() - .mockResolvedValue(mockedResult) - result = await account.minDepositAmount() - }) - - it("should convert value to 1e8 satoshi precision", () => { - expect(spyOnToSatoshi).toHaveBeenCalledWith(mockedResult) - expect(spyOnToSatoshi).toHaveReturnedWith(expectedResult) - }) - - it(`should return ${expectedResult}`, () => { - expect(result).toBe(expectedResult) - }) - }) - }) - describe("getDeposits", () => { const finalizedDeposit = { subgraph: { diff --git a/sdk/test/modules/protocol.test.ts b/sdk/test/modules/protocol.test.ts new file mode 100644 index 000000000..171b825ea --- /dev/null +++ b/sdk/test/modules/protocol.test.ts @@ -0,0 +1,131 @@ +import Protocol from "../../src/modules/protocol" +import { AcreContracts, DepositFee } from "../../src" +import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" +import { MockAcreContracts } from "../utils/mock-acre-contracts" + +const data = { + estimateDepositFees: { + amount: 10_000_000n, // 0.1 BTC, + // 0.1 tBTC in 1e18 precision. + amountIn1e18: 100000000000000000n, + mockedDepositFees: { + tbtc: { + // 0.00005 tBTC in 1e18 precision. + treasuryFee: 50000000000000n, + // 0.001 tBTC in 1e18 precision. + depositTxMaxFee: 1000000000000000n, + // 0.0001999 tBTC in 1e18 precision. + optimisticMintingFee: 199900000000000n, + }, + acre: { + // 0.0001 tBTC in 1e18 precision. + bitcoinDepositorFee: 100000000000000n, + }, + }, + // 0.001 in 1e18 precision. + stBTCDepositFee: 1000000000000000n, + expectedDepositFeesInSatoshi: { + tbtc: 124990n, + acre: 110000n, + total: 234990n, + }, + }, +} + +describe("Protocol", () => { + const contracts: AcreContracts = new MockAcreContracts() + const protocol = new Protocol(contracts) + const spyOnToSatoshi = jest.spyOn(satoshiConverter, "toSatoshi") + + describe("minDepositAmount", () => { + const mockedResult = BigInt(0.015 * 1e18) + // The returned result should be in satoshi precision + const expectedResult = BigInt(0.015 * 1e8) + + let result: bigint + + beforeAll(async () => { + spyOnToSatoshi.mockClear() + contracts.bitcoinDepositor.minDepositAmount = jest + .fn() + .mockResolvedValue(mockedResult) + result = await protocol.minDepositAmount() + }) + + it("should convert value to 1e8 satoshi precision", () => { + expect(spyOnToSatoshi).toHaveBeenCalledWith(mockedResult) + expect(spyOnToSatoshi).toHaveReturnedWith(expectedResult) + }) + + it("should return correct value", () => { + expect(result).toBe(expectedResult) + }) + }) + + describe("estimateDepositFees", () => { + const { + estimateDepositFees: { + amount, + amountIn1e18, + mockedDepositFees, + expectedDepositFeesInSatoshi, + stBTCDepositFee, + }, + } = data + + let result: DepositFee + const spyOnFromSatoshi = jest.spyOn(satoshiConverter, "fromSatoshi") + + beforeAll(async () => { + spyOnToSatoshi.mockClear() + + contracts.bitcoinDepositor.calculateDepositFee = jest + .fn() + .mockResolvedValue(mockedDepositFees) + + contracts.stBTC.calculateDepositFee = jest + .fn() + .mockResolvedValue(stBTCDepositFee) + + result = await protocol.estimateDepositFee(amount) + }) + + it("should convert provided amount from satoshi to token precision", () => { + expect(spyOnFromSatoshi).toHaveBeenNthCalledWith(1, amount) + }) + + it("should get the deposit fees from Acre Bitcoin Depositor contract handle", () => { + expect( + contracts.bitcoinDepositor.calculateDepositFee, + ).toHaveBeenCalledWith(amountIn1e18) + }) + + it("should get the stBTC deposit fee", () => { + expect(contracts.stBTC.calculateDepositFee).toHaveBeenCalledWith( + amountIn1e18, + ) + }) + + it("should convert tBTC network fees to satoshi", () => { + const { + tbtc: { depositTxMaxFee, treasuryFee, optimisticMintingFee }, + } = mockedDepositFees + const totalTbtcFees = depositTxMaxFee + treasuryFee + optimisticMintingFee + + expect(spyOnToSatoshi).toHaveBeenNthCalledWith(1, totalTbtcFees) + }) + + it("should convert Acre network fees to satoshi", () => { + const { + acre: { bitcoinDepositorFee }, + } = mockedDepositFees + const totalAcreFees = bitcoinDepositorFee + + expect(spyOnToSatoshi).toHaveBeenNthCalledWith(2, totalAcreFees) + }) + + it("should return the deposit fees in satoshi precision", () => { + expect(result).toMatchObject(expectedDepositFeesInSatoshi) + }) + }) +}) From 7d2f350c0acb9e5d5d46dc65540f08add8891cc2 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 15:13:52 +0200 Subject: [PATCH 09/18] Remove the `AcreIdentifierResolver` class Sine we resolve the addresses just once on the SDK initialization we can remove this component. --- sdk/src/acre.ts | 22 ++++-- sdk/src/lib/identifier-resolver/index.ts | 48 ------------ sdk/src/modules/account/index.ts | 19 +++-- sdk/test/lib/identifier-resolver.test.ts | 96 ------------------------ sdk/test/modules/account.test.ts | 2 +- 5 files changed, 25 insertions(+), 162 deletions(-) delete mode 100644 sdk/src/lib/identifier-resolver/index.ts delete mode 100644 sdk/test/lib/identifier-resolver.test.ts diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 98118beb6..9709c0485 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -1,14 +1,17 @@ import { OrangeKitSdk } from "@orangekit/sdk" import { getDefaultProvider } from "ethers" import { AcreContracts } from "./lib/contracts" -import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" +import { + EthereumAddress, + EthereumNetwork, + getEthereumContracts, +} from "./lib/ethereum" import Account from "./modules/account" import Tbtc from "./modules/tbtc" import { VoidSigner } from "./lib/utils" import { BitcoinProvider, BitcoinNetwork } from "./lib/bitcoin" import { getChainIdByNetwork } from "./lib/ethereum/network" import AcreSubgraphApi from "./lib/api/AcreSubgraphApi" -import AcreIdentifierResolver from "./lib/identifier-resolver" import Protocol from "./modules/protocol" class Acre { @@ -67,10 +70,15 @@ class Acre { ethereumRpcUrl, ) - const { identifier: acreIdentifier, associatedBitcoinAddress } = - await AcreIdentifierResolver.toAcreIdentifier(bitcoinProvider, orangeKit) + const accountBitcoinAddress = await bitcoinProvider.getAddress() + const accountEthereumAddress = EthereumAddress.from( + await orangeKit.predictAddress(accountBitcoinAddress), + ) - const signer = new VoidSigner(acreIdentifier.identifierHex, ethersProvider) + const signer = new VoidSigner( + accountEthereumAddress.identifierHex, + ethersProvider, + ) const contracts = getEthereumContracts(signer, ethereumNetwork) @@ -86,8 +94,8 @@ class Acre { ) const account = new Account(contracts, tbtc, subgraph, { - bitcoinAddress: associatedBitcoinAddress, - acreIdentifier, + bitcoinAddress: accountBitcoinAddress, + ethereumAddress: accountEthereumAddress, }) const protocol = new Protocol(contracts) diff --git a/sdk/src/lib/identifier-resolver/index.ts b/sdk/src/lib/identifier-resolver/index.ts deleted file mode 100644 index 5f443634d..000000000 --- a/sdk/src/lib/identifier-resolver/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { OrangeKitSdk } from "@orangekit/sdk" -import { EthereumAddress } from "../ethereum" -import { BitcoinProvider } from "../bitcoin" -import { ChainIdentifier } from "../contracts" - -export type AcreAccountIdentifier = ChainIdentifier - -type BitcoinAddress = string - -type AcreIdentifierCache = Map - -const cache: AcreIdentifierCache = new Map< - BitcoinAddress, - AcreAccountIdentifier ->() - -async function toAcreIdentifier( - bitcoinProvider: BitcoinProvider, - orangeKit: OrangeKitSdk, -): Promise<{ - identifier: AcreAccountIdentifier - associatedBitcoinAddress: BitcoinAddress -}> { - const bitcoinAddress = await bitcoinProvider.getAddress() - - const cachedIdentifier = cache.get(bitcoinAddress) - - if (cachedIdentifier) { - return { - identifier: cachedIdentifier, - associatedBitcoinAddress: bitcoinAddress, - } - } - - const identifier = EthereumAddress.from( - await orangeKit.predictAddress(bitcoinAddress), - ) - - cache.set(bitcoinAddress, identifier) - - return { identifier, associatedBitcoinAddress: bitcoinAddress } -} - -const AcreIdentifierResolver = { - toAcreIdentifier, -} - -export default AcreIdentifierResolver diff --git a/sdk/src/modules/account/index.ts b/sdk/src/modules/account/index.ts index 6d5e2c90a..0adc06188 100644 --- a/sdk/src/modules/account/index.ts +++ b/sdk/src/modules/account/index.ts @@ -1,10 +1,9 @@ -import { AcreContracts } from "../../lib/contracts" +import { AcreContracts, ChainIdentifier } 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" -import { AcreAccountIdentifier } from "../../lib/identifier-resolver" export { DepositReceipt } from "../tbtc" @@ -57,19 +56,19 @@ export default class Account { readonly #bitcoinAddress: string - readonly #acreIdentifier: AcreAccountIdentifier + readonly #ethereumAddress: ChainIdentifier constructor( contracts: AcreContracts, tbtc: Tbtc, acreSubgraphApi: AcreSubgraphApi, - account: { bitcoinAddress: string; acreIdentifier: AcreAccountIdentifier }, + account: { bitcoinAddress: string; ethereumAddress: ChainIdentifier }, ) { this.#contracts = contracts this.#tbtc = tbtc this.#acreSubgraphApi = acreSubgraphApi this.#bitcoinAddress = account.bitcoinAddress - this.#acreIdentifier = account.acreIdentifier + this.#ethereumAddress = account.ethereumAddress } /** @@ -92,7 +91,7 @@ export default class Account { bitcoinRecoveryAddress ?? this.#bitcoinAddress const tbtcDeposit = await this.#tbtc.initiateDeposit( - this.#acreIdentifier, + this.#ethereumAddress, finalBitcoinRecoveryAddress, referral, ) @@ -104,14 +103,14 @@ export default class Account { * @returns Value of the basis for calculating final BTC balance. */ async sharesBalance() { - return this.#contracts.stBTC.balanceOf(this.#acreIdentifier) + return this.#contracts.stBTC.balanceOf(this.#ethereumAddress) } /** * @returns Maximum withdraw value. */ async estimatedBitcoinBalance() { - return this.#contracts.stBTC.assetsBalanceOf(this.#acreIdentifier) + return this.#contracts.stBTC.assetsBalanceOf(this.#ethereumAddress) } /** @@ -121,14 +120,14 @@ export default class Account { */ async getDeposits(): Promise { const subgraphData = await this.#acreSubgraphApi.getDepositsByOwner( - this.#acreIdentifier, + this.#ethereumAddress, ) const initializedOrFinalizedDepositsMap = new Map( subgraphData.map((data) => [data.depositKey, data]), ) - const tbtcData = await this.#tbtc.getDepositsByOwner(this.#acreIdentifier) + const tbtcData = await this.#tbtc.getDepositsByOwner(this.#ethereumAddress) return tbtcData.map((deposit) => { const depositFromSubgraph = initializedOrFinalizedDepositsMap.get( diff --git a/sdk/test/lib/identifier-resolver.test.ts b/sdk/test/lib/identifier-resolver.test.ts deleted file mode 100644 index a2f72224a..000000000 --- a/sdk/test/lib/identifier-resolver.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { MockBitcoinProvider } from "../utils/mock-bitcoin-provider" -import { MockOrangeKitSdk } from "../utils/mock-orangekit" -import { EthereumAddress } from "../../src/lib/ethereum" -import AcreIdentifierResolver from "../../src/lib/identifier-resolver" - -describe("AcreIdentifierResolver", () => { - describe("toAcreIdentifier", () => { - const bitcoinProvider = new MockBitcoinProvider() - const orangeKit = new MockOrangeKitSdk() - - describe("when the identifier not yet cached", () => { - const bitcoinAddress = "mjc2zGTypwNyDi4ZxGbBNnUA84bfgiwYc" - const identifier = EthereumAddress.from( - "0x766C69E751460070b160aF93Cbec51ccd77ff4A2", - ) - let result: Awaited< - ReturnType - > - - beforeAll(async () => { - bitcoinProvider.getAddress.mockResolvedValue(bitcoinAddress) - orangeKit.predictAddress = jest - .fn() - .mockResolvedValue(`0x${identifier.identifierHex}`) - - result = await AcreIdentifierResolver.toAcreIdentifier( - bitcoinProvider, - // @ts-expect-error Error: Property '#private' is missing in type - // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. - orangeKit, - ) - }) - - afterAll(() => { - bitcoinProvider.getAddress.mockClear() - }) - - it("should get the bitcoin address", () => { - expect(bitcoinProvider.getAddress).toHaveBeenCalled() - }) - - it("should call orangekit to predict the address", () => { - expect(orangeKit.predictAddress).toHaveBeenCalledWith(bitcoinAddress) - }) - - it("should return the correct identifier", () => { - expect(result.identifier.equals(identifier)).toBeTruthy() - expect(result.associatedBitcoinAddress).toBe(bitcoinAddress) - }) - }) - - describe("when the identifier is already cached", () => { - const bitcoinAddress = "mwyc4iaVjyL9xif9MyG7RuCvD3qizEiChY" - const identifier = EthereumAddress.from( - "0x8fC860a219A401BCe1Fb97EB510Efb35f59c8211", - ) - let result: Awaited< - ReturnType - > - - beforeAll(async () => { - bitcoinProvider.getAddress.mockResolvedValue(bitcoinAddress) - orangeKit.predictAddress = jest - .fn() - .mockResolvedValue(`0x${identifier.identifierHex}`) - - await AcreIdentifierResolver.toAcreIdentifier( - bitcoinProvider, - // @ts-expect-error Error: Property '#private' is missing in type - // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. - orangeKit, - ) - - result = await AcreIdentifierResolver.toAcreIdentifier( - bitcoinProvider, - // @ts-expect-error Error: Property '#private' is missing in type - // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. - orangeKit, - ) - }) - - it("should get the bitcoin address", () => { - expect(bitcoinProvider.getAddress).toHaveBeenCalledTimes(2) - }) - - it("should call the orangekit SDK only once", () => { - expect(orangeKit.predictAddress).toHaveReturnedTimes(1) - }) - - it("should return the correct identifier", () => { - expect(result.identifier.equals(identifier)).toBeTruthy() - expect(result.associatedBitcoinAddress).toBe(bitcoinAddress) - }) - }) - }) -}) diff --git a/sdk/test/modules/account.test.ts b/sdk/test/modules/account.test.ts index 05e4ae688..9e9a8a430 100644 --- a/sdk/test/modules/account.test.ts +++ b/sdk/test/modules/account.test.ts @@ -74,7 +74,7 @@ describe("Account", () => { const account: Account = new Account(contracts, tbtc, acreSubgraph, { bitcoinAddress: bitcoinDepositorAddress, - acreIdentifier: predictedEthereumDepositorAddress, + ethereumAddress: predictedEthereumDepositorAddress, }) describe("initializeStake", () => { From c928d6deb45f3c49ffab62ec1b4cec5d5a84d4bf Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 15:21:21 +0200 Subject: [PATCH 10/18] Update module dirs structure in SDK Since some of the dirs contain only `index.ts` file we move them one level higher with name of the module. --- sdk/src/modules/{account/index.ts => account.ts} | 16 ++++++++-------- .../modules/{protocol/index.ts => protocol.ts} | 4 ++-- sdk/src/modules/{staking/index.ts => staking.ts} | 4 ++-- sdk/test/modules/protocol.test.ts | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) rename sdk/src/modules/{account/index.ts => account.ts} (91%) rename sdk/src/modules/{protocol/index.ts => protocol.ts} (93%) rename sdk/src/modules/{staking/index.ts => staking.ts} (94%) diff --git a/sdk/src/modules/account/index.ts b/sdk/src/modules/account.ts similarity index 91% rename from sdk/src/modules/account/index.ts rename to sdk/src/modules/account.ts index 0adc06188..ec47b8a87 100644 --- a/sdk/src/modules/account/index.ts +++ b/sdk/src/modules/account.ts @@ -1,11 +1,11 @@ -import { AcreContracts, ChainIdentifier } 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 { AcreContracts, ChainIdentifier } 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" /** * Represents the deposit data. diff --git a/sdk/src/modules/protocol/index.ts b/sdk/src/modules/protocol.ts similarity index 93% rename from sdk/src/modules/protocol/index.ts rename to sdk/src/modules/protocol.ts index d4b1c9939..4fde3d0cb 100644 --- a/sdk/src/modules/protocol/index.ts +++ b/sdk/src/modules/protocol.ts @@ -1,5 +1,5 @@ -import { AcreContracts, DepositFees } from "../../lib/contracts" -import { fromSatoshi, toSatoshi } from "../../lib/utils" +import { AcreContracts, DepositFees } from "../lib/contracts" +import { fromSatoshi, toSatoshi } from "../lib/utils" /** * Represents all total deposit fees grouped by network. diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking.ts similarity index 94% rename from sdk/src/modules/staking/index.ts rename to sdk/src/modules/staking.ts index b7507304b..d5d0f1d77 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking.ts @@ -1,6 +1,6 @@ -import TbtcDeposit from "../tbtc/Deposit" +import TbtcDeposit from "./tbtc/Deposit" -import type { DepositReceipt } from "../account" +import type { DepositReceipt } from "./account" /** * Represents an instance of the staking flow. Staking flow requires a few steps diff --git a/sdk/test/modules/protocol.test.ts b/sdk/test/modules/protocol.test.ts index 171b825ea..f3077d2e6 100644 --- a/sdk/test/modules/protocol.test.ts +++ b/sdk/test/modules/protocol.test.ts @@ -1,4 +1,4 @@ -import Protocol from "../../src/modules/protocol" +import Protocol from "./protocol" import { AcreContracts, DepositFee } from "../../src" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" From 61f35d4609efa7f8385f83c11d142bccdfc8886f Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 15:55:49 +0200 Subject: [PATCH 11/18] Update modules docs Add info about the precision of the returned value. --- sdk/src/modules/account.ts | 3 ++- sdk/src/modules/protocol.ts | 2 +- sdk/src/modules/staking.ts | 9 +++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/sdk/src/modules/account.ts b/sdk/src/modules/account.ts index ec47b8a87..ca6ff52b8 100644 --- a/sdk/src/modules/account.ts +++ b/sdk/src/modules/account.ts @@ -100,7 +100,8 @@ export default class Account { } /** - * @returns Value of the basis for calculating final BTC balance. + * @returns Value of the basis for calculating final BTC balance in 1e18 + * precision. */ async sharesBalance() { return this.#contracts.stBTC.balanceOf(this.#ethereumAddress) diff --git a/sdk/src/modules/protocol.ts b/sdk/src/modules/protocol.ts index 4fde3d0cb..7276a06f5 100644 --- a/sdk/src/modules/protocol.ts +++ b/sdk/src/modules/protocol.ts @@ -60,7 +60,7 @@ export default class Protocol { /** * @returns Minimum deposit amount in 1e8 satoshi precision. */ - async minDepositAmount() { + async minimumDepositAmount() { const value = await this.#contracts.bitcoinDepositor.minDepositAmount() return toSatoshi(value) } diff --git a/sdk/src/modules/staking.ts b/sdk/src/modules/staking.ts index d5d0f1d77..9cf67a5f9 100644 --- a/sdk/src/modules/staking.ts +++ b/sdk/src/modules/staking.ts @@ -34,12 +34,9 @@ export default class StakeInitialization { /** * Stakes BTC based on the Bitcoin funding transaction via BitcoinDepositor - * contract. It requires signed staking message, which means `stake` should be - * called after message signing. By default, it detects and uses the outpoint - * of the recent Bitcoin funding transaction and throws if such a transaction - * does not exist. - * @dev Use it as the last step of the staking flow. It requires signed - * staking message otherwise throws an error. + * contract. By default, it detects and uses the outpoint of the recent + * Bitcoin funding transaction and throws if such a transaction does not + * exist. * @param options Optional options parameters to initialize stake. * @see StakeOptions for more details. * @returns Transaction hash of the stake initiation transaction. From f458791dcc5f99099d76127073f39b1e825fc936 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 15:56:24 +0200 Subject: [PATCH 12/18] Update the return type of the `initializeStake` fn --- sdk/src/modules/account.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/src/modules/account.ts b/sdk/src/modules/account.ts index ca6ff52b8..8ec656a5b 100644 --- a/sdk/src/modules/account.ts +++ b/sdk/src/modules/account.ts @@ -84,7 +84,10 @@ export default class Account { * tBTC Bridge recovery address. * @returns Object represents the deposit process. */ - async initializeStake(referral: number, bitcoinRecoveryAddress?: string) { + async initializeStake( + referral: number, + bitcoinRecoveryAddress?: string, + ): Promise { // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if // address is not supported. const finalBitcoinRecoveryAddress = From 2414198a5bf2185fc90b558732eac71e74d1f7a6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 15:57:00 +0200 Subject: [PATCH 13/18] Update `estimatedBitcoinBalance` fn We should be consistent so we want to return values related to the Bitcoin in satoshi precision (1e8). --- sdk/src/modules/account.ts | 6 ++++-- sdk/test/modules/account.test.ts | 12 ++++++++++-- sdk/test/modules/protocol.test.ts | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/sdk/src/modules/account.ts b/sdk/src/modules/account.ts index 8ec656a5b..0f7480748 100644 --- a/sdk/src/modules/account.ts +++ b/sdk/src/modules/account.ts @@ -111,10 +111,12 @@ export default class Account { } /** - * @returns Maximum withdraw value. + * @returns Maximum withdraw value in 1e8 satoshi precision. */ async estimatedBitcoinBalance() { - return this.#contracts.stBTC.assetsBalanceOf(this.#ethereumAddress) + return toSatoshi( + await this.#contracts.stBTC.assetsBalanceOf(this.#ethereumAddress), + ) } /** diff --git a/sdk/test/modules/account.test.ts b/sdk/test/modules/account.test.ts index 9e9a8a430..69bab6e8a 100644 --- a/sdk/test/modules/account.test.ts +++ b/sdk/test/modules/account.test.ts @@ -11,6 +11,7 @@ import { MockAcreContracts } from "../utils/mock-acre-contracts" import { MockTbtc } from "../utils/mock-tbtc" import { DepositReceipt } from "../../src/modules/tbtc" import AcreSubgraphApi from "../../src/lib/api/AcreSubgraphApi" +import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" const stakingModuleData: { initializeDeposit: { @@ -219,14 +220,17 @@ describe("Account", () => { }) describe("estimatedBitcoinBalance", () => { - const expectedResult = 4294967295n + const mockedAssetsBalance = 1234567891230000000n + const expectedResult = 123456789n const depositor = predictedEthereumDepositorAddress + const spyOnToSatoshi = jest.spyOn(satoshiConverter, "toSatoshi") + let result: bigint beforeAll(async () => { contracts.stBTC.assetsBalanceOf = jest .fn() - .mockResolvedValue(expectedResult) + .mockResolvedValue(mockedAssetsBalance) result = await account.estimatedBitcoinBalance() }) @@ -235,6 +239,10 @@ describe("Account", () => { expect(contracts.stBTC.assetsBalanceOf).toHaveBeenCalledWith(depositor) }) + it("should convert to satoshi", () => { + expect(spyOnToSatoshi).toHaveBeenCalledWith(mockedAssetsBalance) + }) + it("should return maximum withdraw value", () => { expect(result).toEqual(expectedResult) }) diff --git a/sdk/test/modules/protocol.test.ts b/sdk/test/modules/protocol.test.ts index f3077d2e6..b84d26acc 100644 --- a/sdk/test/modules/protocol.test.ts +++ b/sdk/test/modules/protocol.test.ts @@ -1,4 +1,4 @@ -import Protocol from "./protocol" +import Protocol from "../../src/modules/protocol" import { AcreContracts, DepositFee } from "../../src" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" @@ -49,7 +49,7 @@ describe("Protocol", () => { contracts.bitcoinDepositor.minDepositAmount = jest .fn() .mockResolvedValue(mockedResult) - result = await protocol.minDepositAmount() + result = await protocol.minimumDepositAmount() }) it("should convert value to 1e8 satoshi precision", () => { From b916ba9b5bf573802fea316f199c061b39a49892 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 6 Jun 2024 16:08:02 +0200 Subject: [PATCH 14/18] Update hook We renamed the method `minDepositAmount` to `minimumDepositAmount` and here we update the dapp part. --- dapp/src/hooks/sdk/useFetchMinDepositAmount.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts b/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts index 050878657..825e3c6a7 100644 --- a/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts +++ b/dapp/src/hooks/sdk/useFetchMinDepositAmount.ts @@ -12,7 +12,7 @@ export function useFetchMinDepositAmount() { if (!isInitialized || !acre) return const fetchMinDepositAmount = async () => { - const minDepositAmount = await acre.protocol.minDepositAmount() + const minDepositAmount = await acre.protocol.minimumDepositAmount() dispatch(setMinDepositAmount(minDepositAmount)) } From db5fdde89c26bccf4bb7a88f282b2332b8bc9ce6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 10 Jun 2024 13:00:15 +0200 Subject: [PATCH 15/18] Fix creating the VoidSigner instance We should add `0x` prefix to the address when creating the `VoidSigner` instance. --- sdk/src/acre.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 9709c0485..a2893960c 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -76,7 +76,7 @@ class Acre { ) const signer = new VoidSigner( - accountEthereumAddress.identifierHex, + `0x${accountEthereumAddress.identifierHex}`, ethersProvider, ) From 0493060b00fc8162bf565248fe59a075794c0a10 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 11 Jun 2024 13:54:04 +0200 Subject: [PATCH 16/18] Expose totalAssets in SDK Here we expose in the SDK the totalAssets function from stBTC contract. The function returns total value of tBTC under Acre protocol management. --- sdk/src/lib/contracts/stbtc.ts | 5 +++++ sdk/src/lib/ethereum/stbtc.ts | 7 +++++++ sdk/src/modules/protocol.ts | 7 +++++++ 3 files changed, 19 insertions(+) diff --git a/sdk/src/lib/contracts/stbtc.ts b/sdk/src/lib/contracts/stbtc.ts index b94b71a34..adf9c503c 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 Total tBTC amount under stBTC contract management in 1e18 precision. + */ + totalAssets(): Promise + /** * @param identifier The generic chain identifier. * @returns Value of the basis for calculating final BTC balance. diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts index e1304529e..9ff8639f3 100644 --- a/sdk/src/lib/ethereum/stbtc.ts +++ b/sdk/src/lib/ethereum/stbtc.ts @@ -37,6 +37,13 @@ class EthereumStBTC super(config, artifact) } + /** + * @see {StBTC#totalAssets} + */ + totalAssets(): Promise { + return this.instance.totalAssets() + } + /** * @see {StBTC#balanceOf} */ diff --git a/sdk/src/modules/protocol.ts b/sdk/src/modules/protocol.ts index 7276a06f5..227699dcc 100644 --- a/sdk/src/modules/protocol.ts +++ b/sdk/src/modules/protocol.ts @@ -23,6 +23,13 @@ export default class Protocol { this.#contracts = contracts } + /** + * @returns Total Bitcoin amount under protocol management in 1e8 satoshi precision. + */ + async totalAssets() { + return toSatoshi(await this.#contracts.stBTC.totalAssets()) + } + /** * Estimates the deposit fee based on the provided amount. * @param amount Amount to deposit in satoshi. From d621a4aaa49219579fd47900b7c222673ff75331 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Tue, 11 Jun 2024 13:58:07 +0200 Subject: [PATCH 17/18] Revert "Expose totalAssets in SDK" This reverts commit 0493060b00fc8162bf565248fe59a075794c0a10. It was pushed by accident to the wrong branch. --- sdk/src/lib/contracts/stbtc.ts | 5 ----- sdk/src/lib/ethereum/stbtc.ts | 7 ------- sdk/src/modules/protocol.ts | 7 ------- 3 files changed, 19 deletions(-) diff --git a/sdk/src/lib/contracts/stbtc.ts b/sdk/src/lib/contracts/stbtc.ts index adf9c503c..b94b71a34 100644 --- a/sdk/src/lib/contracts/stbtc.ts +++ b/sdk/src/lib/contracts/stbtc.ts @@ -1,11 +1,6 @@ import { ChainIdentifier } from "./chain-identifier" export interface StBTC { - /** - * @returns Total tBTC amount under stBTC contract management in 1e18 precision. - */ - totalAssets(): Promise - /** * @param identifier The generic chain identifier. * @returns Value of the basis for calculating final BTC balance. diff --git a/sdk/src/lib/ethereum/stbtc.ts b/sdk/src/lib/ethereum/stbtc.ts index 9ff8639f3..e1304529e 100644 --- a/sdk/src/lib/ethereum/stbtc.ts +++ b/sdk/src/lib/ethereum/stbtc.ts @@ -37,13 +37,6 @@ class EthereumStBTC super(config, artifact) } - /** - * @see {StBTC#totalAssets} - */ - totalAssets(): Promise { - return this.instance.totalAssets() - } - /** * @see {StBTC#balanceOf} */ diff --git a/sdk/src/modules/protocol.ts b/sdk/src/modules/protocol.ts index 227699dcc..7276a06f5 100644 --- a/sdk/src/modules/protocol.ts +++ b/sdk/src/modules/protocol.ts @@ -23,13 +23,6 @@ export default class Protocol { this.#contracts = contracts } - /** - * @returns Total Bitcoin amount under protocol management in 1e8 satoshi precision. - */ - async totalAssets() { - return toSatoshi(await this.#contracts.stBTC.totalAssets()) - } - /** * Estimates the deposit fee based on the provided amount. * @param amount Amount to deposit in satoshi. From 1bdf00f5f67498118669fa70e9ad348eea7d5d05 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 11 Jun 2024 14:18:33 +0200 Subject: [PATCH 18/18] Update docs in `account` and `protocol` modules --- sdk/src/modules/account.ts | 11 +++++------ sdk/src/modules/protocol.ts | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sdk/src/modules/account.ts b/sdk/src/modules/account.ts index 0f7480748..9933c818d 100644 --- a/sdk/src/modules/account.ts +++ b/sdk/src/modules/account.ts @@ -103,15 +103,15 @@ export default class Account { } /** - * @returns Value of the basis for calculating final BTC balance in 1e18 - * precision. + * @returns Balance of the account's stBTC shares (in 1e18 precision). */ async sharesBalance() { return this.#contracts.stBTC.balanceOf(this.#ethereumAddress) } /** - * @returns Maximum withdraw value in 1e8 satoshi precision. + * @returns Balance of Bitcoin position in Acre estimated based on the + * account's stBTC shares (in 1e8 satoshi precision). */ async estimatedBitcoinBalance() { return toSatoshi( @@ -120,9 +120,8 @@ export default class Account { } /** - * @returns All deposits associated with the Bitcoin address that returns the - * Bitcoin Provider. They include all deposits: queued, initialized - * and finalized. + * @returns All deposits associated with the account. They include all + * deposits: queued, initialized and finalized. */ async getDeposits(): Promise { const subgraphData = await this.#acreSubgraphApi.getDepositsByOwner( diff --git a/sdk/src/modules/protocol.ts b/sdk/src/modules/protocol.ts index 7276a06f5..5da325370 100644 --- a/sdk/src/modules/protocol.ts +++ b/sdk/src/modules/protocol.ts @@ -26,7 +26,7 @@ export default class Protocol { /** * Estimates the deposit fee based on the provided amount. * @param amount Amount to deposit in satoshi. - * @returns Deposit fee grouped by tBTC and Acre networks in 1e8 satoshi + * @returns Deposit fee grouped by tBTC and Acre protocols in 1e8 satoshi * precision and total deposit fee value. */ async estimateDepositFee(amount: bigint): Promise {