From 2d624f4180467bf16d2c3dbc876d9f5e7cfb26d7 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 18 Apr 2024 13:19:52 +0200 Subject: [PATCH 01/45] Add `@orangekit/sdk` dependency To build Bitcoin-native experience. --- pnpm-lock.yaml | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ sdk/package.json | 3 ++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 307077ecd..7254142f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: '@keep-network/tbtc-v2.ts': specifier: 2.4.0-dev.3 version: 2.4.0-dev.3(@keep-network/keep-core@1.8.1-dev.0) + '@orangekit/sdk': + specifier: 1.0.0-beta.8 + version: 1.0.0-beta.8 ethers: specifier: ^6.10.0 version: 6.10.0 @@ -383,6 +386,10 @@ packages: /@adraffy/ens-normalize@1.10.0: resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + /@adraffy/ens-normalize@1.10.1: + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + dev: false + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -5502,6 +5509,10 @@ packages: resolution: {integrity: sha512-bv2sdS6LKqVVMLI5+zqnNrNU/CA+6z6CmwFXm/MzmOPBRSO5reEJN7z0Gbzvs0/bv/MZZXNklubpwy3v2+azsw==} dev: false + /@openzeppelin/contracts@5.0.2: + resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} + dev: false + /@openzeppelin/defender-admin-client@1.52.0(debug@4.3.4): resolution: {integrity: sha512-CKs5mMLL7+nXyugsHaAw0aPfLwFNA+vq7ftuJ3sWUKdbQRZsJ+/189HAwp2/BJC64yUbarEeWqOh3jNpaKRJLw==} dependencies: @@ -5638,6 +5649,26 @@ packages: - utf-8-validate dev: false + /@orangekit/contracts@1.0.0-beta.2(ethers@6.11.1): + resolution: {integrity: sha512-xbQBKnVU7bMy/0mlBhfFkiL0e15YziY97Y9mYfMAQKc51mFIH+J93TrGxjLa2kgYyVUiAxSr4++n6BgYWReLVA==} + dependencies: + '@openzeppelin/contracts': 5.0.2 + '@safe-global/safe-contracts': 1.4.1-build.0(ethers@6.11.1) + transitivePeerDependencies: + - ethers + dev: false + + /@orangekit/sdk@1.0.0-beta.8: + resolution: {integrity: sha512-eyEjw5cuT0cigL6GOKrnKEbIMjFWVe1g5hu9dkFhaRHKfsLq8IzYXRpFYBZYdCCiBUj8aN7zMf9WevgMcBiU7g==} + dependencies: + '@orangekit/contracts': 1.0.0-beta.2(ethers@6.11.1) + bitcoinjs-lib: 6.1.5 + ethers: 6.11.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /@parcel/bundler-default@2.8.3(@parcel/core@2.8.3): resolution: {integrity: sha512-yJvRsNWWu5fVydsWk3O2L4yIy3UZiKWO2cPDukGOIWMgp/Vbpp+2Ct5IygVRtE22bnseW/E/oe0PV3d2IkEJGg==} engines: {node: '>= 12.0.0', parcel: ^2.8.3} @@ -6389,6 +6420,14 @@ packages: dev: true optional: true + /@safe-global/safe-contracts@1.4.1-build.0(ethers@6.11.1): + resolution: {integrity: sha512-TIpoKJtMqLcLFoid0cvpxo8YTcnRUj95MHvxzwgPbJPRONOckNS6ebgGyBBRDmnpxFh34IBpPUZ7JD+z2Cfbbg==} + peerDependencies: + ethers: 5.4.0 + dependencies: + ethers: 6.11.1 + dev: false + /@scure/base@1.1.3: resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==} dev: true @@ -11856,6 +11895,22 @@ packages: - bufferutil - utf-8-validate + /ethers@6.11.1: + resolution: {integrity: sha512-mxTAE6wqJQAbp5QAe/+o+rXOID7Nw91OZXvgpjDa1r4fAbq2Nu314oEZSbjoRLacuCzs7kUC3clEvkCQowffGg==} + engines: {node: '>=14.0.0'} + dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 18.15.13 + aes-js: 4.0.0-beta.5 + tslib: 2.4.0 + ws: 8.5.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /ethjs-unit@0.1.6: resolution: {integrity: sha512-/Sn9Y0oKl0uqQuvgFk/zQgR7aw1g36qX/jzSQ5lSwlO0GigPymk4eGQfeNTD03w1dPOqfz8V77Cy43jH56pagw==} engines: {node: '>=6.5.0', npm: '>=3'} diff --git a/sdk/package.json b/sdk/package.json index e29b1f487..9b3d30e09 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -26,8 +26,9 @@ "typescript": "^5.3.2" }, "dependencies": { - "@keep-network/tbtc-v2.ts": "2.4.0-dev.3", "@acre-btc/contracts": "workspace:*", + "@keep-network/tbtc-v2.ts": "2.4.0-dev.3", + "@orangekit/sdk": "1.0.0-beta.8", "ethers": "^6.10.0" } } From 7f7cbffdc85d401e8d63e72d81671f87405e8a12 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 18 Apr 2024 13:40:55 +0200 Subject: [PATCH 02/45] Init the OrangeKit SDK in Acre SDK --- sdk/src/acre.ts | 19 ++++++++++++++++++- sdk/src/modules/staking/index.ts | 8 ++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index f7458d78d..6e27f6c6d 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -1,4 +1,5 @@ import { TBTC } from "@keep-network/tbtc-v2.ts" +import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts } from "./lib/contracts" import { ChainEIP712Signer } from "./lib/eip712-signer" import { @@ -12,6 +13,8 @@ import { EthereumSignerCompatibleWithEthersV5 } from "./lib/utils" class Acre { readonly #tbtc: TBTC + readonly #orangeKit: OrangeKitSdk + readonly #messageSigner: ChainEIP712Signer public readonly contracts: AcreContracts @@ -22,14 +25,17 @@ class Acre { _contracts: AcreContracts, _messageSigner: ChainEIP712Signer, _tbtc: TBTC, + _orangeKit: OrangeKitSdk, ) { this.contracts = _contracts this.#tbtc = _tbtc + this.#orangeKit = _orangeKit this.#messageSigner = _messageSigner this.staking = new StakingModule( this.contracts, this.#messageSigner, this.#tbtc, + this.#orangeKit, ) } @@ -37,11 +43,14 @@ class Acre { signer: EthereumSignerCompatibleWithEthersV5, network: EthereumNetwork, ): Promise { + const chainId = await signer.getChainId() + const tbtc = await Acre.#getTBTCEthereumSDK(signer, network) + const orangeKit = await Acre.#getOrangeKitSDK(chainId) const contracts = getEthereumContracts(signer, network) const messages = new EthereumEIP712Signer(signer) - return new Acre(contracts, messages, tbtc) + return new Acre(contracts, messages, tbtc, orangeKit) } static #getTBTCEthereumSDK( @@ -62,6 +71,14 @@ class Acre { return TBTC.initializeMainnet(signer) } } + + static #getOrangeKitSDK(chainId: number): Promise { + return OrangeKitSdk.init( + chainId, + // TODO: pass rpc url as config. + "https://eth-mainnet.g.alchemy.com/v2/", + ) + } } // eslint-disable-next-line import/prefer-default-export diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index 32e8b5363..8e7938c7d 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -1,4 +1,5 @@ import { ChainIdentifier, TBTC } from "@keep-network/tbtc-v2.ts" +import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts, DepositorProxy, DepositFees } from "../../lib/contracts" import { ChainEIP712Signer } from "../../lib/eip712-signer" import { StakeInitialization } from "./stake-initialization" @@ -32,14 +33,21 @@ class StakingModule { */ readonly #tbtc: TBTC + /** + * OrangeKit SDK. + */ + readonly #orangeKit: OrangeKitSdk + constructor( _contracts: AcreContracts, _messageSigner: ChainEIP712Signer, _tbtc: TBTC, + _orangeKit: OrangeKitSdk, ) { this.#contracts = _contracts this.#messageSigner = _messageSigner this.#tbtc = _tbtc + this.#orangeKit = _orangeKit } /** From 37a58f8412a8de61d873866df671a9f4420e3f50 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 18 Apr 2024 14:51:53 +0200 Subject: [PATCH 03/45] Update the staking module Pass only bitcoin address to initialize the deposit process. Using `@orangekit/sdk` we calculate the Ethereum address based on the Bitcoin address. This created Ethereum address of the Safe should be used to show the user's data like position, transaction history and activity details. --- dapp/src/acre-react/hooks/useStakeFlow.ts | 10 ++--- sdk/src/modules/staking/index.ts | 48 +++++++++++++++++------ sdk/test/modules/staking.test.ts | 10 ++--- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/dapp/src/acre-react/hooks/useStakeFlow.ts b/dapp/src/acre-react/hooks/useStakeFlow.ts index b13c3b8a5..a74973f77 100644 --- a/dapp/src/acre-react/hooks/useStakeFlow.ts +++ b/dapp/src/acre-react/hooks/useStakeFlow.ts @@ -1,7 +1,6 @@ import { useCallback, useState } from "react" import { StakeInitialization, - EthereumAddress, DepositorProxy, DepositReceipt, } from "@acre-btc/sdk" @@ -9,8 +8,7 @@ import { useAcreContext } from "./useAcreContext" export type UseStakeFlowReturn = { initStake: ( - bitcoinRecoveryAddress: string, - ethereumAddress: string, + bitcoinAddress: string, referral: number, depositor?: DepositorProxy, ) => Promise @@ -33,16 +31,14 @@ export function useStakeFlow(): UseStakeFlowReturn { const initStake = useCallback( async ( - bitcoinRecoveryAddress: string, - ethereumAddress: string, + bitcoinAddress: string, referral: number, depositor?: DepositorProxy, ) => { if (!acre || !isInitialized) throw new Error("Acre SDK not defined") const initializedStakeFlow = await acre.staking.initializeStake( - bitcoinRecoveryAddress, - EthereumAddress.from(ethereumAddress), + bitcoinAddress, referral, depositor, ) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index 8e7938c7d..0afe4b5eb 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -1,4 +1,8 @@ -import { ChainIdentifier, TBTC } from "@keep-network/tbtc-v2.ts" +import { + ChainIdentifier, + EthereumAddress, + TBTC, +} from "@keep-network/tbtc-v2.ts" import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts, DepositorProxy, DepositFees } from "../../lib/contracts" import { ChainEIP712Signer } from "../../lib/eip712-signer" @@ -51,32 +55,50 @@ class StakingModule { } /** - * Initializes the Acre staking process. - * @param bitcoinRecoveryAddress P2PKH or P2WPKH Bitcoin address that can be - * used for emergency recovery of the deposited - * funds. - * @param staker The address to which the stBTC shares will be minted. + * Initializes the Acre deposit process. + * @param depositor The Bitcoin depositor address. Supported addresses: + * `P2WPKH`, `P2PKH`, `P2SH-P2WPKH`. * @param referral Data used for referral program. * @param depositorProxy Depositor proxy used to initiate the deposit. - * @returns Object represents the staking process. + * @param bitcoinRecoveryAddress `P2PKH` or `P2WPKH` Bitcoin address that can + * be used for emergency recovery of the deposited funds. If + * `undefined` the `depositor` address is used as bitcoin recovery + * address then the `depositor` must be `P2WPKH` or `P2PKH`. + * @returns Object represents the deposit process. */ async initializeStake( - bitcoinRecoveryAddress: string, - staker: ChainIdentifier, + depositor: string, referral: number, depositorProxy?: DepositorProxy, + bitcoinRecoveryAddress?: string, ) { + // 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 depositOwnerChainAddress = EthereumAddress.from( + await this.#orangeKit.predictAddress(depositor), + ) + + // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if + // address is not supported. + const finalBitcoinRecoveryAddress = bitcoinRecoveryAddress ?? depositor + const deposit = await this.#tbtc.deposits.initiateDepositWithProxy( - bitcoinRecoveryAddress, + finalBitcoinRecoveryAddress, depositorProxy ?? this.#contracts.bitcoinDepositor, - this.#contracts.bitcoinDepositor.encodeExtraData(staker, referral), + this.#contracts.bitcoinDepositor.encodeExtraData( + depositOwnerChainAddress, + referral, + ), ) return new StakeInitialization( this.#contracts, this.#messageSigner, - bitcoinRecoveryAddress, - staker, + finalBitcoinRecoveryAddress, + depositOwnerChainAddress, deposit, ) } diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index 81909295e..ed2d75a29 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, @@ -100,11 +101,13 @@ describe("Staking", () => { const contracts: AcreContracts = new MockAcreContracts() const messageSigner = new MockMessageSigner() const tbtc = new MockTBTC() + const orangeKit = {} as OrangeKitSdk const staking: StakingModule = new StakingModule( contracts, messageSigner, tbtc, + orangeKit, ) describe("initializeStake", () => { @@ -142,11 +145,7 @@ describe("Staking", () => { messageSigner.sign = jest.fn().mockResolvedValue(mockedSignedMessage) - result = await staking.initializeStake( - bitcoinRecoveryAddress, - staker, - referral, - ) + result = await staking.initializeStake(bitcoinRecoveryAddress, referral) }) it("should encode extra data", () => { @@ -370,7 +369,6 @@ describe("Staking", () => { result = await staking.initializeStake( bitcoinRecoveryAddress, - staker, referral, customDepositorProxy, ) From c7733c913c9c60bb39a6e85c2cdf02dba72d8ae8 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Mon, 29 Apr 2024 16:35:00 +0200 Subject: [PATCH 04/45] Add unit test for integration with OrangeKit --- sdk/test/modules/staking.test.ts | 78 ++++++++++++++++++++++---------- sdk/test/utils/mock-orangekit.ts | 37 +++++++++++++++ 2 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 sdk/test/utils/mock-orangekit.ts diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index ed2d75a29..220f38f3b 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -1,6 +1,5 @@ import { BitcoinTxHash } from "@keep-network/tbtc-v2.ts" import { ethers } from "ethers" -import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts, StakingModule, @@ -16,14 +15,15 @@ import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" import { MockMessageSigner } from "../utils/mock-message-signer" import { MockTBTC } from "../utils/mock-tbtc" +import { MockOrangeKitSdk } from "../utils/mock-orangekit" const stakingModuleData: { - initializeStake: { - staker: EthereumAddress + initializeDeposit: { referral: number extraData: Hex - bitcoinRecoveryAddress: string mockedDepositBTCAddress: string + bitcoinDepositorAddress: string + predictedEthereumDepositorAddress: EthereumAddress } estimateDepositFees: { amount: bigint @@ -33,15 +33,18 @@ const stakingModuleData: { expectedDepositFeesInSatoshi: DepositFee } } = { - initializeStake: { - staker: EthereumAddress.from(ethers.Wallet.createRandom().address), + initializeDeposit: { referral: 1, + extraData: Hex.from( "0xeb098d6cde6a202981316b24b19e64d82721e89e4d0000000000000000000000", ), - bitcoinRecoveryAddress: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", mockedDepositBTCAddress: "tb1qma629cu92skg0t86lftyaf9uflzwhp7jk63h6mpmv3ezh6puvdhs6w2r05", + bitcoinDepositorAddress: "mjc2zGWypwpNyDi4ZxGbBNnUA84bfgiwYc", + predictedEthereumDepositorAddress: EthereumAddress.from( + "0x9996baD9C879B1643Ac921454815F93BadA090AB", + ), }, estimateDepositFees: { amount: 10_000_000n, // 0.1 BTC, @@ -85,7 +88,7 @@ const stakingInitializationData: { walletPublicKeyHash: Hex.from("666666"), refundPublicKeyHash: Hex.from("0x2cd680318747b720d67bf4246eb7403b476adb34"), refundLocktime: Hex.from("888888"), - extraData: stakingModuleData.initializeStake.extraData, + extraData: stakingModuleData.initializeDeposit.extraData, }, mockedInitializeTxHash: Hex.from("999999"), fundingUtxo: { @@ -101,23 +104,29 @@ describe("Staking", () => { const contracts: AcreContracts = new MockAcreContracts() const messageSigner = new MockMessageSigner() const tbtc = new MockTBTC() - const orangeKit = {} as OrangeKitSdk + const orangeKit = new MockOrangeKitSdk() const staking: StakingModule = new StakingModule( contracts, messageSigner, tbtc, + // @ts-expect-error Error: Property '#private' is missing in type + // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. orangeKit, ) describe("initializeStake", () => { const { mockedDepositBTCAddress, - bitcoinRecoveryAddress, - staker, + bitcoinDepositorAddress, + predictedEthereumDepositorAddress, referral, extraData, - } = stakingModuleData.initializeStake + } = stakingModuleData.initializeDeposit + // TODO: Rename to `depositor`. + const staker = predictedEthereumDepositorAddress + const bitcoinRecoveryAddress = bitcoinDepositorAddress + const mockedDeposit = { getBitcoinAddress: jest.fn().mockResolvedValue(mockedDepositBTCAddress), detectFunding: jest.fn(), @@ -143,9 +152,24 @@ describe("Staking", () => { .fn() .mockReturnValue(mockedDeposit) + orangeKit.predictAddress = jest + .fn() + .mockResolvedValue( + `0x${predictedEthereumDepositorAddress.identifierHex}`, + ) + messageSigner.sign = jest.fn().mockResolvedValue(mockedSignedMessage) - result = await staking.initializeStake(bitcoinRecoveryAddress, referral) + result = await staking.initializeStake( + bitcoinDepositorAddress, + referral, + ) + }) + + it("should get Ethereum depositor owner address", () => { + expect(orangeKit.predictAddress).toHaveBeenCalledWith( + bitcoinDepositorAddress, + ) }) it("should encode extra data", () => { @@ -154,7 +178,7 @@ describe("Staking", () => { it("should initiate tBTC deposit", () => { expect(tbtc.deposits.initiateDepositWithProxy).toHaveBeenCalledWith( - bitcoinRecoveryAddress, + bitcoinDepositorAddress, contracts.bitcoinDepositor, extraData, ) @@ -195,7 +219,9 @@ describe("Staking", () => { const depositorAddress = ethers.Wallet.createRandom().address beforeEach(async () => { - mockedSignedMessage.verify.mockReturnValue(staker) + mockedSignedMessage.verify.mockReturnValue( + predictedEthereumDepositorAddress, + ) contracts.bitcoinDepositor.getChainIdentifier = jest .fn() .mockReturnValue(EthereumAddress.from(depositorAddress)) @@ -218,8 +244,9 @@ describe("Staking", () => { ], }, { - ethereumStakerAddress: staker.identifierHex, - bitcoinRecoveryAddress, + ethereumStakerAddress: + predictedEthereumDepositorAddress.identifierHex, + bitcoinRecoveryAddress: bitcoinDepositorAddress, }, ) }) @@ -250,7 +277,9 @@ describe("Staking", () => { describe("stake", () => { beforeAll(() => { - mockedSignedMessage.verify.mockReturnValue(staker) + mockedSignedMessage.verify.mockReturnValue( + predictedEthereumDepositorAddress, + ) }) describe("when the message has not been signed yet", () => { @@ -392,17 +421,18 @@ describe("Staking", () => { }) describe("sharesBalance", () => { - const { staker } = stakingModuleData.initializeStake + const depositor = EthereumAddress.from(ethers.Wallet.createRandom().address) + const expectedResult = 4294967295n let result: bigint beforeAll(async () => { contracts.stBTC.balanceOf = jest.fn().mockResolvedValue(expectedResult) - result = await staking.sharesBalance(staker) + result = await staking.sharesBalance(depositor) }) it("should get balance of stBTC", () => { - expect(contracts.stBTC.balanceOf).toHaveBeenCalledWith(staker) + expect(contracts.stBTC.balanceOf).toHaveBeenCalledWith(depositor) }) it("should return value of the basis for calculating final BTC balance", () => { @@ -412,17 +442,17 @@ describe("Staking", () => { describe("estimatedBitcoinBalance", () => { const expectedResult = 4294967295n - const { staker } = stakingModuleData.initializeStake + const depositor = EthereumAddress.from(ethers.Wallet.createRandom().address) let result: bigint beforeAll(async () => { contracts.stBTC.assetsBalanceOf = jest .fn() .mockResolvedValue(expectedResult) - result = await staking.estimatedBitcoinBalance(staker) + result = await staking.estimatedBitcoinBalance(depositor) }) it("should get staker's balance of tBTC tokens in vault ", () => { - expect(contracts.stBTC.assetsBalanceOf).toHaveBeenCalledWith(staker) + expect(contracts.stBTC.assetsBalanceOf).toHaveBeenCalledWith(depositor) }) it("should return maximum withdraw value", () => { diff --git a/sdk/test/utils/mock-orangekit.ts b/sdk/test/utils/mock-orangekit.ts new file mode 100644 index 000000000..7c4d437b6 --- /dev/null +++ b/sdk/test/utils/mock-orangekit.ts @@ -0,0 +1,37 @@ +import { OrangeKitSdk } from "@orangekit/sdk" +import { ContractTransaction } from "ethers" + +// @ts-expect-error Error: Property '#private' is missing in type +// 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. +// eslint-disable-next-line import/prefer-default-export +export class MockOrangeKitSdk implements OrangeKitSdk { + #chainId: number + + constructor() { + this.#chainId = 1 + } + + get chainId(): number { + return this.#chainId + } + + // eslint-disable-next-line class-methods-use-this + predictAddress( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + bitcoinAddress: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + publicKey?: string, + ): Promise<`0x${string}`> { + return Promise.resolve("0x126A16657b7293fdf5D963d6A6E8B9ec387D53e1") + } + + // eslint-disable-next-line class-methods-use-this + populateSafeDeploymentTransaction( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + bitcoinAddress: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + publicKey?: string, + ): Promise { + return Promise.resolve({} as ContractTransaction) + } +} From 55ea42527be0ae4f9da0f766ce734569bd640202 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 7 May 2024 21:04:57 +0200 Subject: [PATCH 05/45] Add `@swan-bitcoin/xpub-lib` To get the bitcoin address for `x'/0'/0'/0/0` path of a given extended public key to use always the same bitocin address as an identifier because Leder Wallet API returns the "next" public address where a user should receive funds. In the context of Bitcoin, the address is "renewed" each time funds are received. We need to use the same bitcoin address to generate always the same deposit owner address on ethereum. --- pnpm-lock.yaml | 135 +++++++++++++++++++++++++++++++++++++++++-- sdk/package.json | 4 +- sdk/src/typings.d.ts | 8 +++ sdk/tsconfig.json | 2 +- 4 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 sdk/src/typings.d.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d95170b78..f973a758f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,9 +138,15 @@ importers: '@keep-network/tbtc-v2.ts': specifier: 2.4.0-dev.3 version: 2.4.0-dev.3(@keep-network/keep-core@1.8.1-dev.0) + '@ledgerhq/wallet-api-client': + specifier: ^1.5.0 + version: 1.5.0 '@orangekit/sdk': - specifier: 1.0.0-beta.8 - version: 1.0.0-beta.8 + specifier: 1.0.0-beta.9 + version: 1.0.0-beta.9 + '@swan-bitcoin/xpub-lib': + specifier: ^0.1.5 + version: 0.1.5 ethers: specifier: ^6.10.0 version: 6.10.0 @@ -1639,6 +1645,14 @@ packages: '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.23.3) '@babel/helper-plugin-utils': 7.22.5 + /@babel/polyfill@7.12.1: + resolution: {integrity: sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==} + deprecated: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information. + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.13.11 + dev: false + /@babel/preset-env@7.23.7(@babel/core@7.23.3): resolution: {integrity: sha512-SY27X/GtTz/L4UryMNJ6p4fH4nsgWbz84y9FE0bQeWJP6O5BhgVCt53CotQKHCOeXJel8VyhlhujhlltKms/CA==} engines: {node: '>=6.9.0'} @@ -5658,8 +5672,8 @@ packages: - ethers dev: false - /@orangekit/sdk@1.0.0-beta.8: - resolution: {integrity: sha512-eyEjw5cuT0cigL6GOKrnKEbIMjFWVe1g5hu9dkFhaRHKfsLq8IzYXRpFYBZYdCCiBUj8aN7zMf9WevgMcBiU7g==} + /@orangekit/sdk@1.0.0-beta.9: + resolution: {integrity: sha512-qQja7YLt1aDolXqCTnC7a3aFTUi5I5DJTQ1yHawsV5OIdDlDGkxy+NQWDylzQXRcB2+Nmc1PMnHiM1ucFtVlzQ==} dependencies: '@orangekit/contracts': 1.0.0-beta.2(ethers@6.11.1) bitcoinjs-lib: 6.1.5 @@ -6748,6 +6762,13 @@ packages: - utf-8-validate dev: false + /@swan-bitcoin/xpub-lib@0.1.5: + resolution: {integrity: sha512-UdCs+GQ0Oh1kEMzlQ4BxQ2BwQJAH2TRUd1HkGZSACgc/yFtfS0fm5dD/Ae2Eaq52BBOoJRFf9AIcvTNT5tcPVQ==} + dependencies: + bitcoinjs-lib: 5.2.0 + unchained-bitcoin: 0.0.15 + dev: false + /@swc/helpers@0.4.14: resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} dependencies: @@ -8717,6 +8738,10 @@ packages: resolution: {integrity: sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==} dev: false + /bignumber.js@8.1.1: + resolution: {integrity: sha512-QD46ppGintwPGuL1KqmwhR0O+N2cZUg8JG/VzwI2e28sM9TqHjQB10lI4QAaMHVbLzwVLLAwEglpKPViWX+5NQ==} + dev: false + /bignumber.js@9.1.2: resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} dev: false @@ -8757,6 +8782,18 @@ packages: engines: {node: '>=8.0.0'} dev: false + /bip32@1.0.4: + resolution: {integrity: sha512-8T21eLWylZETolyqCPgia+MNp+kY37zFr7PTFDTPObHeNi9JlfG4qGIh8WzerIJidtwoK+NsWq2I5i66YfHoIw==} + engines: {node: '>=6.0.0'} + dependencies: + bs58check: 2.1.2 + create-hash: 1.2.0 + create-hmac: 1.1.7 + tiny-secp256k1: 1.1.6 + typeforce: 1.18.0 + wif: 2.0.6 + dev: false + /bip32@2.0.5: resolution: {integrity: sha512-zVY4VvJV+b2fS0/dcap/5XLlpqtgwyN8oRkuGgAS1uLOeEp0Yo6Tw2yUTozTtlrMJO3G8n4g/KX/XGFHW6Pq3g==} engines: {node: '>=6.0.0'} @@ -8779,6 +8816,66 @@ packages: randombytes: 2.1.0 dev: false + /bip66@1.1.5: + resolution: {integrity: sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /bitcoin-address-validation@0.2.9: + resolution: {integrity: sha512-47/XSK0yCA5Ivbt0YK5wCXm82TJWQRfkEiVRQScug5DNvmLCLeUekY6gwtH4dr7Ms2m13Nktq6/dhvsjdut0xg==} + dependencies: + base-x: 3.0.9 + bech32: 1.1.4 + hash.js: 1.1.7 + dev: false + + /bitcoin-ops@1.4.1: + resolution: {integrity: sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==} + dev: false + + /bitcoinjs-lib@4.0.5: + resolution: {integrity: sha512-gYs7K2hiY4Xb96J8AIF+Rx+hqbwjVlp5Zt6L6AnHOdzfe/2tODdmDxsEytnaxVCdhOUg0JnsGpl+KowBpGLxtA==} + engines: {node: '>=8.0.0'} + dependencies: + bech32: 1.1.4 + bip32: 1.0.4 + bip66: 1.1.5 + bitcoin-ops: 1.4.1 + bs58check: 2.1.2 + create-hash: 1.2.0 + create-hmac: 1.1.7 + merkle-lib: 2.0.10 + pushdata-bitcoin: 1.0.1 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + tiny-secp256k1: 1.1.6 + typeforce: 1.18.0 + varuint-bitcoin: 1.1.2 + wif: 2.0.6 + dev: false + + /bitcoinjs-lib@5.2.0: + resolution: {integrity: sha512-5DcLxGUDejgNBYcieMIUfjORtUeNWl828VWLHJGVKZCb4zIS1oOySTUr0LGmcqJBQgTBz3bGbRQla4FgrdQEIQ==} + engines: {node: '>=8.0.0'} + dependencies: + bech32: 1.1.4 + bip174: 2.1.1 + bip32: 2.0.5 + bip66: 1.1.5 + bitcoin-ops: 1.4.1 + bs58check: 2.1.2 + create-hash: 1.2.0 + create-hmac: 1.1.7 + merkle-lib: 2.0.10 + pushdata-bitcoin: 1.0.1 + randombytes: 2.1.0 + tiny-secp256k1: 1.1.6 + typeforce: 1.18.0 + varuint-bitcoin: 1.1.2 + wif: 2.0.6 + dev: false + /bitcoinjs-lib@6.1.5: resolution: {integrity: sha512-yuf6xs9QX/E8LWE2aMJPNd0IxGofwfuVOiYdNUESkc+2bHHVKjhJd8qewqapeoolh9fihzHGoDCB5Vkr57RZCQ==} engines: {node: '>=8.0.0'} @@ -9836,6 +9933,12 @@ packages: resolution: {integrity: sha512-taJ00IDOP+XYQEA2dAe4ESkmHt1fL8wzYDo3mRWQey8uO9UojlBFMneA65kMyxfYP7106c6LzWaq7/haDT6BCQ==} requiresBuild: true + /core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + requiresBuild: true + dev: false + /core-js@3.33.3: resolution: {integrity: sha512-lo0kOocUlLKmm6kv/FswQL8zbkH7mVsLJ/FULClOhv8WRVmKLVcs6XPNQAzstfeJTCHMyButEwG+z1kHxHoDZw==} requiresBuild: true @@ -15934,6 +16037,10 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + /merkle-lib@2.0.10: + resolution: {integrity: sha512-XrNQvUbn1DL5hKNe46Ccs+Tu3/PYOlrcZILuGUhb95oKBPjc/nmIC8D462PQkipVDGKRvwhn+QFg2cCdIvmDJA==} + dev: false + /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -17773,6 +17880,12 @@ packages: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} dev: true + /pushdata-bitcoin@1.0.1: + resolution: {integrity: sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ==} + dependencies: + bitcoin-ops: 1.4.1 + dev: false + /pvtsutils@1.3.5: resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} dependencies: @@ -20332,6 +20445,20 @@ packages: resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} engines: {node: '>=0.10.0'} + /unchained-bitcoin@0.0.15: + resolution: {integrity: sha512-IubzpcTT4spAV5uZ7bcpT50qNBKbda8epu2zilEvEBerUQNKXEidUIdccSNVYwabu4d04VkXK+3vyMX02Ilw+g==} + deprecated: future maintenance for this library has been moved to @caravan/bitcoin + dependencies: + '@babel/polyfill': 7.12.1 + bignumber.js: 8.1.1 + bip32: 2.0.5 + bitcoin-address-validation: 0.2.9 + bitcoinjs-lib: 4.0.5 + bs58check: 2.1.2 + bufio: 1.2.1 + core-js: 2.6.12 + dev: false + /underscore@1.12.1: resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} dev: false diff --git a/sdk/package.json b/sdk/package.json index 43a237452..f8aeb9002 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -29,7 +29,9 @@ "dependencies": { "@acre-btc/contracts": "workspace:*", "@keep-network/tbtc-v2.ts": "2.4.0-dev.3", - "@orangekit/sdk": "1.0.0-beta.8", + "@ledgerhq/wallet-api-client": "^1.5.0", + "@orangekit/sdk": "1.0.0-beta.9", + "@swan-bitcoin/xpub-lib": "^0.1.5", "ethers": "^6.10.0" } } diff --git a/sdk/src/typings.d.ts b/sdk/src/typings.d.ts new file mode 100644 index 000000000..07b4b64e5 --- /dev/null +++ b/sdk/src/typings.d.ts @@ -0,0 +1,8 @@ +type Network = "mainnet" | "testnet" + +declare module "@swan-bitcoin/xpub-lib" { + const addressFromExtPubKey: (xpubData: { + extPubKey: string + network: Network + }) => { address: string } +} diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index 79ce3e331..4ef31968f 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -13,5 +13,5 @@ "resolveJsonModule": true, "skipLibCheck": true }, - "include": ["src", "test", "jest.config.js"] + "include": ["src", "test", "src/typings.d.ts", "jest.config.js"] } From d2027a82108b8432cf9b8cd21d9e8bdf2f906f95 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 7 May 2024 21:10:45 +0200 Subject: [PATCH 06/45] Add `BitcoinProvider` interface We want to define an interface for communication with Bitcoin wallet and use it in Acre SDK. Currently it defines `getAddress` function because we need the bitcoin address to initialize the deposit. --- sdk/src/lib/bitcoin/providers/index.ts | 1 + sdk/src/lib/bitcoin/providers/provider.ts | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 sdk/src/lib/bitcoin/providers/index.ts create mode 100644 sdk/src/lib/bitcoin/providers/provider.ts diff --git a/sdk/src/lib/bitcoin/providers/index.ts b/sdk/src/lib/bitcoin/providers/index.ts new file mode 100644 index 000000000..4730a5370 --- /dev/null +++ b/sdk/src/lib/bitcoin/providers/index.ts @@ -0,0 +1 @@ +export * from "./provider" diff --git a/sdk/src/lib/bitcoin/providers/provider.ts b/sdk/src/lib/bitcoin/providers/provider.ts new file mode 100644 index 000000000..5467b79e6 --- /dev/null +++ b/sdk/src/lib/bitcoin/providers/provider.ts @@ -0,0 +1,3 @@ +export interface BitcoinProvider { + getAddress(): Promise +} From 0a80e298c87e63875c2a01f370c6d5e62391b131 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 7 May 2024 21:13:13 +0200 Subject: [PATCH 07/45] Implement `LedgerLiveWalletApiBitcoinProvider` Implements the `getAddress` function which always returns the same bitcoin address based on the extended public key. --- sdk/src/lib/bitcoin/providers/index.ts | 1 + .../ledger-live-wallet-api-provider.ts | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts diff --git a/sdk/src/lib/bitcoin/providers/index.ts b/sdk/src/lib/bitcoin/providers/index.ts index 4730a5370..02f9f0244 100644 --- a/sdk/src/lib/bitcoin/providers/index.ts +++ b/sdk/src/lib/bitcoin/providers/index.ts @@ -1 +1,2 @@ export * from "./provider" +export * from "./ledger-live-wallet-api-provider" diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts new file mode 100644 index 000000000..0417cc359 --- /dev/null +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -0,0 +1,70 @@ +import { + WalletAPIClient, + WindowMessageTransport, +} from "@ledgerhq/wallet-api-client" +import { addressFromExtPubKey } from "@swan-bitcoin/xpub-lib" +import { BitcoinProvider } from "./provider" +import { BitcoinNetwork } from "../network" + +type Network = Exclude + +class LedgerLiveWalletApiBitcoinProvider implements BitcoinProvider { + readonly #walletApiClient: WalletAPIClient + + readonly #windowMessageTransport: WindowMessageTransport + + readonly #accountId: string + + readonly #network: Network + + static async init(accountId: string, network: Network) { + const windowMessageTransport = new WindowMessageTransport() + windowMessageTransport.connect() + + const walletApiClient = new WalletAPIClient(windowMessageTransport) + + const currency = + network === BitcoinNetwork.Mainnet ? "bitcoin" : "bitcoin_testnet" + + const accounts = await walletApiClient.account.list({ + currencyIds: [currency], + }) + + const account = accounts.find((acc) => acc.id === accountId) + + if (!account) throw new Error("Account not found") + + return new LedgerLiveWalletApiBitcoinProvider( + accountId, + windowMessageTransport, + walletApiClient, + network, + ) + } + + private constructor( + _accountId: string, + _windowMessageTransport: WindowMessageTransport, + _walletApiClient: WalletAPIClient, + _network: Network, + ) { + this.#windowMessageTransport = _windowMessageTransport + this.#walletApiClient = _walletApiClient + this.#accountId = _accountId + this.#network = _network + } + + async getAddress(): Promise { + const xpub = await this.#walletApiClient.bitcoin.getXPub(this.#accountId) + + const { address } = addressFromExtPubKey({ + extPubKey: xpub, + network: this.#network, + }) + + return address + } +} + +// eslint-disable-next-line import/prefer-default-export +export { LedgerLiveWalletApiBitcoinProvider } From 260877b049828fabb0aca1e78290235618b8c684 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 8 May 2024 21:38:03 +0200 Subject: [PATCH 08/45] Update the `ethereum-signer` utils in SDK Use mixin pattern to share abstract logic but for different base classes. --- sdk/src/lib/utils/ethereum-signer.ts | 64 ++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/sdk/src/lib/utils/ethereum-signer.ts b/sdk/src/lib/utils/ethereum-signer.ts index d1dd5b639..db186479d 100644 --- a/sdk/src/lib/utils/ethereum-signer.ts +++ b/sdk/src/lib/utils/ethereum-signer.ts @@ -1,32 +1,68 @@ -import { AbstractSigner } from "ethers" +/* eslint-disable max-classes-per-file */ +import { VoidSigner, AbstractSigner } from "ethers" + +type AbstractSignerConstructor = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + abstract new (...args: any[]) => T /** - * This abstract signer adds necessary methods to be compatible with ethers v5 - * signer which is used in tBTC-v2.ts SDK. + * This abstract signer interface that defines necessary methods to be + * compatible with ethers v5 signer which is used in tBTC-v2.ts SDK. */ -abstract class EthereumSignerCompatibleWithEthersV5 extends AbstractSigner { +export interface IEthereumSignerCompatibleWithEthersV5 extends AbstractSigner { /** * @dev Required by ethers v5. */ - readonly _isSigner: boolean = true + readonly _isSigner: boolean // eslint-disable-next-line no-underscore-dangle - _checkProvider() { - if (!this.provider) throw new Error("Provider not available") - } + _checkProvider(): void /** * @dev Required by ethers v5. */ - async getChainId(): Promise { + getChainId(): Promise +} + +function ethereumSignerCompatibleWithEthersV5Mixin< + T extends AbstractSignerConstructor, +>(SignerBase: T) { + /** + * This abstract signer adds necessary methods to be compatible with ethers v5 + * signer which is used in tBTC-v2.ts SDK. + */ + abstract class EthereumSignerCompatibleWithEthersV5 + extends SignerBase + implements IEthereumSignerCompatibleWithEthersV5 + { + /** + * @dev Required by ethers v5. + */ + readonly _isSigner: boolean = true + // eslint-disable-next-line no-underscore-dangle - this._checkProvider() + _checkProvider() { + if (!this.provider) throw new Error("Provider not available") + } + + /** + * @dev Required by ethers v5. + */ + async getChainId(): Promise { + // eslint-disable-next-line no-underscore-dangle + this._checkProvider() - const network = await this.provider!.getNetwork() + const network = await this.provider!.getNetwork() - return Number(network.chainId) + return Number(network.chainId) + } } + + return EthereumSignerCompatibleWithEthersV5 } -// eslint-disable-next-line import/prefer-default-export -export { EthereumSignerCompatibleWithEthersV5 } +export const EthereumSignerCompatibleWithEthersV5 = + ethereumSignerCompatibleWithEthersV5Mixin(AbstractSigner) + +export const VoidSignerCompatibleWithEthersV5 = + ethereumSignerCompatibleWithEthersV5Mixin(VoidSigner) From df7380c6040479a4de46fd3f774895edb189a604 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 8 May 2024 21:54:28 +0200 Subject: [PATCH 09/45] Use `BitcoinProvider` to initialize the Acre SDK We want to relay on the `BitcoinProvider` implementation to get the bitcoin address because different wallets use different strategies. For example in Ledger Live the address is "renewed" each time funds are received in order to allow some privacy. In that case always getting the same bitcoin address is hidden under a given implementation. --- sdk/src/acre.ts | 49 ++++--- sdk/src/lib/ethereum/network.ts | 9 ++ sdk/src/modules/staking/index.ts | 25 ++-- .../modules/staking/stake-initialization.ts | 27 ++-- sdk/src/modules/tbtc/Tbtc.ts | 2 +- sdk/test/modules/staking.test.ts | 138 +++++++++--------- sdk/test/modules/tbtc/Tbtc.test.ts | 7 +- sdk/test/utils/mock-bitcoin-provider.ts | 8 + 8 files changed, 147 insertions(+), 118 deletions(-) create mode 100644 sdk/test/utils/mock-bitcoin-provider.ts diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 22bfa2694..313a1c791 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -1,21 +1,19 @@ import { OrangeKitSdk } from "@orangekit/sdk" +import { JsonRpcProvider } from "ethers" import { AcreContracts } from "./lib/contracts" -import { ChainEIP712Signer } from "./lib/eip712-signer" -import { - EthereumEIP712Signer, - EthereumNetwork, - getEthereumContracts, -} from "./lib/ethereum" +import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" import { StakingModule } from "./modules/staking" import Tbtc from "./modules/tbtc" -import { EthereumSignerCompatibleWithEthersV5 } from "./lib/utils" +import { VoidSignerCompatibleWithEthersV5 } from "./lib/utils" +import { BitcoinProvider } from "./lib/bitcoin/providers" +import { getChainIdByNetwork } from "./lib/ethereum/network" class Acre { readonly #tbtc: Tbtc readonly #orangeKit: OrangeKitSdk - readonly #messageSigner: ChainEIP712Signer + readonly #bitcoinProvider: BitcoinProvider public readonly contracts: AcreContracts @@ -23,33 +21,48 @@ class Acre { constructor( _contracts: AcreContracts, - _messageSigner: ChainEIP712Signer, + _bitcoinProvider: BitcoinProvider, _orangeKit: OrangeKitSdk, _tbtc: Tbtc, ) { this.contracts = _contracts this.#tbtc = _tbtc this.#orangeKit = _orangeKit - this.#messageSigner = _messageSigner + this.#bitcoinProvider = _bitcoinProvider this.staking = new StakingModule( this.contracts, - this.#messageSigner, + this.#bitcoinProvider, this.#orangeKit, this.#tbtc, ) } static async initializeEthereum( - signer: EthereumSignerCompatibleWithEthersV5, + bitcoinProvider: BitcoinProvider, + // TODO: change to Bitcoin network. network: EthereumNetwork, tbtcApiUrl: string, ): Promise { - const chainId = await signer.getChainId() + const chainId = getChainIdByNetwork(network) + const orangeKit = await Acre.#getOrangeKitSDK(chainId) - const contracts = getEthereumContracts(signer, network) - const messages = new EthereumEIP712Signer(signer) + // TODO: Should we store this address in context so that we do not to + // recalculate it when necessary? + const ethereumAddress = await orangeKit.predictAddress( + await bitcoinProvider.getAddress(), + ) - const orangeKit = await Acre.#getOrangeKitSDK(chainId) + // TODO: Should we hardcode the url on the Acre side or pass it as a config? + const provider = new JsonRpcProvider( + "https://eth-sepolia.g.alchemy.com/v2/", + ) + + const signer = new VoidSignerCompatibleWithEthersV5( + ethereumAddress, + provider, + ) + + const contracts = getEthereumContracts(signer, network) const tbtc = await Tbtc.initialize( signer, @@ -58,14 +71,14 @@ class Acre { contracts.bitcoinDepositor, ) - return new Acre(contracts, messages, orangeKit, tbtc) + return new Acre(contracts, bitcoinProvider, orangeKit, tbtc) } static #getOrangeKitSDK(chainId: number): Promise { return OrangeKitSdk.init( chainId, // TODO: pass rpc url as config. - "https://eth-mainnet.g.alchemy.com/v2/", + "https://eth-sepolia.g.alchemy.com/v2/", ) } } diff --git a/sdk/src/lib/ethereum/network.ts b/sdk/src/lib/ethereum/network.ts index e5a7a7dba..902e0e90a 100644 --- a/sdk/src/lib/ethereum/network.ts +++ b/sdk/src/lib/ethereum/network.ts @@ -1 +1,10 @@ export type EthereumNetwork = "mainnet" | "sepolia" + +const NETWORK_TO_CHAIN_ID: Record = { + mainnet: 1, + sepolia: 11155111, +} + +export function getChainIdByNetwork(network: EthereumNetwork) { + return NETWORK_TO_CHAIN_ID[network] +} diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index bb27a7233..a2592defa 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -1,10 +1,10 @@ import { ChainIdentifier, EthereumAddress } from "@keep-network/tbtc-v2.ts" import { OrangeKitSdk } from "@orangekit/sdk" import { AcreContracts, DepositFees } from "../../lib/contracts" -import { ChainEIP712Signer } from "../../lib/eip712-signer" import { StakeInitialization } from "./stake-initialization" import { fromSatoshi, toSatoshi } from "../../lib/utils" import Tbtc from "../tbtc" +import { BitcoinProvider } from "../../lib/bitcoin/providers" export { DepositReceipt } from "../tbtc" @@ -29,7 +29,7 @@ class StakingModule { /** * Typed structured data signer. */ - readonly #messageSigner: ChainEIP712Signer + readonly #bitcoinProvider: BitcoinProvider /** * tBTC Module. @@ -43,32 +43,29 @@ class StakingModule { constructor( _contracts: AcreContracts, - _messageSigner: ChainEIP712Signer, + _bitcoinProvider: BitcoinProvider, _orangeKit: OrangeKitSdk, _tbtc: Tbtc, ) { this.#contracts = _contracts - this.#messageSigner = _messageSigner + this.#bitcoinProvider = _bitcoinProvider this.#tbtc = _tbtc this.#orangeKit = _orangeKit } /** * Initializes the Acre deposit process. - * @param depositor The Bitcoin depositor address. Supported addresses: - * `P2WPKH`, `P2PKH`, `P2SH-P2WPKH`. * @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 `depositor` address is used as bitcoin recovery - * address then the `depositor` must be `P2WPKH` or `P2PKH`. + * `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`. * @returns Object represents the deposit process. */ - async initializeStake( - depositor: string, - referral: number, - bitcoinRecoveryAddress?: string, - ) { + async initializeStake(referral: number, bitcoinRecoveryAddress?: string) { + const depositor = 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` @@ -90,7 +87,7 @@ class StakingModule { return new StakeInitialization( this.#contracts, - this.#messageSigner, + this.#bitcoinProvider, finalBitcoinRecoveryAddress, depositOwnerChainAddress, tbtcDeposit, diff --git a/sdk/src/modules/staking/stake-initialization.ts b/sdk/src/modules/staking/stake-initialization.ts index 722067179..8e5db6be7 100644 --- a/sdk/src/modules/staking/stake-initialization.ts +++ b/sdk/src/modules/staking/stake-initialization.ts @@ -3,13 +3,13 @@ import TbtcDeposit from "../tbtc/Deposit" import type { DepositReceipt } from "." import { - ChainEIP712Signer, ChainSignedMessage, Domain, Message, Types, } from "../../lib/eip712-signer" import { AcreContracts, ChainIdentifier } from "../../lib/contracts" +import { BitcoinProvider } from "../../lib/bitcoin/providers" /** * Represents an instance of the staking flow. Staking flow requires a few steps @@ -22,9 +22,9 @@ class StakeInitialization { readonly #contracts: AcreContracts /** - * Typed structured data signer. + * Bitcoin wallet provider. */ - readonly #messageSigner: ChainEIP712Signer + readonly #bitcoinProvider: BitcoinProvider /** * Component representing an instance of the tBTC deposit process. @@ -51,13 +51,13 @@ class StakeInitialization { constructor( _contracts: AcreContracts, - _messageSigner: ChainEIP712Signer, + _bitcoinProvider: BitcoinProvider, _bitcoinRecoveryAddress: string, _staker: ChainIdentifier, _tbtcDeposit: TbtcDeposit, ) { this.#contracts = _contracts - this.#messageSigner = _messageSigner + this.#bitcoinProvider = _bitcoinProvider this.#staker = _staker this.#bitcoinRecoveryAddress = _bitcoinRecoveryAddress this.#tbtcDeposit = _tbtcDeposit @@ -85,18 +85,19 @@ class StakeInitialization { * @dev Use this function as a second step of the staking flow. Signed message * is required to stake BTC. */ - async signMessage() { - const { domain, types, message } = this.#getStakeMessageTypedData() + signMessage() { + // TODO: Add `signMessage` to the `BitcoinProvider` and use it. + // const { domain, types, message } = this.#getStakeMessageTypedData() - const signedMessage = await this.#messageSigner.sign(domain, types, message) + // const signedMessage = await this.#messageSigner.sign(domain, types, message) - const addressFromSignature = signedMessage.verify() + // const addressFromSignature = signedMessage.verify() - if (!this.#staker.equals(addressFromSignature)) { - throw new Error("Invalid staker address") - } + // if (!this.#staker.equals(addressFromSignature)) { + // throw new Error("Invalid staker address") + // } - this.#signedMessage = signedMessage + this.#signedMessage = {} as ChainSignedMessage } /** diff --git a/sdk/src/modules/tbtc/Tbtc.ts b/sdk/src/modules/tbtc/Tbtc.ts index ddafdcc94..6e0782c79 100644 --- a/sdk/src/modules/tbtc/Tbtc.ts +++ b/sdk/src/modules/tbtc/Tbtc.ts @@ -5,7 +5,7 @@ import { BitcoinDepositor } from "../../lib/contracts" import { EthereumNetwork } from "../../lib/ethereum" import Deposit from "./Deposit" -import { EthereumSignerCompatibleWithEthersV5 } from "../../lib/utils" +import { IEthereumSignerCompatibleWithEthersV5 as EthereumSignerCompatibleWithEthersV5 } from "../../lib/utils" /** * Represents the tBTC module. diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index 60d7ceb7e..09ca44e93 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -15,6 +15,7 @@ import { MockMessageSigner } from "../utils/mock-message-signer" 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" const stakingModuleData: { initializeDeposit: { @@ -103,13 +104,17 @@ const stakingInitializationData: { describe("Staking", () => { const contracts: AcreContracts = new MockAcreContracts() - const messageSigner = new MockMessageSigner() + const bitcoinProvider = new MockBitcoinProvider() const orangeKit = new MockOrangeKitSdk() const tbtc = new MockTbtc() + bitcoinProvider.getAddress.mockResolvedValue( + stakingModuleData.initializeDeposit.bitcoinDepositorAddress, + ) + const staking: StakingModule = new StakingModule( contracts, - messageSigner, + bitcoinProvider, // @ts-expect-error Error: Property '#private' is missing in type // 'MockOrangeKitSdk' but required in type 'OrangeKitSdk'. orangeKit, @@ -159,12 +164,7 @@ describe("Staking", () => { `0x${predictedEthereumDepositorAddress.identifierHex}`, ) - messageSigner.sign = jest.fn().mockResolvedValue(mockedSignedMessage) - - result = await staking.initializeStake( - bitcoinDepositorAddress, - referral, - ) + result = await staking.initializeStake(referral) }) it("should get Ethereum depositor owner address", () => { @@ -186,7 +186,6 @@ describe("Staking", () => { expect(result.getBitcoinAddress).toBeDefined() expect(result.getDepositReceipt).toBeDefined() expect(result.stake).toBeDefined() - expect(result.signMessage).toBeDefined() }) describe("StakeInitialization", () => { @@ -211,66 +210,66 @@ describe("Staking", () => { }) }) - describe("signMessage", () => { - describe("when signing by valid staker", () => { - const depositorAddress = ethers.Wallet.createRandom().address - - beforeEach(async () => { - mockedSignedMessage.verify.mockReturnValue( - predictedEthereumDepositorAddress, - ) - contracts.bitcoinDepositor.getChainIdentifier = jest - .fn() - .mockReturnValue(EthereumAddress.from(depositorAddress)) - - await result.signMessage() - }) - - it("should sign message", () => { - expect(messageSigner.sign).toHaveBeenCalledWith( - { - name: "BitcoinDepositor", - version: "1", - verifyingContract: - contracts.bitcoinDepositor.getChainIdentifier(), - }, - { - Stake: [ - { name: "ethereumStakerAddress", type: "address" }, - { name: "bitcoinRecoveryAddress", type: "string" }, - ], - }, - { - ethereumStakerAddress: - predictedEthereumDepositorAddress.identifierHex, - bitcoinRecoveryAddress: bitcoinDepositorAddress, - }, - ) - }) - - it("should verify signed message", () => { - expect(mockedSignedMessage.verify).toHaveBeenCalled() - }) - }) - - describe("when signing by invalid staker", () => { - const invalidStaker = EthereumAddress.from( - ethers.Wallet.createRandom().address, - ) - - beforeEach(() => { - mockedSignedMessage.verify = jest - .fn() - .mockResolvedValue(invalidStaker) - }) - - it("should throw an error", async () => { - await expect(result.signMessage()).rejects.toThrow( - "Invalid staker address", - ) - }) - }) - }) + // describe("signMessage", () => { + // describe("when signing by valid staker", () => { + // const depositorAddress = ethers.Wallet.createRandom().address + + // beforeEach(async () => { + // mockedSignedMessage.verify.mockReturnValue( + // predictedEthereumDepositorAddress, + // ) + // contracts.bitcoinDepositor.getChainIdentifier = jest + // .fn() + // .mockReturnValue(EthereumAddress.from(depositorAddress)) + + // await result.signMessage() + // }) + + // it("should sign message", () => { + // expect(messageSigner.sign).toHaveBeenCalledWith( + // { + // name: "BitcoinDepositor", + // version: "1", + // verifyingContract: + // contracts.bitcoinDepositor.getChainIdentifier(), + // }, + // { + // Stake: [ + // { name: "ethereumStakerAddress", type: "address" }, + // { name: "bitcoinRecoveryAddress", type: "string" }, + // ], + // }, + // { + // ethereumStakerAddress: + // predictedEthereumDepositorAddress.identifierHex, + // bitcoinRecoveryAddress: bitcoinDepositorAddress, + // }, + // ) + // }) + + // it("should verify signed message", () => { + // expect(mockedSignedMessage.verify).toHaveBeenCalled() + // }) + // }) + + // describe("when signing by invalid staker", () => { + // const invalidStaker = EthereumAddress.from( + // ethers.Wallet.createRandom().address, + // ) + + // beforeEach(() => { + // mockedSignedMessage.verify = jest + // .fn() + // .mockResolvedValue(invalidStaker) + // }) + + // it("should throw an error", async () => { + // await expect(result.signMessage()).rejects.toThrow( + // "Invalid staker address", + // ) + // }) + // }) + // }) describe("stake", () => { beforeAll(() => { @@ -290,7 +289,6 @@ describe("Staking", () => { beforeAll(async () => { mockedDeposit.waitForFunding.mockResolvedValue(undefined) - await result.signMessage() depositId = await result.stake() }) diff --git a/sdk/test/modules/tbtc/Tbtc.test.ts b/sdk/test/modules/tbtc/Tbtc.test.ts index 043ddec7c..b63055ac7 100644 --- a/sdk/test/modules/tbtc/Tbtc.test.ts +++ b/sdk/test/modules/tbtc/Tbtc.test.ts @@ -4,7 +4,10 @@ import { Deposit as TbtcSdkDeposit, } from "@keep-network/tbtc-v2.ts" -import { EthereumSignerCompatibleWithEthersV5 } from "../../../src" +import { + EthereumSignerCompatibleWithEthersV5, + IEthereumSignerCompatibleWithEthersV5, +} from "../../../src" import Deposit from "../../../src/modules/tbtc/Deposit" import TbtcApi from "../../../src/lib/api/TbtcApi" @@ -43,7 +46,7 @@ describe("Tbtc", () => { const { bitcoinDepositor } = new MockAcreContracts() describe("initialize", () => { - const mockedSigner: EthereumSignerCompatibleWithEthersV5 = + const mockedSigner: IEthereumSignerCompatibleWithEthersV5 = new MockEthereumSignerCompatibleWithEthersV5() describe("when network is mainnet", () => { diff --git a/sdk/test/utils/mock-bitcoin-provider.ts b/sdk/test/utils/mock-bitcoin-provider.ts new file mode 100644 index 000000000..43067d42e --- /dev/null +++ b/sdk/test/utils/mock-bitcoin-provider.ts @@ -0,0 +1,8 @@ +import { BitcoinProvider } from "../../src/lib/bitcoin/providers" + +class MockBitcoinProvider implements BitcoinProvider { + getAddress = jest.fn() +} + +// eslint-disable-next-line import/prefer-default-export +export { MockBitcoinProvider } From 0965b4ca3a3d32683b056dacd4f092d521847a7e Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 9 May 2024 14:01:41 +0200 Subject: [PATCH 10/45] Update staking unit tests Cover a case when the custom bitcoin recovery address is passed to initialize deposit. --- sdk/test/modules/staking.test.ts | 309 +++++++++++++++++-------------- 1 file changed, 166 insertions(+), 143 deletions(-) diff --git a/sdk/test/modules/staking.test.ts b/sdk/test/modules/staking.test.ts index 09ca44e93..e70a42be0 100644 --- a/sdk/test/modules/staking.test.ts +++ b/sdk/test/modules/staking.test.ts @@ -11,7 +11,6 @@ import { } from "../../src" import * as satoshiConverter from "../../src/lib/utils/satoshi-converter" import { MockAcreContracts } from "../utils/mock-acre-contracts" -import { MockMessageSigner } from "../utils/mock-message-signer" import { MockOrangeKitSdk } from "../utils/mock-orangekit" import { MockTbtc } from "../utils/mock-tbtc" import { DepositReceipt } from "../../src/modules/tbtc" @@ -131,7 +130,6 @@ describe("Staking", () => { } = stakingModuleData.initializeDeposit // TODO: Rename to `depositor`. const staker = predictedEthereumDepositorAddress - const bitcoinRecoveryAddress = bitcoinDepositorAddress const { mockedDepositId } = stakingInitializationData @@ -142,172 +140,197 @@ describe("Staking", () => { createDeposit: jest.fn().mockReturnValue(mockedDepositId), } - describe("with default depositor proxy implementation", () => { - const mockedSignedMessage = { verify: jest.fn() } - - let result: StakeInitialization - - beforeEach(async () => { - contracts.bitcoinDepositor.decodeExtraData = jest - .fn() - .mockReturnValue({ staker, referral }) - - contracts.bitcoinDepositor.encodeExtraData = jest - .fn() - .mockReturnValue(extraData) - - tbtc.initiateDeposit = jest.fn().mockReturnValue(mockedDeposit) + describe.each([ + { + bitcoinRecoveryAddress: undefined, + description: "when the bitcoin recovery address is not defined", + }, + { + bitcoinRecoveryAddress: "tb1qumuaw3exkxdhtut0u85latkqfz4ylgwstkdzsx", + description: "when the bitcoin recovery address is defined", + }, + ])( + "$description", + ({ bitcoinRecoveryAddress: _bitcoinRecoveryAddress }) => { + const mockedSignedMessage = { verify: jest.fn() } + const bitcoinRecoveryAddress = + _bitcoinRecoveryAddress ?? bitcoinDepositorAddress + + let result: StakeInitialization + + beforeEach(async () => { + contracts.bitcoinDepositor.decodeExtraData = jest + .fn() + .mockReturnValue({ staker, referral }) + + contracts.bitcoinDepositor.encodeExtraData = jest + .fn() + .mockReturnValue(extraData) + + tbtc.initiateDeposit = jest.fn().mockReturnValue(mockedDeposit) + + orangeKit.predictAddress = jest + .fn() + .mockResolvedValue( + `0x${predictedEthereumDepositorAddress.identifierHex}`, + ) - orangeKit.predictAddress = jest - .fn() - .mockResolvedValue( - `0x${predictedEthereumDepositorAddress.identifierHex}`, + result = await staking.initializeStake( + referral, + bitcoinRecoveryAddress, ) + }) - result = await staking.initializeStake(referral) - }) - - it("should get Ethereum depositor owner address", () => { - expect(orangeKit.predictAddress).toHaveBeenCalledWith( - bitcoinDepositorAddress, - ) - }) - - it("should initiate tBTC deposit", () => { - expect(tbtc.initiateDeposit).toHaveBeenCalledWith( - predictedEthereumDepositorAddress, - bitcoinRecoveryAddress, - referral, - ) - }) - - it("should return stake initialization object", () => { - expect(result).toBeInstanceOf(StakeInitialization) - expect(result.getBitcoinAddress).toBeDefined() - expect(result.getDepositReceipt).toBeDefined() - expect(result.stake).toBeDefined() - }) - - describe("StakeInitialization", () => { - const { depositReceipt } = stakingInitializationData + it("should get the bitcoin address from bitcoin provider", () => { + expect(bitcoinProvider.getAddress).toHaveBeenCalled() + }) - beforeAll(() => { - mockedDeposit.getReceipt.mockReturnValue(depositReceipt) + it("should get Ethereum depositor owner address", () => { + expect(orangeKit.predictAddress).toHaveBeenCalledWith( + bitcoinDepositorAddress, + ) }) - describe("getBitcoinAddress", () => { - it("should return bitcoin deposit address", async () => { - expect(await result.getBitcoinAddress()).toBe( - mockedDepositBTCAddress, - ) - }) + it("should initiate tBTC deposit", () => { + expect(tbtc.initiateDeposit).toHaveBeenCalledWith( + predictedEthereumDepositorAddress, + bitcoinRecoveryAddress, + referral, + ) }) - describe("getDepositReceipt", () => { - it("should return tbtc deposit receipt", () => { - expect(result.getDepositReceipt()).toBe(depositReceipt) - expect(mockedDeposit.getReceipt).toHaveBeenCalled() - }) + it("should return stake initialization object", () => { + expect(result).toBeInstanceOf(StakeInitialization) + expect(result.getBitcoinAddress).toBeDefined() + expect(result.getDepositReceipt).toBeDefined() + expect(result.stake).toBeDefined() }) - // describe("signMessage", () => { - // describe("when signing by valid staker", () => { - // const depositorAddress = ethers.Wallet.createRandom().address - - // beforeEach(async () => { - // mockedSignedMessage.verify.mockReturnValue( - // predictedEthereumDepositorAddress, - // ) - // contracts.bitcoinDepositor.getChainIdentifier = jest - // .fn() - // .mockReturnValue(EthereumAddress.from(depositorAddress)) - - // await result.signMessage() - // }) - - // it("should sign message", () => { - // expect(messageSigner.sign).toHaveBeenCalledWith( - // { - // name: "BitcoinDepositor", - // version: "1", - // verifyingContract: - // contracts.bitcoinDepositor.getChainIdentifier(), - // }, - // { - // Stake: [ - // { name: "ethereumStakerAddress", type: "address" }, - // { name: "bitcoinRecoveryAddress", type: "string" }, - // ], - // }, - // { - // ethereumStakerAddress: - // predictedEthereumDepositorAddress.identifierHex, - // bitcoinRecoveryAddress: bitcoinDepositorAddress, - // }, - // ) - // }) - - // it("should verify signed message", () => { - // expect(mockedSignedMessage.verify).toHaveBeenCalled() - // }) - // }) - - // describe("when signing by invalid staker", () => { - // const invalidStaker = EthereumAddress.from( - // ethers.Wallet.createRandom().address, - // ) - - // beforeEach(() => { - // mockedSignedMessage.verify = jest - // .fn() - // .mockResolvedValue(invalidStaker) - // }) - - // it("should throw an error", async () => { - // await expect(result.signMessage()).rejects.toThrow( - // "Invalid staker address", - // ) - // }) - // }) - // }) - - describe("stake", () => { + describe("StakeInitialization", () => { + const { depositReceipt } = stakingInitializationData + beforeAll(() => { - mockedSignedMessage.verify.mockReturnValue( - predictedEthereumDepositorAddress, - ) + mockedDeposit.getReceipt.mockReturnValue(depositReceipt) }) - describe("when the message has not been signed yet", () => { - it("should throw an error", async () => { - await expect(result.stake()).rejects.toThrow("Sign message first") + describe("getBitcoinAddress", () => { + it("should return bitcoin deposit address", async () => { + expect(await result.getBitcoinAddress()).toBe( + mockedDepositBTCAddress, + ) }) }) - describe("when message has already been signed", () => { - let depositId: string - - beforeAll(async () => { - mockedDeposit.waitForFunding.mockResolvedValue(undefined) - - depositId = await result.stake() + describe("getDepositReceipt", () => { + it("should return tbtc deposit receipt", () => { + expect(result.getDepositReceipt()).toBe(depositReceipt) + expect(mockedDeposit.getReceipt).toHaveBeenCalled() }) + }) - it("should wait for funding", () => { - expect(mockedDeposit.waitForFunding).toHaveBeenCalled() + // describe("signMessage", () => { + // describe("when signing by valid staker", () => { + // const depositorAddress = ethers.Wallet.createRandom().address + + // beforeEach(async () => { + // mockedSignedMessage.verify.mockReturnValue( + // predictedEthereumDepositorAddress, + // ) + // contracts.bitcoinDepositor.getChainIdentifier = jest + // .fn() + // .mockReturnValue(EthereumAddress.from(depositorAddress)) + + // await result.signMessage() + // }) + + // it("should sign message", () => { + // expect(messageSigner.sign).toHaveBeenCalledWith( + // { + // name: "BitcoinDepositor", + // version: "1", + // verifyingContract: + // contracts.bitcoinDepositor.getChainIdentifier(), + // }, + // { + // Stake: [ + // { name: "ethereumStakerAddress", type: "address" }, + // { name: "bitcoinRecoveryAddress", type: "string" }, + // ], + // }, + // { + // ethereumStakerAddress: + // predictedEthereumDepositorAddress.identifierHex, + // bitcoinRecoveryAddress: bitcoinDepositorAddress, + // }, + // ) + // }) + + // it("should verify signed message", () => { + // expect(mockedSignedMessage.verify).toHaveBeenCalled() + // }) + // }) + + // describe("when signing by invalid staker", () => { + // const invalidStaker = EthereumAddress.from( + // ethers.Wallet.createRandom().address, + // ) + + // beforeEach(() => { + // mockedSignedMessage.verify = jest + // .fn() + // .mockResolvedValue(invalidStaker) + // }) + + // it("should throw an error", async () => { + // await expect(result.signMessage()).rejects.toThrow( + // "Invalid staker address", + // ) + // }) + // }) + // }) + + describe("stake", () => { + beforeAll(() => { + mockedSignedMessage.verify.mockReturnValue( + predictedEthereumDepositorAddress, + ) }) - it("should create the deposit", () => { - expect(mockedDeposit.createDeposit).toHaveBeenCalled() + describe("when the message has not been signed yet", () => { + it("should throw an error", async () => { + await expect(result.stake()).rejects.toThrow( + "Sign message first", + ) + }) }) - it("should return deposit id", () => { - expect(depositId).toBe(mockedDepositId) + describe("when message has already been signed", () => { + let depositId: string + + beforeAll(async () => { + mockedDeposit.waitForFunding.mockResolvedValue(undefined) + + result.signMessage() + + depositId = await result.stake() + }) + + it("should wait for funding", () => { + expect(mockedDeposit.waitForFunding).toHaveBeenCalled() + }) + + it("should create the deposit", () => { + expect(mockedDeposit.createDeposit).toHaveBeenCalled() + }) + + it("should return deposit id", () => { + expect(depositId).toBe(mockedDepositId) + }) }) }) }) - }) - }) + }, + ) }) describe("sharesBalance", () => { From da302d8c21180ec6e4e945686c30ddea71987064 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 9 May 2024 14:27:36 +0200 Subject: [PATCH 11/45] Update docs Explain in the docs that 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. --- sdk/src/modules/staking/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index a2592defa..800d5fa96 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -60,7 +60,10 @@ class StakingModule { * 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`. + * 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) { From aac6f6f7511d684a0a6d77033cfe7f25582b3611 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 9 May 2024 14:29:42 +0200 Subject: [PATCH 12/45] Rename variable `depositor` -> `depositorOwnerBitcoinAddress` - let's avoid `naming it `depositor` so we don't confuse it with the `BitcoinDepositor` contract. --- sdk/src/modules/staking/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index 800d5fa96..b5e25b754 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -67,7 +67,8 @@ class StakingModule { * @returns Object represents the deposit process. */ async initializeStake(referral: number, bitcoinRecoveryAddress?: string) { - const depositor = await this.#bitcoinProvider.getAddress() + const depositorOwnerBitcoinAddress = + 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 @@ -75,12 +76,13 @@ class StakingModule { // and `lib`. Currently we support only `Ethereum` so here we force to // `EthereumAddress`. const depositOwnerChainAddress = EthereumAddress.from( - await this.#orangeKit.predictAddress(depositor), + await this.#orangeKit.predictAddress(depositorOwnerBitcoinAddress), ) // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if // address is not supported. - const finalBitcoinRecoveryAddress = bitcoinRecoveryAddress ?? depositor + const finalBitcoinRecoveryAddress = + bitcoinRecoveryAddress ?? depositorOwnerBitcoinAddress const tbtcDeposit = await this.#tbtc.initiateDeposit( depositOwnerChainAddress, From d2f3a514b480d99ced6441ddf93a571809ff8a51 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 10 May 2024 23:43:29 +0200 Subject: [PATCH 13/45] Remove @babel/preset-env According to the [jest documentation](https://jestjs.io/docs/getting-started#using-typescript) there are two ways to get TypeScript to work with Jest: - babel - ts-jest We already use ts-jest preset in jest configuration, so we don't need to import babel presets. --- pnpm-lock.yaml | 3 --- sdk/babel.config.js | 1 - sdk/package.json | 1 - 3 files changed, 5 deletions(-) delete mode 100644 sdk/babel.config.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f973a758f..2db3f9d37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,9 +151,6 @@ importers: specifier: ^6.10.0 version: 6.10.0 devDependencies: - '@babel/preset-env': - specifier: ^7.23.7 - version: 7.23.7(@babel/core@7.23.3) '@ethersproject/bignumber': specifier: ^5.7.0 version: 5.7.0 diff --git a/sdk/babel.config.js b/sdk/babel.config.js deleted file mode 100644 index bb1dd9599..000000000 --- a/sdk/babel.config.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { presets: ["@babel/preset-env"] } diff --git a/sdk/package.json b/sdk/package.json index f8aeb9002..808b8d711 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -14,7 +14,6 @@ "test": "jest --verbose" }, "devDependencies": { - "@babel/preset-env": "^7.23.7", "@ethersproject/bignumber": "^5.7.0", "@thesis-co/eslint-config": "github:thesis/eslint-config#7b9bc8c", "@types/jest": "^29.5.11", From ddfaae425aedcd13c795bd2bcb9fb6ab9dc75648 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 10 May 2024 23:48:56 +0200 Subject: [PATCH 14/45] Use JestConfigWithTsJest type in jest config file We use the JestConfigWithTsJest type to be explicit about the type of the config object we're dealing with. --- sdk/jest.config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/jest.config.ts b/sdk/jest.config.ts index 37ce37c74..d15006424 100644 --- a/sdk/jest.config.ts +++ b/sdk/jest.config.ts @@ -1,4 +1,8 @@ -export default { +import type { JestConfigWithTsJest } from "ts-jest" + +const jestConfig: JestConfigWithTsJest = { preset: "ts-jest", testPathIgnorePatterns: ["/dist/", "/node_modules/"], } + +export default jestConfig From 553b5be6ab75587dcfc3a17c00ee25b3e19ae4dc Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Sat, 11 May 2024 00:18:27 +0200 Subject: [PATCH 15/45] Fix Jest and @orangekit/sdk support After we added @orangekit/sdk import Jest started to complain with: ``` Jest encountered an unexpected token Details: /Users/jakub/workspace/acre/acre/node_modules/.pnpm/@orangekit+sdk@1.0.0-beta.9/node_modules/@orangekit/sdk/dist/src/index.js:1 ({"Object.":function(module,exports,require,__dirname,__filename,jest){export * from "./lib"; ^^^^^^ SyntaxError: Unexpected token 'export' > 1 | import { OrangeKitSdk } from "@orangekit/sdk" | ^ 2 | import { JsonRpcProvider } from "ethers" 3 | import { AcreContracts } from "./lib/contracts" 4 | import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" at Runtime.createScriptFromCode (../node_modules/.pnpm/jest-runtime@29.7.0/node_modules/jest-runtime/build/index.js:1505:14) at Object. (src/acre.ts:1:1) at Object. (src/index.ts:9:1) at Object. (test/modules/tbtc/Tbtc.test.ts:7:1) ``` The `@orangekit/sdk` module exports `dist/` directory containing JS files in ESM syntax. Jest doesn't support ESM syntax, so we have to convert these JS files to CommonJS syntax with `ts-jest/presets/js-with-ts` preset. We have to define `transformIgnorePatterns` property as `node_modules/` directory is excluded from transformations by default. We also had to add `allowJs: true` in tsconfig.json to support transformation of JS files. Reference: - https://stackoverflow.com/questions/75452411/why-isnt-jest-handling-es-module-dependencies - https://stackoverflow.com/questions/49263429/jest-gives-an-error-syntaxerror-unexpected-token-export?noredirect=1&lq=1 --- sdk/jest.config.ts | 3 ++- sdk/tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk/jest.config.ts b/sdk/jest.config.ts index d15006424..04e040912 100644 --- a/sdk/jest.config.ts +++ b/sdk/jest.config.ts @@ -1,8 +1,9 @@ import type { JestConfigWithTsJest } from "ts-jest" const jestConfig: JestConfigWithTsJest = { - preset: "ts-jest", + preset: "ts-jest/presets/js-with-ts", testPathIgnorePatterns: ["/dist/", "/node_modules/"], + transformIgnorePatterns: ["/node_modules/(?!@orangekit/sdk)/"], } export default jestConfig diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index 4ef31968f..df2c1f6d0 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -11,7 +11,8 @@ "esModuleInterop": true, "moduleResolution": "Bundler", "resolveJsonModule": true, - "skipLibCheck": true + "skipLibCheck": true, + "allowJs": true }, "include": ["src", "test", "src/typings.d.ts", "jest.config.js"] } From 0614e453bd13f61b5bf0d468dc476c1cedb56ea9 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 10:10:45 +0200 Subject: [PATCH 16/45] Lock the version of `@swan-bitcoin/xpub-lib` lib For security reasons. In the future, we should probably fork it or copy that we need to resolve the addresses from `xpub`. --- pnpm-lock.yaml | 2 +- sdk/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2db3f9d37..3e3f65752 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,7 +145,7 @@ importers: specifier: 1.0.0-beta.9 version: 1.0.0-beta.9 '@swan-bitcoin/xpub-lib': - specifier: ^0.1.5 + specifier: 0.1.5 version: 0.1.5 ethers: specifier: ^6.10.0 diff --git a/sdk/package.json b/sdk/package.json index 808b8d711..d7fb7e32e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -30,7 +30,7 @@ "@keep-network/tbtc-v2.ts": "2.4.0-dev.3", "@ledgerhq/wallet-api-client": "^1.5.0", "@orangekit/sdk": "1.0.0-beta.9", - "@swan-bitcoin/xpub-lib": "^0.1.5", + "@swan-bitcoin/xpub-lib": "0.1.5", "ethers": "^6.10.0" } } From 6c2971c18f04598ffdefa65f8ac0d4d19424b1ce Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 10:16:06 +0200 Subject: [PATCH 17/45] Update constructo parameters names We should avoid prefixing constructor args with `_` as it may suggest that the argument should be ignored, but we use it. --- sdk/src/acre.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 313a1c791..7ea41d6e2 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -20,15 +20,15 @@ class Acre { public readonly staking: StakingModule constructor( - _contracts: AcreContracts, - _bitcoinProvider: BitcoinProvider, - _orangeKit: OrangeKitSdk, - _tbtc: Tbtc, + contracts: AcreContracts, + bitcoinProvider: BitcoinProvider, + orangeKit: OrangeKitSdk, + tbtc: Tbtc, ) { - this.contracts = _contracts - this.#tbtc = _tbtc - this.#orangeKit = _orangeKit - this.#bitcoinProvider = _bitcoinProvider + this.contracts = contracts + this.#tbtc = tbtc + this.#orangeKit = orangeKit + this.#bitcoinProvider = bitcoinProvider this.staking = new StakingModule( this.contracts, this.#bitcoinProvider, From a48e63b1de898e28b9489ccf2fa70c13f09a88b8 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 10:47:52 +0200 Subject: [PATCH 18/45] Update Acre SDK initialization fn Define two separate functions `initializeMainnet` and `initializeTestnet` so consumers won't pass the network param unnecessarily. --- sdk/src/acre.ts | 70 ++++++++++++++++++++++----------- sdk/src/lib/bitcoin/index.ts | 1 + sdk/src/lib/ethereum/network.ts | 6 +-- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 7ea41d6e2..7b7d96e55 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -1,11 +1,11 @@ import { OrangeKitSdk } from "@orangekit/sdk" -import { JsonRpcProvider } from "ethers" +import { getDefaultProvider } from "ethers" import { AcreContracts } from "./lib/contracts" import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" import { StakingModule } from "./modules/staking" import Tbtc from "./modules/tbtc" import { VoidSignerCompatibleWithEthersV5 } from "./lib/utils" -import { BitcoinProvider } from "./lib/bitcoin/providers" +import { BitcoinProvider, BitcoinNetwork } from "./lib/bitcoin" import { getChainIdByNetwork } from "./lib/ethereum/network" class Acre { @@ -37,14 +37,51 @@ class Acre { ) } - static async initializeEthereum( + static async initializeMainnet( bitcoinProvider: BitcoinProvider, - // TODO: change to Bitcoin network. - network: EthereumNetwork, tbtcApiUrl: string, - ): Promise { - const chainId = getChainIdByNetwork(network) - const orangeKit = await Acre.#getOrangeKitSDK(chainId) + evmRpcUrl: string, + ) { + return Acre.#initialize( + BitcoinNetwork.Mainnet, + bitcoinProvider, + tbtcApiUrl, + evmRpcUrl, + ) + } + + static async initializeTestnet( + bitcoinProvider: BitcoinProvider, + tbtcApiUrl: string, + evmRpcUrl: string, + ) { + return Acre.#initialize( + BitcoinNetwork.Testnet, + bitcoinProvider, + tbtcApiUrl, + evmRpcUrl, + ) + } + + static async #initialize( + network: BitcoinNetwork, + bitcoinProvider: BitcoinProvider, + tbtcApiUrl: string, + rpcUrl: string, + ) { + const evmNetwork: EthereumNetwork = + network === BitcoinNetwork.Testnet ? "sepolia" : "mainnet" + const evmChainId = getChainIdByNetwork(evmNetwork) + const provider = getDefaultProvider(rpcUrl) + + const providerChainId = (await provider.getNetwork()).chainId + if (evmChainId !== providerChainId) { + throw new Error( + `Invalid RPC node chain id. Provider chain id: ${providerChainId}; expected chain id: ${evmChainId}`, + ) + } + + const orangeKit = await OrangeKitSdk.init(Number(evmChainId), rpcUrl) // TODO: Should we store this address in context so that we do not to // recalculate it when necessary? @@ -52,35 +89,22 @@ class Acre { await bitcoinProvider.getAddress(), ) - // TODO: Should we hardcode the url on the Acre side or pass it as a config? - const provider = new JsonRpcProvider( - "https://eth-sepolia.g.alchemy.com/v2/", - ) - const signer = new VoidSignerCompatibleWithEthersV5( ethereumAddress, provider, ) - const contracts = getEthereumContracts(signer, network) + const contracts = getEthereumContracts(signer, evmNetwork) const tbtc = await Tbtc.initialize( signer, - network, + evmNetwork, tbtcApiUrl, contracts.bitcoinDepositor, ) return new Acre(contracts, bitcoinProvider, orangeKit, tbtc) } - - static #getOrangeKitSDK(chainId: number): Promise { - return OrangeKitSdk.init( - chainId, - // TODO: pass rpc url as config. - "https://eth-sepolia.g.alchemy.com/v2/", - ) - } } // eslint-disable-next-line import/prefer-default-export diff --git a/sdk/src/lib/bitcoin/index.ts b/sdk/src/lib/bitcoin/index.ts index b19a9cff0..5d11a3bc3 100644 --- a/sdk/src/lib/bitcoin/index.ts +++ b/sdk/src/lib/bitcoin/index.ts @@ -1,3 +1,4 @@ export * from "./transaction" export * from "./network" export * from "./address" +export * from "./providers" diff --git a/sdk/src/lib/ethereum/network.ts b/sdk/src/lib/ethereum/network.ts index 902e0e90a..20e36cade 100644 --- a/sdk/src/lib/ethereum/network.ts +++ b/sdk/src/lib/ethereum/network.ts @@ -1,8 +1,8 @@ export type EthereumNetwork = "mainnet" | "sepolia" -const NETWORK_TO_CHAIN_ID: Record = { - mainnet: 1, - sepolia: 11155111, +const NETWORK_TO_CHAIN_ID: Record = { + mainnet: 1n, + sepolia: 11155111n, } export function getChainIdByNetwork(network: EthereumNetwork) { From 769b1ba60e4ef8183b50a5604653799e6086d2bb Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:00:56 +0200 Subject: [PATCH 19/45] Simplify custom ethereum signer utils Create `VoidSigner` compatible with ethers v5. In the future we are going to use the ethers signer from `@orangekit/sdk` under the hood. This signer is used only interanlly by the Acre SDK to interact with Ethereum contracts. --- sdk/src/acre.ts | 7 +--- sdk/src/lib/utils/ethereum-signer.ts | 59 ++++++++-------------------- sdk/test/modules/tbtc/Tbtc.test.ts | 16 +++++--- 3 files changed, 29 insertions(+), 53 deletions(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 7b7d96e55..0523f69a9 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -4,7 +4,7 @@ import { AcreContracts } from "./lib/contracts" import { EthereumNetwork, getEthereumContracts } from "./lib/ethereum" import { StakingModule } from "./modules/staking" import Tbtc from "./modules/tbtc" -import { VoidSignerCompatibleWithEthersV5 } from "./lib/utils" +import { VoidSigner } from "./lib/utils" import { BitcoinProvider, BitcoinNetwork } from "./lib/bitcoin" import { getChainIdByNetwork } from "./lib/ethereum/network" @@ -89,10 +89,7 @@ class Acre { await bitcoinProvider.getAddress(), ) - const signer = new VoidSignerCompatibleWithEthersV5( - ethereumAddress, - provider, - ) + const signer = new VoidSigner(ethereumAddress, provider) const contracts = getEthereumContracts(signer, evmNetwork) diff --git a/sdk/src/lib/utils/ethereum-signer.ts b/sdk/src/lib/utils/ethereum-signer.ts index db186479d..f3288ba8e 100644 --- a/sdk/src/lib/utils/ethereum-signer.ts +++ b/sdk/src/lib/utils/ethereum-signer.ts @@ -1,15 +1,10 @@ -/* eslint-disable max-classes-per-file */ -import { VoidSigner, AbstractSigner } from "ethers" - -type AbstractSignerConstructor = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - abstract new (...args: any[]) => T +import { VoidSigner as EthersVoidSigner } from "ethers" /** * This abstract signer interface that defines necessary methods to be * compatible with ethers v5 signer which is used in tBTC-v2.ts SDK. */ -export interface IEthereumSignerCompatibleWithEthersV5 extends AbstractSigner { +export interface IEthereumSignerCompatibleWithEthersV5 { /** * @dev Required by ethers v5. */ @@ -24,45 +19,23 @@ export interface IEthereumSignerCompatibleWithEthersV5 extends AbstractSigner { getChainId(): Promise } -function ethereumSignerCompatibleWithEthersV5Mixin< - T extends AbstractSignerConstructor, ->(SignerBase: T) { - /** - * This abstract signer adds necessary methods to be compatible with ethers v5 - * signer which is used in tBTC-v2.ts SDK. - */ - abstract class EthereumSignerCompatibleWithEthersV5 - extends SignerBase - implements IEthereumSignerCompatibleWithEthersV5 - { - /** - * @dev Required by ethers v5. - */ - readonly _isSigner: boolean = true +export class VoidSigner + extends EthersVoidSigner + implements IEthereumSignerCompatibleWithEthersV5 +{ + readonly _isSigner: boolean = true - // eslint-disable-next-line no-underscore-dangle - _checkProvider() { - if (!this.provider) throw new Error("Provider not available") - } + // eslint-disable-next-line no-underscore-dangle + _checkProvider() { + if (!this.provider) throw new Error("Provider not available") + } - /** - * @dev Required by ethers v5. - */ - async getChainId(): Promise { - // eslint-disable-next-line no-underscore-dangle - this._checkProvider() + async getChainId(): Promise { + // eslint-disable-next-line no-underscore-dangle + this._checkProvider() - const network = await this.provider!.getNetwork() + const network = await this.provider!.getNetwork() - return Number(network.chainId) - } + return Number(network.chainId) } - - return EthereumSignerCompatibleWithEthersV5 } - -export const EthereumSignerCompatibleWithEthersV5 = - ethereumSignerCompatibleWithEthersV5Mixin(AbstractSigner) - -export const VoidSignerCompatibleWithEthersV5 = - ethereumSignerCompatibleWithEthersV5Mixin(VoidSigner) diff --git a/sdk/test/modules/tbtc/Tbtc.test.ts b/sdk/test/modules/tbtc/Tbtc.test.ts index b63055ac7..c9ee02f05 100644 --- a/sdk/test/modules/tbtc/Tbtc.test.ts +++ b/sdk/test/modules/tbtc/Tbtc.test.ts @@ -4,10 +4,7 @@ import { Deposit as TbtcSdkDeposit, } from "@keep-network/tbtc-v2.ts" -import { - EthereumSignerCompatibleWithEthersV5, - IEthereumSignerCompatibleWithEthersV5, -} from "../../../src" +import { IEthereumSignerCompatibleWithEthersV5 } from "../../../src" import Deposit from "../../../src/modules/tbtc/Deposit" import TbtcApi from "../../../src/lib/api/TbtcApi" @@ -20,13 +17,16 @@ import { import { MockAcreContracts } from "../../utils/mock-acre-contracts" import { MockTbtcSdk } from "../../utils/mock-tbtc-sdk" +import { getChainIdByNetwork } from "../../../src/lib/ethereum/network" jest.mock("@keep-network/tbtc-v2.ts", (): object => ({ TbtcSdk: jest.fn(), ...jest.requireActual("@keep-network/tbtc-v2.ts"), })) -class MockEthereumSignerCompatibleWithEthersV5 extends EthereumSignerCompatibleWithEthersV5 { +class MockEthereumSignerCompatibleWithEthersV5 + implements IEthereumSignerCompatibleWithEthersV5 +{ getAddress = jest.fn() connect = jest.fn() @@ -36,6 +36,12 @@ class MockEthereumSignerCompatibleWithEthersV5 extends EthereumSignerCompatibleW signMessage = jest.fn() signTypedData = jest.fn() + + _isSigner: boolean = true + + _checkProvider = jest.fn() + + getChainId = jest.fn().mockResolvedValue(getChainIdByNetwork("sepolia")) } describe("Tbtc", () => { From c6dfc244779e0a992d93afcfa131c0f3d1c63690 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:05:44 +0200 Subject: [PATCH 20/45] Avoid skipping `import/prefer-default-export` rule If there is one export we should use it as default. --- sdk/src/lib/bitcoin/providers/index.ts | 2 +- .../bitcoin/providers/ledger-live-wallet-api-provider.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/sdk/src/lib/bitcoin/providers/index.ts b/sdk/src/lib/bitcoin/providers/index.ts index 02f9f0244..a796e1cd8 100644 --- a/sdk/src/lib/bitcoin/providers/index.ts +++ b/sdk/src/lib/bitcoin/providers/index.ts @@ -1,2 +1,2 @@ export * from "./provider" -export * from "./ledger-live-wallet-api-provider" +export { default as LedgerLiveWalletApiBitcoinProvider } from "./ledger-live-wallet-api-provider" diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index 0417cc359..51cd16f9c 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -8,7 +8,9 @@ import { BitcoinNetwork } from "../network" type Network = Exclude -class LedgerLiveWalletApiBitcoinProvider implements BitcoinProvider { +export default class LedgerLiveWalletApiBitcoinProvider + implements BitcoinProvider +{ readonly #walletApiClient: WalletAPIClient readonly #windowMessageTransport: WindowMessageTransport @@ -65,6 +67,3 @@ class LedgerLiveWalletApiBitcoinProvider implements BitcoinProvider { return address } } - -// eslint-disable-next-line import/prefer-default-export -export { LedgerLiveWalletApiBitcoinProvider } From ce33d42e2976ebe8d75ce57d263bd8208c8285c6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:22:32 +0200 Subject: [PATCH 21/45] Add docs for bitcoin provider interfaces --- .../providers/ledger-live-wallet-api-provider.ts | 13 +++++++++++++ sdk/src/lib/bitcoin/providers/provider.ts | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index 51cd16f9c..ec6aff673 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -8,6 +8,9 @@ import { BitcoinNetwork } from "../network" type Network = Exclude +/** + * Ledger Live Wallet API Bitcoin Provider. + */ export default class LedgerLiveWalletApiBitcoinProvider implements BitcoinProvider { @@ -56,6 +59,16 @@ export default class LedgerLiveWalletApiBitcoinProvider this.#network = _network } + /** + * In the Ledger Live Wallet API the address is "renewed" each time funds are + * received in order to allow some privacy. But to get the same depositor + * owner Ethereum address we must always get the same Bitcoin address so we + * must rely on the extended public key. + * + * @returns Always the same bitcoin address based on the extended public key + * (an address under the `m/x'/0'/0'/0/0` derivation path) even the + * address has been "renewed" by the Ledger Live Wallet API. + */ async getAddress(): Promise { const xpub = await this.#walletApiClient.bitcoin.getXPub(this.#accountId) diff --git a/sdk/src/lib/bitcoin/providers/provider.ts b/sdk/src/lib/bitcoin/providers/provider.ts index 5467b79e6..97a3cbf1e 100644 --- a/sdk/src/lib/bitcoin/providers/provider.ts +++ b/sdk/src/lib/bitcoin/providers/provider.ts @@ -1,3 +1,15 @@ +/** + * Interface for communication with Bitcoin Wallet. + */ export interface BitcoinProvider { + /** + * Acre SDK must rely on the `BitcoinProvider` implementation to get the + * bitcoin address because different wallets use different strategies. For + * example in Ledger Live Wallet API the address is "renewed" each time funds + * are received in order to allow some privacy. In that case, always getting + * the same Bitcoin address should be hidden under a given implementation. + * + * @returns Bitcoin address selected by the user. + */ getAddress(): Promise } From 1a84644b3ffa8a095865218b9fbf6a0551213aa8 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:24:04 +0200 Subject: [PATCH 22/45] Update the `Network` type in Ledger Live Provider Since we plan to extract this file to a separate provider in the OrangeKit repo, let's define the network type without reference to the `BitcoinNetwork` type from `../network`. --- .../bitcoin/providers/ledger-live-wallet-api-provider.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index ec6aff673..a55e3fcbb 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -4,9 +4,8 @@ import { } from "@ledgerhq/wallet-api-client" import { addressFromExtPubKey } from "@swan-bitcoin/xpub-lib" import { BitcoinProvider } from "./provider" -import { BitcoinNetwork } from "../network" -type Network = Exclude +type Network = "mainnet" | "testnet" /** * Ledger Live Wallet API Bitcoin Provider. @@ -28,8 +27,7 @@ export default class LedgerLiveWalletApiBitcoinProvider const walletApiClient = new WalletAPIClient(windowMessageTransport) - const currency = - network === BitcoinNetwork.Mainnet ? "bitcoin" : "bitcoin_testnet" + const currency = network === "mainnet" ? "bitcoin" : "bitcoin_testnet" const accounts = await walletApiClient.account.list({ currencyIds: [currency], From 14f572d611ce41f4f52978f1354bec7cabf97ed3 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:36:13 +0200 Subject: [PATCH 23/45] Update `@swan-bitcoin/xpub-lib` declaration types Set all the propertires expected by the `addressFromExtPubKey` function. --- sdk/src/typings.d.ts | 48 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/sdk/src/typings.d.ts b/sdk/src/typings.d.ts index 07b4b64e5..184f5c6eb 100644 --- a/sdk/src/typings.d.ts +++ b/sdk/src/typings.d.ts @@ -1,8 +1,48 @@ -type Network = "mainnet" | "testnet" - declare module "@swan-bitcoin/xpub-lib" { + /** + * Derivation purpose as defined in BIP 44. + */ + enum Purpose { + /** + * BIP 44: Pay To Pubkey Hash (addresses starting with 1). + */ + P2PKH = "44", // 1... + /** + * BIP 49: Pay To Witness Pubkey Hash nested in Pay To Script Hash (addresses + * starting with 3). + */ + P2SH = "49", // 3... + /** + * BIP 84: Pay To Witness Pubkey Hash (addresses starting with bc1). + */ + P2WPKH = "84", // bc1... + } + const addressFromExtPubKey: (xpubData: { + /** + * Extended Public Key. + */ extPubKey: string - network: Network - }) => { address: string } + + /** + * Change (0 = external chain, 1 = internal chain / change). Default `0`. + */ + change?: number + + /** + * The unhardened key index. Default `0`. + */ + keyIndex?: number + + /** + * The derivation purpose. The `P2WPKH` is used as default. + */ + purpose?: Purpose + + /** + * The target network (testnet or mainnet). If no network is specified the + * library defaults to testnet. + */ + network?: "mainnet" | "testnet" + }) => { address: string; path: string } } From 32ebcefe4120e14402a968cae960481c4cbfeb2b Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:40:20 +0200 Subject: [PATCH 24/45] Rename variable `depositOwnerChainAddress` -> `depositOwnerEvmAddress`. --- sdk/src/modules/staking/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index b5e25b754..1313d1ba0 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -75,7 +75,7 @@ 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 depositOwnerChainAddress = EthereumAddress.from( + const depositOwnerEvmAddress = EthereumAddress.from( await this.#orangeKit.predictAddress(depositorOwnerBitcoinAddress), ) @@ -85,7 +85,7 @@ class StakingModule { bitcoinRecoveryAddress ?? depositorOwnerBitcoinAddress const tbtcDeposit = await this.#tbtc.initiateDeposit( - depositOwnerChainAddress, + depositOwnerEvmAddress, finalBitcoinRecoveryAddress, referral, ) @@ -94,7 +94,7 @@ class StakingModule { this.#contracts, this.#bitcoinProvider, finalBitcoinRecoveryAddress, - depositOwnerChainAddress, + depositOwnerEvmAddress, tbtcDeposit, ) } From 8bdb89504a7083a304065b536304ab340889b5b6 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 11:56:18 +0200 Subject: [PATCH 25/45] Fix linting errors --- dapp/src/acre-react/hooks/useStakeFlow.ts | 2 +- dapp/src/components/Layout.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dapp/src/acre-react/hooks/useStakeFlow.ts b/dapp/src/acre-react/hooks/useStakeFlow.ts index d806240b7..b7fd25f04 100644 --- a/dapp/src/acre-react/hooks/useStakeFlow.ts +++ b/dapp/src/acre-react/hooks/useStakeFlow.ts @@ -45,7 +45,7 @@ export function useStakeFlow(): UseStakeFlowReturn { const signMessage = useCallback(async () => { if (!stakeFlow) throw new Error("Initialize stake first") - await stakeFlow.signMessage() + await Promise.resolve(stakeFlow.signMessage()) }, [stakeFlow]) const stake = useCallback(async () => { diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index 08c738b2b..ef7dfda71 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -1,6 +1,5 @@ -import React from "react" +import React, { useState } from "react" import { AnimatePresence, motion, Variants } from "framer-motion" -import { useState } from "react" import { useLocation, useOutlet } from "react-router-dom" import DocsDrawer from "./DocsDrawer" import Header from "./Header" From 855dc38e4f890aba0789584deeb00858b2298720 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 16:57:40 +0200 Subject: [PATCH 26/45] Update the docs in Ledger Live Bitcoin Provider --- .../lib/bitcoin/providers/ledger-live-wallet-api-provider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index a55e3fcbb..de26afcf9 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -64,8 +64,9 @@ export default class LedgerLiveWalletApiBitcoinProvider * must rely on the extended public key. * * @returns Always the same bitcoin address based on the extended public key - * (an address under the `m/x'/0'/0'/0/0` derivation path) even the - * address has been "renewed" by the Ledger Live Wallet API. + * (an address under the `m/purpose'/0'/accountId'/0/0` derivation + * path) even the address has been "renewed" by the Ledger Live + * Wallet API. */ async getAddress(): Promise { const xpub = await this.#walletApiClient.bitcoin.getXPub(this.#accountId) From 3df3cf766e1c5e32e380ce97da5a322e53b62bd9 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 14 May 2024 16:59:37 +0200 Subject: [PATCH 27/45] Update `getAddress` fn Provide values for optional properties so we are clear about what we expect to be set for the derivation. --- .../lib/bitcoin/providers/ledger-live-wallet-api-provider.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index de26afcf9..2a75abe09 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -2,7 +2,7 @@ import { WalletAPIClient, WindowMessageTransport, } from "@ledgerhq/wallet-api-client" -import { addressFromExtPubKey } from "@swan-bitcoin/xpub-lib" +import { addressFromExtPubKey, Purpose } from "@swan-bitcoin/xpub-lib" import { BitcoinProvider } from "./provider" type Network = "mainnet" | "testnet" @@ -73,6 +73,9 @@ export default class LedgerLiveWalletApiBitcoinProvider const { address } = addressFromExtPubKey({ extPubKey: xpub, + change: 0, + keyIndex: 0, + purpose: Purpose.P2PKH, network: this.#network, }) From 097cac229babcd5c5d23191298301d85a6b57e5e Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 10:16:39 +0200 Subject: [PATCH 28/45] Update `getAddress` fn in Ledger Live provider Set `purpose` property based on the type of the account user selected. By default the `addressFromExtPubKey` function uses `P2WPKH`. --- .../ledger-live-wallet-api-provider.ts | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index 2a75abe09..b5bcaa26d 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -1,8 +1,10 @@ import { + Account, WalletAPIClient, WindowMessageTransport, } from "@ledgerhq/wallet-api-client" import { addressFromExtPubKey, Purpose } from "@swan-bitcoin/xpub-lib" +import { BitcoinAddressHelper } from "@orangekit/sdk" import { BitcoinProvider } from "./provider" type Network = "mainnet" | "testnet" @@ -17,7 +19,7 @@ export default class LedgerLiveWalletApiBitcoinProvider readonly #windowMessageTransport: WindowMessageTransport - readonly #accountId: string + readonly #account: Account readonly #network: Network @@ -38,7 +40,7 @@ export default class LedgerLiveWalletApiBitcoinProvider if (!account) throw new Error("Account not found") return new LedgerLiveWalletApiBitcoinProvider( - accountId, + account, windowMessageTransport, walletApiClient, network, @@ -46,15 +48,15 @@ export default class LedgerLiveWalletApiBitcoinProvider } private constructor( - _accountId: string, - _windowMessageTransport: WindowMessageTransport, - _walletApiClient: WalletAPIClient, - _network: Network, + account: Account, + windowMessageTransport: WindowMessageTransport, + walletApiClient: WalletAPIClient, + network: Network, ) { - this.#windowMessageTransport = _windowMessageTransport - this.#walletApiClient = _walletApiClient - this.#accountId = _accountId - this.#network = _network + this.#windowMessageTransport = windowMessageTransport + this.#walletApiClient = walletApiClient + this.#account = account + this.#network = network } /** @@ -69,13 +71,24 @@ export default class LedgerLiveWalletApiBitcoinProvider * Wallet API. */ async getAddress(): Promise { - const xpub = await this.#walletApiClient.bitcoin.getXPub(this.#accountId) + const bitcoinAddress = this.#account.address + let purpose: Purpose | undefined + + if (BitcoinAddressHelper.isP2PKHAddress(bitcoinAddress)) { + purpose = Purpose.P2PKH + } else if (BitcoinAddressHelper.isP2WPKHAddress(bitcoinAddress)) { + purpose = Purpose.P2WPKH + } else { + throw new Error("Unsupported Bitcoin address type") + } + + const xpub = await this.#walletApiClient.bitcoin.getXPub(this.#account.id) const { address } = addressFromExtPubKey({ extPubKey: xpub, change: 0, keyIndex: 0, - purpose: Purpose.P2PKH, + purpose, network: this.#network, }) From 196b94f5d461badb474444b7c249636c08713f21 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 10:21:41 +0200 Subject: [PATCH 29/45] Leave TODO in `LedgerLiveWalletApiBitcoinProvider` Leave TODO for unused variable. Currently this variable is not used but we should probably close the connection once the operation is finished. We will handle it in a separate PR. --- .../lib/bitcoin/providers/ledger-live-wallet-api-provider.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts index b5bcaa26d..4d0e1bc00 100644 --- a/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts +++ b/sdk/src/lib/bitcoin/providers/ledger-live-wallet-api-provider.ts @@ -17,6 +17,9 @@ export default class LedgerLiveWalletApiBitcoinProvider { readonly #walletApiClient: WalletAPIClient + // TODO: Currently this variable is not used but we should probably close the + // connection once the operation is finished. We will handle it in a separate + // PR. readonly #windowMessageTransport: WindowMessageTransport readonly #account: Account From 46ed33f08ac847a16486ec657f61cb8f8e837dc2 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 24 Apr 2024 16:21:07 +0200 Subject: [PATCH 30/45] Remove Ethereum account from the wallet context We do not need to connect Ethereum account with the dapp since we want to operate only on the Bitcoin wallet. The Acre SDK will generate the Ethereum address based on the Bitcoin address under the hood and the user's identifier should always be bitcoin address and the SDK will take a care of converting it to Ethereum address under the hood. Still, some features in the Acre SDK expect the Ethereum address eg. to get the estimated bitcoin balance. Hence as a temporary solution we set the ethereum account to zero address to not break the dapp. In the future we want to completely get rid of the ethereum account from the dapp wallet context and we will only pass the bitcoin address to the Acre SDK. --- .../acre-react/contexts/AcreSdkContext.tsx | 1 + .../TransactionModal/ActionFormModal.tsx | 9 +++---- .../ActiveStakingStep/DepositBTCModal.tsx | 2 +- .../TransactionModal/ModalContentWrapper.tsx | 15 ++--------- dapp/src/contexts/StakeFlowContext.tsx | 9 +++---- dapp/src/contexts/WalletContext.tsx | 6 ++--- dapp/src/hooks/index.ts | 1 - dapp/src/hooks/sdk/useFetchBTCBalance.ts | 2 +- dapp/src/hooks/sdk/useInitializeAcreSdk.ts | 7 ++--- dapp/src/hooks/toasts/useInitGlobalToasts.ts | 1 - .../hooks/toasts/useShowWalletErrorToast.ts | 8 ++---- dapp/src/hooks/useRequestEthereumAccount.ts | 26 ------------------- dapp/src/hooks/useWallet.ts | 19 +++++++++----- dapp/src/types/toast.ts | 2 -- 14 files changed, 34 insertions(+), 74 deletions(-) delete mode 100644 dapp/src/hooks/useRequestEthereumAccount.ts diff --git a/dapp/src/acre-react/contexts/AcreSdkContext.tsx b/dapp/src/acre-react/contexts/AcreSdkContext.tsx index 75dd77a60..6949d8a8e 100644 --- a/dapp/src/acre-react/contexts/AcreSdkContext.tsx +++ b/dapp/src/acre-react/contexts/AcreSdkContext.tsx @@ -20,6 +20,7 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) { const [acre, setAcre] = useState(undefined) const [isInitialized, setIsInitialized] = useState(false) + // TODO: initialize Acre SDK w/o Ethereum address. const init = useCallback( async (ethereumAddress: string, network: EthereumNetwork) => { if (!ethereumAddress) throw new Error("Ethereum address not defined") diff --git a/dapp/src/components/TransactionModal/ActionFormModal.tsx b/dapp/src/components/TransactionModal/ActionFormModal.tsx index 0f7c824c9..c509f2fd1 100644 --- a/dapp/src/components/TransactionModal/ActionFormModal.tsx +++ b/dapp/src/components/TransactionModal/ActionFormModal.tsx @@ -23,7 +23,7 @@ import UnstakeFormModal from "./ActiveUnstakingStep/UnstakeFormModal" const TABS = Object.values(ACTION_FLOW_TYPES) function ActionFormModal({ defaultType }: { defaultType: ActionFlowType }) { - const { btcAccount, ethAccount } = useWalletContext() + const { btcAccount } = useWalletContext() const { type, setType } = useModalFlowContext() const { setTokenAmount } = useTransactionContext() const { initStake } = useStakeFlowContext() @@ -32,12 +32,11 @@ function ActionFormModal({ defaultType }: { defaultType: ActionFlowType }) { const handleInitStake = useCallback(async () => { const btcAddress = btcAccount?.address - const ethAddress = ethAccount?.address - if (btcAddress && ethAddress) { - await initStake(btcAddress, ethAddress) + if (btcAddress) { + await initStake(btcAddress) } - }, [btcAccount?.address, ethAccount?.address, initStake]) + }, [btcAccount?.address, initStake]) const handleSubmitForm = useCallback( async (values: TokenAmountFormValues) => { diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index 4b68afa54..a5b9a0bff 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -75,7 +75,7 @@ export default function DepositBTCModal() { const response = await depositTelemetry( depositReceipt, btcAddress, - ethAccount.address, + ethAccount, ) setIsLoading(false) diff --git a/dapp/src/components/TransactionModal/ModalContentWrapper.tsx b/dapp/src/components/TransactionModal/ModalContentWrapper.tsx index 33c94bdbe..7fe4973d1 100644 --- a/dapp/src/components/TransactionModal/ModalContentWrapper.tsx +++ b/dapp/src/components/TransactionModal/ModalContentWrapper.tsx @@ -2,11 +2,10 @@ import React from "react" import { useModalFlowContext, useRequestBitcoinAccount, - useRequestEthereumAccount, useTransactionContext, useWalletContext, } from "#/hooks" -import { BitcoinIcon, EthereumIcon } from "#/assets/icons" +import { BitcoinIcon } from "#/assets/icons" import { ActionFlowType, PROCESS_STATUSES } from "#/types" import { isSupportedBTCAddressType } from "#/utils" import ActionFormModal from "./ActionFormModal" @@ -23,9 +22,8 @@ export default function ModalContentWrapper({ defaultType: ActionFlowType children: React.ReactNode }) { - const { btcAccount, ethAccount } = useWalletContext() + const { btcAccount } = useWalletContext() const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() - const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() const { type, status, onClose, onResume } = useModalFlowContext() const { tokenAmount } = useTransactionContext() @@ -38,15 +36,6 @@ export default function ModalContentWrapper({ /> ) - if (!ethAccount) - return ( - - ) - if (!tokenAmount) return if (status === PROCESS_STATUSES.PAUSED) diff --git a/dapp/src/contexts/StakeFlowContext.tsx b/dapp/src/contexts/StakeFlowContext.tsx index adadd561c..18938b86b 100644 --- a/dapp/src/contexts/StakeFlowContext.tsx +++ b/dapp/src/contexts/StakeFlowContext.tsx @@ -7,10 +7,7 @@ import { import { REFERRAL } from "#/constants" type StakeFlowContextValue = Omit & { - initStake: ( - bitcoinRecoveryAddress: string, - ethereumAddress: string, - ) => Promise + initStake: (bitcoinAddress: string) => Promise } export const StakeFlowContext = React.createContext({ @@ -30,10 +27,10 @@ export function StakeFlowProvider({ children }: { children: React.ReactNode }) { } = useStakeFlow() const initStake = useCallback( - async (bitcoinRecoveryAddress: string, ethereumAddress: string) => { + async (bitcoinAddress: string) => { if (!acre) throw new Error("Acre SDK not defined") - await acreInitStake(bitcoinRecoveryAddress, ethereumAddress, REFERRAL) + await acreInitStake(bitcoinAddress, REFERRAL) }, [acreInitStake, acre], ) diff --git a/dapp/src/contexts/WalletContext.tsx b/dapp/src/contexts/WalletContext.tsx index 82cb00ffd..5d8cca3a3 100644 --- a/dapp/src/contexts/WalletContext.tsx +++ b/dapp/src/contexts/WalletContext.tsx @@ -4,8 +4,8 @@ import React, { createContext, useEffect, useMemo, useState } from "react" type WalletContextValue = { btcAccount: Account | undefined setBtcAccount: React.Dispatch> - ethAccount: Account | undefined - setEthAccount: React.Dispatch> + ethAccount: string | undefined + setEthAccount: React.Dispatch> isConnected: boolean } @@ -23,7 +23,7 @@ export function WalletContextProvider({ children: React.ReactNode }): React.ReactElement { const [btcAccount, setBtcAccount] = useState(undefined) - const [ethAccount, setEthAccount] = useState(undefined) + const [ethAccount, setEthAccount] = useState(undefined) const [isConnected, setIsConnected] = useState(false) useEffect(() => { diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 5524c6f4b..abcb24f8e 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -4,7 +4,6 @@ export * from "./sdk" export * from "./subgraph" export * from "./useDetectThemeMode" export * from "./useRequestBitcoinAccount" -export * from "./useRequestEthereumAccount" export * from "./useWalletContext" export * from "./useSidebar" export * from "./useDocsDrawer" diff --git a/dapp/src/hooks/sdk/useFetchBTCBalance.ts b/dapp/src/hooks/sdk/useFetchBTCBalance.ts index ab73285d3..284ecaf67 100644 --- a/dapp/src/hooks/sdk/useFetchBTCBalance.ts +++ b/dapp/src/hooks/sdk/useFetchBTCBalance.ts @@ -15,7 +15,7 @@ export function useFetchBTCBalance() { const getBtcBalance = async () => { if (!isInitialized || !ethAccount || !acre) return - const chainIdentifier = EthereumAddress.from(ethAccount.address) + const chainIdentifier = EthereumAddress.from(ethAccount) const sharesBalance = await acre.staking.sharesBalance(chainIdentifier) const estimatedBitcoinBalance = await acre.staking.estimatedBitcoinBalance(chainIdentifier) diff --git a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts index 56af453e1..79c04a4c7 100644 --- a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts +++ b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts @@ -9,11 +9,12 @@ export function useInitializeAcreSdk() { const { init } = useAcreContext() useEffect(() => { - if (!ethAccount?.address) return + // TODO: Init Acre SDK w/o Ethereum account. + if (!ethAccount) return const initSDK = async (ethAddress: string) => { await init(ethAddress, ETHEREUM_NETWORK) } - logPromiseFailure(initSDK(ethAccount.address)) - }, [ethAccount?.address, init]) + logPromiseFailure(initSDK(ethAccount)) + }, [ethAccount, init]) } diff --git a/dapp/src/hooks/toasts/useInitGlobalToasts.ts b/dapp/src/hooks/toasts/useInitGlobalToasts.ts index 0fec6ef12..fd2bcdbcf 100644 --- a/dapp/src/hooks/toasts/useInitGlobalToasts.ts +++ b/dapp/src/hooks/toasts/useInitGlobalToasts.ts @@ -1,6 +1,5 @@ import { useShowWalletErrorToast } from "./useShowWalletErrorToast" export function useInitGlobalToasts() { - useShowWalletErrorToast("ethereum") useShowWalletErrorToast("bitcoin") } diff --git a/dapp/src/hooks/toasts/useShowWalletErrorToast.ts b/dapp/src/hooks/toasts/useShowWalletErrorToast.ts index df57b308c..0ed8130c8 100644 --- a/dapp/src/hooks/toasts/useShowWalletErrorToast.ts +++ b/dapp/src/hooks/toasts/useShowWalletErrorToast.ts @@ -6,21 +6,17 @@ import { useToast } from "./useToast" import { useWallet } from "../useWallet" import { useTimeout } from "../useTimeout" -const { BITCOIN_WALLET_ERROR, ETHEREUM_WALLET_ERROR } = TOAST_IDS +const { BITCOIN_WALLET_ERROR } = TOAST_IDS const WALLET_ERROR_TOAST_ID = { bitcoin: { id: BITCOIN_WALLET_ERROR, Component: TOASTS[BITCOIN_WALLET_ERROR], }, - ethereum: { - id: ETHEREUM_WALLET_ERROR, - Component: TOASTS[ETHEREUM_WALLET_ERROR], - }, } export function useShowWalletErrorToast( - type: "bitcoin" | "ethereum", + type: "bitcoin", delay = ONE_SEC_IN_MILLISECONDS, ) { const { diff --git a/dapp/src/hooks/useRequestEthereumAccount.ts b/dapp/src/hooks/useRequestEthereumAccount.ts deleted file mode 100644 index 1fa5c1f16..000000000 --- a/dapp/src/hooks/useRequestEthereumAccount.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useRequestAccount } from "@ledgerhq/wallet-api-client-react" -import { useCallback, useContext, useEffect } from "react" -import { WalletContext } from "#/contexts" -import { UseRequestAccountReturn } from "#/types" -import { CURRENCY_ID_ETHEREUM } from "#/constants" -import { useWalletApiReactTransport } from "./useWalletApiReactTransport" - -export function useRequestEthereumAccount(): UseRequestAccountReturn { - const { setEthAccount } = useContext(WalletContext) - const { account, requestAccount } = useRequestAccount() - const { walletApiReactTransport } = useWalletApiReactTransport() - - useEffect(() => { - if (account) { - setEthAccount(account) - } - }, [account, setEthAccount]) - - const requestEthereumAccount = useCallback(async () => { - walletApiReactTransport.connect() - await requestAccount({ currencyIds: [CURRENCY_ID_ETHEREUM] }) - walletApiReactTransport.disconnect() - }, [requestAccount, walletApiReactTransport]) - - return { requestAccount: requestEthereumAccount } -} diff --git a/dapp/src/hooks/useWallet.ts b/dapp/src/hooks/useWallet.ts index 7511494d6..f97cec66d 100644 --- a/dapp/src/hooks/useWallet.ts +++ b/dapp/src/hooks/useWallet.ts @@ -1,18 +1,25 @@ import { useMemo } from "react" +import { ZeroAddress } from "ethers" import { useWalletContext } from "./useWalletContext" import { useRequestBitcoinAccount } from "./useRequestBitcoinAccount" -import { useRequestEthereumAccount } from "./useRequestEthereumAccount" export function useWallet() { - const { btcAccount, ethAccount } = useWalletContext() + const { btcAccount, ethAccount, setEthAccount } = useWalletContext() const { requestAccount: requestBitcoinAccount } = useRequestBitcoinAccount() - const { requestAccount: requestEthereumAccount } = useRequestEthereumAccount() return useMemo( () => ({ - bitcoin: { account: btcAccount, requestAccount: requestBitcoinAccount }, - ethereum: { account: ethAccount, requestAccount: requestEthereumAccount }, + bitcoin: { + account: btcAccount, + requestAccount: async () => { + await requestBitcoinAccount() + // TODO: Temporary solution - we do not need the eth account and we + // want to create the Acre SDK w/o passing the Ethereum Account. + setEthAccount(ZeroAddress) + }, + }, + ethereum: { account: ethAccount }, }), - [btcAccount, requestBitcoinAccount, ethAccount, requestEthereumAccount], + [btcAccount, requestBitcoinAccount, ethAccount, setEthAccount], ) } diff --git a/dapp/src/types/toast.ts b/dapp/src/types/toast.ts index c4a3fe962..7bb564427 100644 --- a/dapp/src/types/toast.ts +++ b/dapp/src/types/toast.ts @@ -7,7 +7,6 @@ import { export const TOAST_IDS = { BITCOIN_WALLET_ERROR: "bitcoin-wallet-error", - ETHEREUM_WALLET_ERROR: "ethereum-wallet-error", SIGNING_ERROR: "signing-error", DEPOSIT_TRANSACTION_ERROR: "deposit-transaction-error", } as const @@ -17,7 +16,6 @@ export type ToastID = (typeof TOAST_IDS)[keyof typeof TOAST_IDS] // eslint-disable-next-line @typescript-eslint/no-explicit-any export const TOASTS: Record ReactNode> = { [TOAST_IDS.BITCOIN_WALLET_ERROR]: WalletErrorToast, - [TOAST_IDS.ETHEREUM_WALLET_ERROR]: WalletErrorToast, [TOAST_IDS.SIGNING_ERROR]: SigningMessageErrorToast, [TOAST_IDS.DEPOSIT_TRANSACTION_ERROR]: DepositTransactionErrorToast, } From 315b54521b1d94bd1f1e3b1fd8668084e3634f4d Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 24 Apr 2024 17:11:47 +0200 Subject: [PATCH 31/45] Initialize the Acre SDK w/o Ethereum address --- .../acre-react/contexts/AcreSdkContext.tsx | 8 ++--- dapp/src/hooks/sdk/useInitializeAcreSdk.ts | 15 +++++---- dapp/src/web3/ledger-live-signer.ts | 33 ++++++++++++------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/dapp/src/acre-react/contexts/AcreSdkContext.tsx b/dapp/src/acre-react/contexts/AcreSdkContext.tsx index 6949d8a8e..a05a9de9f 100644 --- a/dapp/src/acre-react/contexts/AcreSdkContext.tsx +++ b/dapp/src/acre-react/contexts/AcreSdkContext.tsx @@ -6,7 +6,7 @@ const TBTC_API_ENDPOINT = import.meta.env.VITE_TBTC_API_ENDPOINT type AcreSdkContextValue = { acre?: Acre - init: (ethereumAddress: string, network: EthereumNetwork) => Promise + init: (bitcoinAddress: string, network: EthereumNetwork) => Promise isInitialized: boolean } @@ -22,11 +22,11 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) { // TODO: initialize Acre SDK w/o Ethereum address. const init = useCallback( - async (ethereumAddress: string, network: EthereumNetwork) => { - if (!ethereumAddress) throw new Error("Ethereum address not defined") + async (bitcoinAddress: string, network: EthereumNetwork) => { + if (!bitcoinAddress) throw new Error("Bitcoin address not defined") const sdk = await Acre.initializeEthereum( - await LedgerLiveEthereumSigner.fromAddress(ethereumAddress), + await LedgerLiveEthereumSigner.fromAddress(bitcoinAddress), network, TBTC_API_ENDPOINT, ) diff --git a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts index 79c04a4c7..3e97ebf4d 100644 --- a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts +++ b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts @@ -5,16 +5,17 @@ import { useAcreContext } from "#/acre-react/hooks" import { useWalletContext } from "../useWalletContext" export function useInitializeAcreSdk() { - const { ethAccount } = useWalletContext() + const { btcAccount } = useWalletContext() const { init } = useAcreContext() + const bitcoinAddress = btcAccount?.address useEffect(() => { - // TODO: Init Acre SDK w/o Ethereum account. - if (!ethAccount) return + if (!bitcoinAddress) return - const initSDK = async (ethAddress: string) => { - await init(ethAddress, ETHEREUM_NETWORK) + const initSDK = async (_bitcoinAddress: string) => { + await init(_bitcoinAddress, ETHEREUM_NETWORK) } - logPromiseFailure(initSDK(ethAccount)) - }, [ethAccount, init]) + + logPromiseFailure(initSDK(bitcoinAddress)) + }, [bitcoinAddress, init]) } diff --git a/dapp/src/web3/ledger-live-signer.ts b/dapp/src/web3/ledger-live-signer.ts index 7bdd658c6..95812617e 100644 --- a/dapp/src/web3/ledger-live-signer.ts +++ b/dapp/src/web3/ledger-live-signer.ts @@ -12,7 +12,7 @@ import { TypedDataField, TransactionResponse, } from "ethers" -import { CURRENCY_ID_ETHEREUM } from "#/constants" +import { CURRENCY_ID_BITCOIN } from "#/constants" import { EthereumSignerCompatibleWithEthersV5 } from "@acre-btc/sdk" import { getLedgerWalletAPITransport as getDappLedgerWalletAPITransport, @@ -28,10 +28,10 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { readonly #client: WalletAPIClient - readonly #account: Account + readonly #bitcoinAccount: Account static async fromAddress( - address: string, + bitcoinAddress: string, getLedgerWalletAPITransport: () => WindowMessageTransport = getDappLedgerWalletAPITransport, ) { const dappTransport = getLedgerWalletAPITransport() @@ -41,14 +41,14 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { const client = new WalletAPIClient(dappTransport) const accountsList = await client.account.list({ - currencyIds: [CURRENCY_ID_ETHEREUM], + currencyIds: [CURRENCY_ID_BITCOIN], }) dappTransport.disconnect() - const account = accountsList.find((acc) => acc.address === address) + const account = accountsList.find((acc) => acc.address === bitcoinAddress) - if (!account) throw new Error("Account not found") + if (!account) throw new Error("Bitcoin Account not found") return new LedgerLiveEthereumSigner( dappTransport, @@ -65,13 +65,19 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { provider: Provider | null, ) { super(provider) - this.#account = account + this.#bitcoinAccount = account this.#transport = transport this.#client = walletApiClient } + // eslint-disable-next-line class-methods-use-this getAddress(): Promise { - return Promise.resolve(this.#account.address) + // TODO: We should return the Ethereum address created based on the Bitcoin + // address. We probably will use the Signer from OrangeKit once it is + // implemented. For now, the `Signer.getAddress` is not used anywhere in the + // Acre SDK, only during the tBTC-v2.ts SDK initialization, so we can set + // random ethereum account. + return Promise.resolve("0x7b570B83D53e0671271DCa2CDf3429E9C4CAb12E") } async #clientRequest(callback: () => Promise) { @@ -87,7 +93,7 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { return new LedgerLiveEthereumSigner( this.#transport, this.#client, - this.#account, + this.#bitcoinAccount, provider, ) } @@ -97,7 +103,10 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { serializeLedgerWalletApiEthereumTransaction(transaction) const buffer = await this.#clientRequest(() => - this.#client.transaction.sign(this.#account.id, ethereumTransaction), + this.#client.transaction.sign( + this.#bitcoinAccount.id, + ethereumTransaction, + ), ) return buffer.toString() @@ -111,7 +120,7 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { const transactionHash = await this.#clientRequest(() => this.#client.transaction.signAndBroadcast( - this.#account.id, + this.#bitcoinAccount.id, ethereumTransaction, ), ) @@ -129,7 +138,7 @@ class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { async signMessage(message: string | Uint8Array): Promise { const buffer = await this.#clientRequest(() => this.#client.message.sign( - this.#account.id, + this.#bitcoinAccount.id, Buffer.from(message.toString()), ), ) From bb9147779b8e13eda184b3c32ff8883de10f844d Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 8 May 2024 22:07:26 +0200 Subject: [PATCH 32/45] Init the Acre SDK in dapp w/o ethereum address Create the Bitcoin Provider based on the selected bitcoin account id providede by the Ledger Live Wallet API. --- dapp/ledger-manifest-development.json | 3 ++- .../acre-react/contexts/AcreSdkContext.tsx | 14 +++++++------- dapp/src/acre-react/hooks/useStakeFlow.ts | 9 ++++++--- dapp/src/constants/chains.ts | 14 +++++++++++--- dapp/src/contexts/StakeFlowContext.tsx | 13 +++++-------- dapp/src/hooks/sdk/useInitializeAcreSdk.ts | 19 +++++++++++-------- 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/dapp/ledger-manifest-development.json b/dapp/ledger-manifest-development.json index a986387d0..1a938120c 100644 --- a/dapp/ledger-manifest-development.json +++ b/dapp/ledger-manifest-development.json @@ -23,7 +23,8 @@ "account.list", "message.sign", "transaction.sign", - "transaction.signAndBroadcast" + "transaction.signAndBroadcast", + "bitcoin.getXPub" ], "domains": ["http://*"], "type": "walletApp" diff --git a/dapp/src/acre-react/contexts/AcreSdkContext.tsx b/dapp/src/acre-react/contexts/AcreSdkContext.tsx index a05a9de9f..2cff3d675 100644 --- a/dapp/src/acre-react/contexts/AcreSdkContext.tsx +++ b/dapp/src/acre-react/contexts/AcreSdkContext.tsx @@ -1,12 +1,15 @@ import React, { useCallback, useMemo, useState } from "react" -import { LedgerLiveEthereumSigner } from "#/web3" import { Acre, EthereumNetwork } from "@acre-btc/sdk" +import { BitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers" const TBTC_API_ENDPOINT = import.meta.env.VITE_TBTC_API_ENDPOINT type AcreSdkContextValue = { acre?: Acre - init: (bitcoinAddress: string, network: EthereumNetwork) => Promise + init: ( + bitcoinProvider: BitcoinProvider, + network: EthereumNetwork, + ) => Promise isInitialized: boolean } @@ -20,13 +23,10 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) { const [acre, setAcre] = useState(undefined) const [isInitialized, setIsInitialized] = useState(false) - // TODO: initialize Acre SDK w/o Ethereum address. const init = useCallback( - async (bitcoinAddress: string, network: EthereumNetwork) => { - if (!bitcoinAddress) throw new Error("Bitcoin address not defined") - + async (bitcoinProvider: BitcoinProvider, network: EthereumNetwork) => { const sdk = await Acre.initializeEthereum( - await LedgerLiveEthereumSigner.fromAddress(bitcoinAddress), + bitcoinProvider, network, TBTC_API_ENDPOINT, ) diff --git a/dapp/src/acre-react/hooks/useStakeFlow.ts b/dapp/src/acre-react/hooks/useStakeFlow.ts index b7fd25f04..87a4fd0f8 100644 --- a/dapp/src/acre-react/hooks/useStakeFlow.ts +++ b/dapp/src/acre-react/hooks/useStakeFlow.ts @@ -3,7 +3,10 @@ import { StakeInitialization, DepositReceipt } from "@acre-btc/sdk" import { useAcreContext } from "./useAcreContext" export type UseStakeFlowReturn = { - initStake: (bitcoinAddress: string, referral: number) => Promise + initStake: ( + referral: number, + bitcoinRecoveryAddress?: string, + ) => Promise btcAddress?: string depositReceipt?: DepositReceipt signMessage: () => Promise @@ -22,12 +25,12 @@ export function useStakeFlow(): UseStakeFlowReturn { >(undefined) const initStake = useCallback( - async (bitcoinAddress: string, referral: number) => { + async (referral: number, bitcoinRecoveryAddress?: string) => { if (!acre || !isInitialized) throw new Error("Acre SDK not defined") const initializedStakeFlow = await acre.staking.initializeStake( - bitcoinAddress, referral, + bitcoinRecoveryAddress, ) const btcDepositAddress = await initializedStakeFlow.getBitcoinAddress() diff --git a/dapp/src/constants/chains.ts b/dapp/src/constants/chains.ts index 150d7575f..2da4a4a90 100644 --- a/dapp/src/constants/chains.ts +++ b/dapp/src/constants/chains.ts @@ -1,5 +1,13 @@ import { Chain } from "#/types" -import { EthereumNetwork, BitcoinNetwork } from "@acre-btc/sdk" +import { + EthereumNetwork, + BitcoinNetwork as AcreSDKBitcoinNetwork, +} from "@acre-btc/sdk" + +export type BitcoinNetwork = Exclude< + AcreSDKBitcoinNetwork, + AcreSDKBitcoinNetwork.Unknown +> export const BLOCK_EXPLORER: Record = { ethereum: { title: "Etherscan", url: "https://etherscan.io" }, @@ -11,5 +19,5 @@ export const ETHEREUM_NETWORK: EthereumNetwork = export const BITCOIN_NETWORK: BitcoinNetwork = import.meta.env.VITE_USE_TESTNET === "true" - ? BitcoinNetwork.Testnet - : BitcoinNetwork.Mainnet + ? AcreSDKBitcoinNetwork.Testnet + : AcreSDKBitcoinNetwork.Mainnet diff --git a/dapp/src/contexts/StakeFlowContext.tsx b/dapp/src/contexts/StakeFlowContext.tsx index 18938b86b..8e08b0366 100644 --- a/dapp/src/contexts/StakeFlowContext.tsx +++ b/dapp/src/contexts/StakeFlowContext.tsx @@ -7,7 +7,7 @@ import { import { REFERRAL } from "#/constants" type StakeFlowContextValue = Omit & { - initStake: (bitcoinAddress: string) => Promise + initStake: () => Promise } export const StakeFlowContext = React.createContext({ @@ -26,14 +26,11 @@ export function StakeFlowProvider({ children }: { children: React.ReactNode }) { stake, } = useStakeFlow() - const initStake = useCallback( - async (bitcoinAddress: string) => { - if (!acre) throw new Error("Acre SDK not defined") + const initStake = useCallback(async () => { + if (!acre) throw new Error("Acre SDK not defined") - await acreInitStake(bitcoinAddress, REFERRAL) - }, - [acreInitStake, acre], - ) + await acreInitStake(REFERRAL) + }, [acreInitStake, acre]) const context = useMemo( () => ({ diff --git a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts index 3e97ebf4d..02c1b9ab8 100644 --- a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts +++ b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts @@ -1,21 +1,24 @@ import { useEffect } from "react" -import { ETHEREUM_NETWORK } from "#/constants" +import { BITCOIN_NETWORK, ETHEREUM_NETWORK } from "#/constants" import { logPromiseFailure } from "#/utils" import { useAcreContext } from "#/acre-react/hooks" +import { LedgerLiveWalletApiBitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers" import { useWalletContext } from "../useWalletContext" export function useInitializeAcreSdk() { const { btcAccount } = useWalletContext() const { init } = useAcreContext() - const bitcoinAddress = btcAccount?.address useEffect(() => { - if (!bitcoinAddress) return + if (!btcAccount?.id) return - const initSDK = async (_bitcoinAddress: string) => { - await init(_bitcoinAddress, ETHEREUM_NETWORK) + const initSDK = async (bitcoinAccountId: string) => { + const bitcoinProvider = await LedgerLiveWalletApiBitcoinProvider.init( + bitcoinAccountId, + BITCOIN_NETWORK, + ) + await init(bitcoinProvider, ETHEREUM_NETWORK) } - - logPromiseFailure(initSDK(bitcoinAddress)) - }, [bitcoinAddress, init]) + logPromiseFailure(initSDK(btcAccount.id)) + }, [btcAccount?.id, init]) } From 9fb193cc3d68882a37630a824ab0dbd219aad5cc Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 10:12:45 +0200 Subject: [PATCH 33/45] Update Acre SDK initialization in dapp Use `initializeMainnet` or `initializeTestnet` fn to init the Acre SDK. --- dapp/.env | 3 ++ .../acre-react/contexts/AcreSdkContext.tsx | 32 ++++++++++++------- dapp/src/vite-env.d.ts | 1 + 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/dapp/.env b/dapp/.env index 7a39c66c5..6fa402df3 100644 --- a/dapp/.env +++ b/dapp/.env @@ -14,3 +14,6 @@ VITE_REFERRAL=123 VITE_DEFENDER_RELAYER_WEBHOOK_URL="https://api.defender.openzeppelin.com/actions/a0d6d2e2-ce9c-4619-aa2b-6c874fe97af7/runs/webhook/b1f17c89-8230-46e3-866f-a3213887974c/Sbddsy54cJ6sPg2bLPyuHJ" VITE_ACRE_SUBGRAPH_URL="https://api.studio.thegraph.com/query/73600/acre/version/latest" + +# TODO: Set this env variable in CI. +VITE_TBTC_API_ENDPOINT="" diff --git a/dapp/src/acre-react/contexts/AcreSdkContext.tsx b/dapp/src/acre-react/contexts/AcreSdkContext.tsx index 2cff3d675..0d2eb16b4 100644 --- a/dapp/src/acre-react/contexts/AcreSdkContext.tsx +++ b/dapp/src/acre-react/contexts/AcreSdkContext.tsx @@ -1,15 +1,14 @@ import React, { useCallback, useMemo, useState } from "react" -import { Acre, EthereumNetwork } from "@acre-btc/sdk" +import { Acre, BitcoinNetwork } from "@acre-btc/sdk" import { BitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers" +import { BITCOIN_NETWORK } from "#/constants" const TBTC_API_ENDPOINT = import.meta.env.VITE_TBTC_API_ENDPOINT +const ETH_RPC_URL = import.meta.env.VITE_ETH_HOSTNAME_HTTP type AcreSdkContextValue = { acre?: Acre - init: ( - bitcoinProvider: BitcoinProvider, - network: EthereumNetwork, - ) => Promise + init: (bitcoinProvider: BitcoinProvider) => Promise isInitialized: boolean } @@ -24,12 +23,23 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) { const [isInitialized, setIsInitialized] = useState(false) const init = useCallback( - async (bitcoinProvider: BitcoinProvider, network: EthereumNetwork) => { - const sdk = await Acre.initializeEthereum( - bitcoinProvider, - network, - TBTC_API_ENDPOINT, - ) + async (bitcoinProvider: BitcoinProvider) => { + let sdk: Acre + + if (BITCOIN_NETWORK === BitcoinNetwork.Mainnet) { + sdk = await Acre.initializeMainnet( + bitcoinProvider, + TBTC_API_ENDPOINT, + ETH_RPC_URL, + ) + } else { + sdk = await Acre.initializeTestnet( + bitcoinProvider, + TBTC_API_ENDPOINT, + ETH_RPC_URL, + ) + } + setAcre(sdk) setIsInitialized(true) }, diff --git a/dapp/src/vite-env.d.ts b/dapp/src/vite-env.d.ts index 477206fe4..efe1127b6 100644 --- a/dapp/src/vite-env.d.ts +++ b/dapp/src/vite-env.d.ts @@ -6,6 +6,7 @@ interface ImportMetaEnv { readonly VITE_REFERRAL: number readonly VITE_TBTC_API_ENDPOINT: string readonly VITE_ACRE_SUBGRAPH_URL: string + readonly VITE_TBTC_API_ENDPOINT: string } interface ImportMeta { From 3cdb91ed239537294a492d9b01a9c1c360b9a928 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 10:15:03 +0200 Subject: [PATCH 34/45] Remove unused ethers signer for Ledger Live Now the Acre SDK requires the bitcoin provider not ethereum singer. --- dapp/src/web3/index.ts | 1 - dapp/src/web3/ledger-live-signer.ts | 165 ---------------------------- dapp/src/web3/utils/index.ts | 1 - dapp/src/web3/utils/ledger-live.ts | 56 ---------- 4 files changed, 223 deletions(-) delete mode 100644 dapp/src/web3/index.ts delete mode 100644 dapp/src/web3/ledger-live-signer.ts delete mode 100644 dapp/src/web3/utils/index.ts delete mode 100644 dapp/src/web3/utils/ledger-live.ts diff --git a/dapp/src/web3/index.ts b/dapp/src/web3/index.ts deleted file mode 100644 index f38e7e840..000000000 --- a/dapp/src/web3/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ledger-live-signer" diff --git a/dapp/src/web3/ledger-live-signer.ts b/dapp/src/web3/ledger-live-signer.ts deleted file mode 100644 index 95812617e..000000000 --- a/dapp/src/web3/ledger-live-signer.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { - Account, - WalletAPIClient, - WindowMessageTransport, -} from "@ledgerhq/wallet-api-client" -import { - TypedDataEncoder, - Provider, - Signer, - TransactionRequest, - TypedDataDomain, - TypedDataField, - TransactionResponse, -} from "ethers" -import { CURRENCY_ID_BITCOIN } from "#/constants" -import { EthereumSignerCompatibleWithEthersV5 } from "@acre-btc/sdk" -import { - getLedgerWalletAPITransport as getDappLedgerWalletAPITransport, - getLedgerLiveProvider, - serializeLedgerWalletApiEthereumTransaction, -} from "./utils" - -// Created based on the -// https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/utils/ledger.ts -// but with support for ethers v6. -class LedgerLiveEthereumSigner extends EthereumSignerCompatibleWithEthersV5 { - readonly #transport: WindowMessageTransport - - readonly #client: WalletAPIClient - - readonly #bitcoinAccount: Account - - static async fromAddress( - bitcoinAddress: string, - getLedgerWalletAPITransport: () => WindowMessageTransport = getDappLedgerWalletAPITransport, - ) { - const dappTransport = getLedgerWalletAPITransport() - - dappTransport.connect() - - const client = new WalletAPIClient(dappTransport) - - const accountsList = await client.account.list({ - currencyIds: [CURRENCY_ID_BITCOIN], - }) - - dappTransport.disconnect() - - const account = accountsList.find((acc) => acc.address === bitcoinAddress) - - if (!account) throw new Error("Bitcoin Account not found") - - return new LedgerLiveEthereumSigner( - dappTransport, - client, - account, - getLedgerLiveProvider(), - ) - } - - private constructor( - transport: WindowMessageTransport, - walletApiClient: WalletAPIClient, - account: Account, - provider: Provider | null, - ) { - super(provider) - this.#bitcoinAccount = account - this.#transport = transport - this.#client = walletApiClient - } - - // eslint-disable-next-line class-methods-use-this - getAddress(): Promise { - // TODO: We should return the Ethereum address created based on the Bitcoin - // address. We probably will use the Signer from OrangeKit once it is - // implemented. For now, the `Signer.getAddress` is not used anywhere in the - // Acre SDK, only during the tBTC-v2.ts SDK initialization, so we can set - // random ethereum account. - return Promise.resolve("0x7b570B83D53e0671271DCa2CDf3429E9C4CAb12E") - } - - async #clientRequest(callback: () => Promise) { - try { - this.#transport.connect() - return await callback() - } finally { - this.#transport.disconnect() - } - } - - connect(provider: Provider | null): Signer { - return new LedgerLiveEthereumSigner( - this.#transport, - this.#client, - this.#bitcoinAccount, - provider, - ) - } - - async signTransaction(transaction: TransactionRequest): Promise { - const ethereumTransaction = - serializeLedgerWalletApiEthereumTransaction(transaction) - - const buffer = await this.#clientRequest(() => - this.#client.transaction.sign( - this.#bitcoinAccount.id, - ethereumTransaction, - ), - ) - - return buffer.toString() - } - - async sendTransaction(tx: TransactionRequest): Promise { - const populatedTransaction = await this.populateTransaction(tx) - - const ethereumTransaction = - serializeLedgerWalletApiEthereumTransaction(populatedTransaction) - - const transactionHash = await this.#clientRequest(() => - this.#client.transaction.signAndBroadcast( - this.#bitcoinAccount.id, - ethereumTransaction, - ), - ) - - const transactionResponse = - await this.provider?.getTransaction(transactionHash) - - if (!transactionResponse) { - throw new Error("Transaction response not found!") - } - - return transactionResponse - } - - async signMessage(message: string | Uint8Array): Promise { - const buffer = await this.#clientRequest(() => - this.#client.message.sign( - this.#bitcoinAccount.id, - Buffer.from(message.toString()), - ), - ) - - return buffer.toString("hex") - } - - async signTypedData( - domain: TypedDataDomain, - types: Record, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: Record, - ): Promise { - const payload = TypedDataEncoder.getPayload( - domain, - types, - value, - ) as unknown as object - - return this.signMessage(JSON.stringify(payload)) - } -} - -export { LedgerLiveEthereumSigner } diff --git a/dapp/src/web3/utils/index.ts b/dapp/src/web3/utils/index.ts deleted file mode 100644 index 222c65f9b..000000000 --- a/dapp/src/web3/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ledger-live" diff --git a/dapp/src/web3/utils/ledger-live.ts b/dapp/src/web3/utils/ledger-live.ts deleted file mode 100644 index 5ee8122f7..000000000 --- a/dapp/src/web3/utils/ledger-live.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - EthereumTransaction, - WindowMessageTransport, -} from "@ledgerhq/wallet-api-client" -import { TransactionRequest, ZeroAddress, JsonRpcProvider } from "ethers" -import { Hex } from "@acre-btc/sdk" - -export const getLedgerWalletAPITransport = () => new WindowMessageTransport() - -export const getLedgerLiveProvider = () => - new JsonRpcProvider(import.meta.env.VITE_ETH_HOSTNAME_HTTP) - -// Created based on the -// https://github.com/keep-network/tbtc-v2/blob/main/typescript/src/lib/utils/ledger.ts. -export function serializeLedgerWalletApiEthereumTransaction( - transaction: TransactionRequest, -): EthereumTransaction { - const { - value, - to, - nonce, - data, - gasPrice, - gasLimit, - maxFeePerGas, - maxPriorityFeePerGas, - } = transaction - - const ethereumTransaction: EthereumTransaction = { - family: "ethereum" as const, - // @ts-expect-error We do not want to install external bignumber.js lib so - // here we use bigint. The Ledger Wallet Api just converts the bignumber.js - // object to string so we can pass bigint. See: - // https://github.com/LedgerHQ/wallet-api/blob/main/packages/core/src/families/ethereum/serializer.ts#L4 - amount: value ?? 0, - recipient: to?.toString() || ZeroAddress, - nonce: nonce ?? undefined, - // @ts-expect-error See comment above. - gasPrice: gasPrice ?? undefined, - // @ts-expect-error See comment above. - gasLimit: gasLimit ?? undefined, - // @ts-expect-error See comment above. - maxFeePerGas: maxFeePerGas ?? undefined, - // @ts-expect-error See comment above. - maxPriorityFeePerGas: maxPriorityFeePerGas ?? undefined, - } - - if (nonce) ethereumTransaction.nonce = Number(nonce) - if (data) - ethereumTransaction.data = Buffer.from( - Hex.from(data.toString()).toString(), - "hex", - ) - - return ethereumTransaction -} From 6499c9abf15e09236e5cf2db358cf77f849b8ef9 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 15:00:56 +0200 Subject: [PATCH 35/45] Mock `@orangekit/sdk` to run jest test Reverts 553b5be6ab75587dcfc3a17c00ee25b3e19ae4dc commit. We can just mock the `@orangekit/sdk` module globally to run jest tests correctly w/o converting the JS files to CommonJS syntax. --- sdk/jest.config.ts | 3 +-- sdk/test/__mocks__/@orangekit/sdk.ts | 1 + sdk/tsconfig.json | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 sdk/test/__mocks__/@orangekit/sdk.ts diff --git a/sdk/jest.config.ts b/sdk/jest.config.ts index 04e040912..d15006424 100644 --- a/sdk/jest.config.ts +++ b/sdk/jest.config.ts @@ -1,9 +1,8 @@ import type { JestConfigWithTsJest } from "ts-jest" const jestConfig: JestConfigWithTsJest = { - preset: "ts-jest/presets/js-with-ts", + preset: "ts-jest", testPathIgnorePatterns: ["/dist/", "/node_modules/"], - transformIgnorePatterns: ["/node_modules/(?!@orangekit/sdk)/"], } export default jestConfig diff --git a/sdk/test/__mocks__/@orangekit/sdk.ts b/sdk/test/__mocks__/@orangekit/sdk.ts new file mode 100644 index 000000000..bc7bd20f4 --- /dev/null +++ b/sdk/test/__mocks__/@orangekit/sdk.ts @@ -0,0 +1 @@ +jest.mock("@orangekit/sdk") diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index df2c1f6d0..0ac0507f9 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -11,8 +11,6 @@ "esModuleInterop": true, "moduleResolution": "Bundler", "resolveJsonModule": true, - "skipLibCheck": true, - "allowJs": true }, "include": ["src", "test", "src/typings.d.ts", "jest.config.js"] } From 4f05fe8f28d76f6a80ba906159665572edd47f3c Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 15:08:16 +0200 Subject: [PATCH 36/45] Fix formatting error --- sdk/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index 0ac0507f9..04757398b 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -10,7 +10,7 @@ "rootDirs": ["src", "test"], "esModuleInterop": true, "moduleResolution": "Bundler", - "resolveJsonModule": true, + "resolveJsonModule": true }, "include": ["src", "test", "src/typings.d.ts", "jest.config.js"] } From 30526c7471f0615691f68d2a63aefd0f7937c240 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 15 May 2024 18:20:59 +0200 Subject: [PATCH 37/45] Fix `useInitializeAcreSdk` hook Remove unnecessary `ETHEREUM_NETWORK` param. The ethereum network is no longer needed to create the Acre SDK. --- dapp/src/hooks/sdk/useInitializeAcreSdk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts index 02c1b9ab8..1c1f86b63 100644 --- a/dapp/src/hooks/sdk/useInitializeAcreSdk.ts +++ b/dapp/src/hooks/sdk/useInitializeAcreSdk.ts @@ -1,5 +1,5 @@ import { useEffect } from "react" -import { BITCOIN_NETWORK, ETHEREUM_NETWORK } from "#/constants" +import { BITCOIN_NETWORK } from "#/constants" import { logPromiseFailure } from "#/utils" import { useAcreContext } from "#/acre-react/hooks" import { LedgerLiveWalletApiBitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers" @@ -17,7 +17,7 @@ export function useInitializeAcreSdk() { bitcoinAccountId, BITCOIN_NETWORK, ) - await init(bitcoinProvider, ETHEREUM_NETWORK) + await init(bitcoinProvider) } logPromiseFailure(initSDK(btcAccount.id)) }, [btcAccount?.id, init]) From f351fa42f6301afa7ef5751e31b937c8041dd5ab Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 16 May 2024 19:08:21 +0200 Subject: [PATCH 38/45] Rename variable `ethereumAddress` -> `depositOwnerEvmAddress` --- sdk/src/acre.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 0523f69a9..0d1057fab 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -85,11 +85,11 @@ class Acre { // TODO: Should we store this address in context so that we do not to // recalculate it when necessary? - const ethereumAddress = await orangeKit.predictAddress( + const depositOwnerEvmAddress = await orangeKit.predictAddress( await bitcoinProvider.getAddress(), ) - const signer = new VoidSigner(ethereumAddress, provider) + const signer = new VoidSigner(depositOwnerEvmAddress, provider) const contracts = getEthereumContracts(signer, evmNetwork) From a51688eb5e7f68b933db7109e08276332b4627c8 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 16 May 2024 19:10:44 +0200 Subject: [PATCH 39/45] Update docs Fix docs for the `bitcoinProvider` property. --- sdk/src/modules/staking/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index 1313d1ba0..1ecead708 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -27,7 +27,7 @@ class StakingModule { readonly #contracts: AcreContracts /** - * Typed structured data signer. + * Bitcoin provider to communicate with the wallet. */ readonly #bitcoinProvider: BitcoinProvider From 56aa212549bda16743329c9d882b3d9deb91bc38 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Thu, 16 May 2024 20:05:01 +0200 Subject: [PATCH 40/45] Update `IEthereumSignerCompatibleWithEthersV5` Extends the `Signer` interface from ethers v6 to include more methods required by ethers v5. --- sdk/src/lib/utils/ethereum-signer.ts | 4 ++-- sdk/test/modules/tbtc/Tbtc.test.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/sdk/src/lib/utils/ethereum-signer.ts b/sdk/src/lib/utils/ethereum-signer.ts index f3288ba8e..4c81e4063 100644 --- a/sdk/src/lib/utils/ethereum-signer.ts +++ b/sdk/src/lib/utils/ethereum-signer.ts @@ -1,10 +1,10 @@ -import { VoidSigner as EthersVoidSigner } from "ethers" +import { VoidSigner as EthersVoidSigner, Signer } from "ethers" /** * This abstract signer interface that defines necessary methods to be * compatible with ethers v5 signer which is used in tBTC-v2.ts SDK. */ -export interface IEthereumSignerCompatibleWithEthersV5 { +export interface IEthereumSignerCompatibleWithEthersV5 extends Signer { /** * @dev Required by ethers v5. */ diff --git a/sdk/test/modules/tbtc/Tbtc.test.ts b/sdk/test/modules/tbtc/Tbtc.test.ts index c9ee02f05..5af2f3eca 100644 --- a/sdk/test/modules/tbtc/Tbtc.test.ts +++ b/sdk/test/modules/tbtc/Tbtc.test.ts @@ -4,7 +4,8 @@ import { Deposit as TbtcSdkDeposit, } from "@keep-network/tbtc-v2.ts" -import { IEthereumSignerCompatibleWithEthersV5 } from "../../../src" +import { ZeroAddress, Provider } from "ethers" +import { IEthereumSignerCompatibleWithEthersV5, VoidSigner } from "../../../src" import Deposit from "../../../src/modules/tbtc/Deposit" import TbtcApi from "../../../src/lib/api/TbtcApi" @@ -24,9 +25,11 @@ jest.mock("@keep-network/tbtc-v2.ts", (): object => ({ ...jest.requireActual("@keep-network/tbtc-v2.ts"), })) -class MockEthereumSignerCompatibleWithEthersV5 - implements IEthereumSignerCompatibleWithEthersV5 -{ +class MockEthereumSignerCompatibleWithEthersV5 extends VoidSigner { + constructor() { + super(ZeroAddress, {} as Provider) + } + getAddress = jest.fn() connect = jest.fn() From c6228a7fcc16264e6122c12567c2ecac2b97cfc8 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Tue, 21 May 2024 16:51:30 +0200 Subject: [PATCH 41/45] Add unit tests for Ledger Live Wallet Api Provider --- .../ledger-live-wallet-api-provider.test.ts | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 sdk/test/lib/bitcoin/providers/ledger-live-wallet-api-provider.test.ts diff --git a/sdk/test/lib/bitcoin/providers/ledger-live-wallet-api-provider.test.ts b/sdk/test/lib/bitcoin/providers/ledger-live-wallet-api-provider.test.ts new file mode 100644 index 000000000..cd55bc6e2 --- /dev/null +++ b/sdk/test/lib/bitcoin/providers/ledger-live-wallet-api-provider.test.ts @@ -0,0 +1,249 @@ +import ledgerLib, { + Account, + WalletAPIClient, + WindowMessageTransport, +} from "@ledgerhq/wallet-api-client" +import xpubLib, { Purpose } from "@swan-bitcoin/xpub-lib" +import { BitcoinAddressHelper } from "@orangekit/sdk" +import { LedgerLiveWalletApiBitcoinProvider } from "../../../../src" + +jest.mock("@ledgerhq/wallet-api-client", () => ({ + WalletAPIClient: jest.fn(), + WindowMessageTransport: jest.fn(), +})) + +jest.mock("@orangekit/sdk", () => ({ + BitcoinAddressHelper: { + isP2PKHAddress: jest + .fn() + .mockImplementation( + (address: string) => + address.startsWith("1") || + address.startsWith("m") || + address.startsWith("n"), + ), + isP2WPKHAddress: jest + .fn() + .mockImplementation( + (address: string) => + (address.startsWith("tb1") || address.startsWith("bc1")) && + address.length !== 62, + ), + }, +})) + +jest.mock("@swan-bitcoin/xpub-lib", (): object => ({ + addressFromExtPubKey: jest.fn(), + ...jest.requireActual("@swan-bitcoin/xpub-lib"), +})) + +describe("Ledger Live Wallet API Bitcoin provider", () => { + const mockTransport = { connect: jest.fn() } + const mockWalletApiClient = { + account: { + list: jest.fn(), + }, + bitcoin: { + getXPub: jest.fn(), + }, + } + + describe("init", () => { + const accountId = "123" + + beforeAll(() => { + mockWalletApiClient.account.list.mockReturnValue([{ id: accountId }]) + + jest + .spyOn(ledgerLib, "WalletAPIClient") + // @ts-expect-error we only mock the functions we use in the code. + .mockReturnValue(mockWalletApiClient) + + jest + .spyOn(ledgerLib, "WindowMessageTransport") + // @ts-expect-error we only mock the functions we use in the code. + .mockReturnValue(mockTransport) + }) + + describe("when account exists", () => { + describe.each<{ network: "mainnet" | "testnet"; currencyIds: string[] }>([ + { network: "mainnet", currencyIds: ["bitcoin"] }, + { network: "testnet", currencyIds: ["bitcoin_testnet"] }, + ])("when the network is $network", ({ network, currencyIds }) => { + let result: LedgerLiveWalletApiBitcoinProvider + + beforeAll(async () => { + result = await LedgerLiveWalletApiBitcoinProvider.init( + accountId, + network, + ) + }) + + it("should connect with message transport", () => { + expect(WindowMessageTransport).toHaveBeenCalled() + expect(mockTransport.connect).toHaveBeenCalled() + }) + + it("should create the wallet api object", () => { + expect(WalletAPIClient).toHaveBeenCalledWith(mockTransport) + }) + + it("should check if the given account exists in wallet", () => { + expect(mockWalletApiClient.account.list).toHaveBeenCalledWith({ + currencyIds, + }) + }) + + it("should initialize the provider correctly", () => { + expect(result).toBeDefined() + expect(result.getAddress).toBeDefined() + }) + }) + }) + + describe("when the account does not exist", () => { + it("should throw an error", async () => { + await expect(LedgerLiveWalletApiBitcoinProvider.init).rejects.toThrow( + "Account not found", + ) + }) + }) + }) + + describe("getAddress", () => { + describe.each<{ + xpub: string + addressType: "P2PKH" | "P2WPKH" + // The "renewed" address used to receive funds. + freshAddress: string + expectedPurpose: Purpose + expectedAddress: string + }>([ + { + xpub: "tpubDDJ2EkVNzDKvfq4pX7rHuJLzQE2m3xqewzkRhWQE1TEnjRUERdn3tvEkDHPc5bfjZWt9pPY3T6R7jeM1BugLdJZVkqTaAhRJ7C5nCpidvgY", + addressType: "P2PKH", + expectedPurpose: Purpose.P2PKH, + expectedAddress: "mnuQubqM5W4FRXvnfc2LB4Vw1dQLVNB8sa", + freshAddress: "n25cBRVKcAvdTK3Ho88LbT9bsQti5mR4ay", + }, + { + xpub: "tpubDDaFptfbeW9PaJvCwjcqt8GNwFe19a4ZQ6N8KRJV7tXz7pkaXxCnpswBDBooAe3dwGj4TwecmrFfsbC8dwJmTZAihJ2ci8mxSnH4jr9NknA", + addressType: "P2WPKH", + expectedPurpose: Purpose.P2WPKH, + expectedAddress: "tb1q3erffggwvqlcnt0yh8say9xnm6ya0jdvtw98f2", + freshAddress: "tb1qf3t77hmw4f0zl407r7jk7ykaphdzk4m6a2xvml", + }, + ])( + "when it is $addressType address type", + ({ + xpub, + expectedAddress, + expectedPurpose, + freshAddress, + addressType, + }) => { + const network = "testnet" + const spyOnAddressFromExtPubKey = jest.spyOn( + xpubLib, + "addressFromExtPubKey", + ) + let account: Account + let provider: LedgerLiveWalletApiBitcoinProvider + + let result: string + + beforeEach(async () => { + account = { id: "123", address: freshAddress } as Account + mockWalletApiClient.account.list.mockResolvedValue([account]) + mockWalletApiClient.bitcoin.getXPub.mockResolvedValue(xpub) + + provider = await LedgerLiveWalletApiBitcoinProvider.init( + account.id, + network, + ) + + jest + .spyOn(ledgerLib, "WalletAPIClient") + // @ts-expect-error we only mock the functions we use in the code. + .mockReturnValue(mockWalletApiClient) + + jest + .spyOn(ledgerLib, "WindowMessageTransport") + // @ts-expect-error we only mock the functions we use in the code. + .mockReturnValue(mockTransport) + + result = await provider.getAddress() + }) + + it("should check if it is supported address type", () => { + expect(BitcoinAddressHelper.isP2PKHAddress).toHaveBeenCalledWith( + account.address, + ) + + if (addressType !== "P2PKH") { + expect(BitcoinAddressHelper.isP2WPKHAddress).toHaveBeenCalledWith( + account.address, + ) + } + }) + + it("should get the xpub for a given account", () => { + expect(mockWalletApiClient.bitcoin.getXPub).toHaveBeenCalledWith( + account.id, + ) + }) + + it("should get the address from extended public key", () => { + expect(spyOnAddressFromExtPubKey).toHaveBeenCalledWith({ + extPubKey: xpub, + change: 0, + keyIndex: 0, + purpose: expectedPurpose, + network, + }) + + expect(result).toBe(expectedAddress) + }) + }, + ) + + describe("when it is unsupported address type", () => { + let provider: LedgerLiveWalletApiBitcoinProvider + + beforeEach(async () => { + const account = { + id: "123", + address: + "tb1p00vwwtpuucdengeyyzsrcvf70e8ln98e5c8m3kf3cz9nzldvq3qqrl7kqn", + } as Account + const network = "testnet" + const xpub = + "tpubDDDTd2KnT6BEcagK3ib46tYfh8FCZ7aXxkuj84j9GQnuiyMfsXw5CJ5NiRov8pf81DeSpcwTXeoNsYYxwYEdRdriKZrCeXF7JQrgbTp71PM" + + mockWalletApiClient.account.list.mockResolvedValue([account]) + mockWalletApiClient.bitcoin.getXPub.mockResolvedValue(xpub) + + provider = await LedgerLiveWalletApiBitcoinProvider.init( + account.id, + network, + ) + + jest + .spyOn(ledgerLib, "WalletAPIClient") + // @ts-expect-error we only mock the functions we use in the code. + .mockReturnValue(mockWalletApiClient) + + jest + .spyOn(ledgerLib, "WindowMessageTransport") + // @ts-expect-error we only mock the functions we use in the code. + .mockReturnValue(mockTransport) + }) + + it("should throw an error", async () => { + await expect(provider.getAddress()).rejects.toThrow( + "Unsupported Bitcoin address type", + ) + }) + }) + }) +}) From 37db599df7035e411e177e5a5731d4c527e0405f Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 29 May 2024 13:17:25 +0200 Subject: [PATCH 42/45] Simplify the bitcoin network type in SDK Instead of exporting the BitcoinNetwork we import from tBTC SDK we define it with the networks we support - mainnet and testnet. --- dapp/src/constants/chains.ts | 14 +++----------- sdk/src/lib/bitcoin/index.ts | 2 +- sdk/src/lib/bitcoin/network.ts | 8 ++++++-- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/dapp/src/constants/chains.ts b/dapp/src/constants/chains.ts index 1964bd55a..43d408d31 100644 --- a/dapp/src/constants/chains.ts +++ b/dapp/src/constants/chains.ts @@ -1,13 +1,5 @@ import { Chain } from "#/types" -import { - EthereumNetwork, - BitcoinNetwork as AcreSDKBitcoinNetwork, -} from "@acre-btc/sdk" - -export type BitcoinNetwork = Exclude< - AcreSDKBitcoinNetwork, - AcreSDKBitcoinNetwork.Unknown -> +import { EthereumNetwork, BitcoinNetwork } from "@acre-btc/sdk" const BLOCK_EXPLORER_TESTNET = { ethereum: { title: "Etherscan", url: "https://sepolia.etherscan.io" }, @@ -29,5 +21,5 @@ export const ETHEREUM_NETWORK: EthereumNetwork = export const BITCOIN_NETWORK: BitcoinNetwork = import.meta.env.VITE_USE_TESTNET === "true" - ? AcreSDKBitcoinNetwork.Testnet - : AcreSDKBitcoinNetwork.Mainnet + ? BitcoinNetwork.Testnet + : BitcoinNetwork.Mainnet diff --git a/sdk/src/lib/bitcoin/index.ts b/sdk/src/lib/bitcoin/index.ts index 5d11a3bc3..4008f4590 100644 --- a/sdk/src/lib/bitcoin/index.ts +++ b/sdk/src/lib/bitcoin/index.ts @@ -1,4 +1,4 @@ export * from "./transaction" -export * from "./network" +export { default as BitcoinNetwork } from "./network" export * from "./address" export * from "./providers" diff --git a/sdk/src/lib/bitcoin/network.ts b/sdk/src/lib/bitcoin/network.ts index bfd491bbb..89ebdd239 100644 --- a/sdk/src/lib/bitcoin/network.ts +++ b/sdk/src/lib/bitcoin/network.ts @@ -1,2 +1,6 @@ -// eslint-disable-next-line import/prefer-default-export -export { BitcoinNetwork } from "@keep-network/tbtc-v2.ts" +enum BitcoinNetwork { + Mainnet = "mainnet", + Testnet = "testnet", +} + +export default BitcoinNetwork From 60a2bf5e9fc843673eb5943a11652d97bebb81ad Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 29 May 2024 13:21:18 +0200 Subject: [PATCH 43/45] Simplify the the Acre initialization function Remove the `initializeMainnet` and `initializeTestnet` static functions and keep only one function `initialize` and the consumer must pass bitcoin network to which it wants to connect. This simplifies the initialization in the dapp. --- .../acre-react/contexts/AcreSdkContext.tsx | 23 ++++-------- sdk/src/acre.ts | 36 +++---------------- 2 files changed, 12 insertions(+), 47 deletions(-) diff --git a/dapp/src/acre-react/contexts/AcreSdkContext.tsx b/dapp/src/acre-react/contexts/AcreSdkContext.tsx index 0d2eb16b4..5873e3e65 100644 --- a/dapp/src/acre-react/contexts/AcreSdkContext.tsx +++ b/dapp/src/acre-react/contexts/AcreSdkContext.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useMemo, useState } from "react" -import { Acre, BitcoinNetwork } from "@acre-btc/sdk" +import { Acre } from "@acre-btc/sdk" import { BitcoinProvider } from "@acre-btc/sdk/dist/src/lib/bitcoin/providers" import { BITCOIN_NETWORK } from "#/constants" @@ -24,21 +24,12 @@ export function AcreSdkProvider({ children }: { children: React.ReactNode }) { const init = useCallback( async (bitcoinProvider: BitcoinProvider) => { - let sdk: Acre - - if (BITCOIN_NETWORK === BitcoinNetwork.Mainnet) { - sdk = await Acre.initializeMainnet( - bitcoinProvider, - TBTC_API_ENDPOINT, - ETH_RPC_URL, - ) - } else { - sdk = await Acre.initializeTestnet( - bitcoinProvider, - TBTC_API_ENDPOINT, - ETH_RPC_URL, - ) - } + const sdk: Acre = await Acre.initialize( + BITCOIN_NETWORK, + bitcoinProvider, + TBTC_API_ENDPOINT, + ETH_RPC_URL, + ) setAcre(sdk) setIsInitialized(true) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index 0d1057fab..e58644273 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -19,7 +19,7 @@ class Acre { public readonly staking: StakingModule - constructor( + private constructor( contracts: AcreContracts, bitcoinProvider: BitcoinProvider, orangeKit: OrangeKitSdk, @@ -37,42 +37,16 @@ class Acre { ) } - static async initializeMainnet( - bitcoinProvider: BitcoinProvider, - tbtcApiUrl: string, - evmRpcUrl: string, - ) { - return Acre.#initialize( - BitcoinNetwork.Mainnet, - bitcoinProvider, - tbtcApiUrl, - evmRpcUrl, - ) - } - - static async initializeTestnet( - bitcoinProvider: BitcoinProvider, - tbtcApiUrl: string, - evmRpcUrl: string, - ) { - return Acre.#initialize( - BitcoinNetwork.Testnet, - bitcoinProvider, - tbtcApiUrl, - evmRpcUrl, - ) - } - - static async #initialize( + static async initialize( network: BitcoinNetwork, bitcoinProvider: BitcoinProvider, tbtcApiUrl: string, - rpcUrl: string, + evmRpcUrl: string, ) { const evmNetwork: EthereumNetwork = network === BitcoinNetwork.Testnet ? "sepolia" : "mainnet" const evmChainId = getChainIdByNetwork(evmNetwork) - const provider = getDefaultProvider(rpcUrl) + const provider = getDefaultProvider(evmRpcUrl) const providerChainId = (await provider.getNetwork()).chainId if (evmChainId !== providerChainId) { @@ -81,7 +55,7 @@ class Acre { ) } - const orangeKit = await OrangeKitSdk.init(Number(evmChainId), rpcUrl) + const orangeKit = await OrangeKitSdk.init(Number(evmChainId), evmRpcUrl) // TODO: Should we store this address in context so that we do not to // recalculate it when necessary? From 219127e7ad25a44e67042654488556eaebd0b252 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 29 May 2024 13:28:24 +0200 Subject: [PATCH 44/45] Rename variables To improve the readability. --- sdk/src/acre.ts | 25 ++++++++++++++----------- sdk/src/modules/staking/index.ts | 7 +++---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index e58644273..b30a5af46 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -41,21 +41,24 @@ class Acre { network: BitcoinNetwork, bitcoinProvider: BitcoinProvider, tbtcApiUrl: string, - evmRpcUrl: string, + ethereumRpcUrl: string, ) { - const evmNetwork: EthereumNetwork = + const ethereumNetwork: EthereumNetwork = network === BitcoinNetwork.Testnet ? "sepolia" : "mainnet" - const evmChainId = getChainIdByNetwork(evmNetwork) - const provider = getDefaultProvider(evmRpcUrl) + const ethereumChainId = getChainIdByNetwork(ethereumNetwork) + const ethersProvider = getDefaultProvider(ethereumRpcUrl) - const providerChainId = (await provider.getNetwork()).chainId - if (evmChainId !== providerChainId) { + const providerChainId = (await ethersProvider.getNetwork()).chainId + if (ethereumChainId !== providerChainId) { throw new Error( - `Invalid RPC node chain id. Provider chain id: ${providerChainId}; expected chain id: ${evmChainId}`, + `Invalid RPC node chain id. Provider chain id: ${providerChainId}; expected chain id: ${ethereumChainId}`, ) } - const orangeKit = await OrangeKitSdk.init(Number(evmChainId), evmRpcUrl) + const orangeKit = await OrangeKitSdk.init( + Number(ethereumChainId), + ethereumRpcUrl, + ) // TODO: Should we store this address in context so that we do not to // recalculate it when necessary? @@ -63,13 +66,13 @@ class Acre { await bitcoinProvider.getAddress(), ) - const signer = new VoidSigner(depositOwnerEvmAddress, provider) + const signer = new VoidSigner(depositOwnerEvmAddress, ethersProvider) - const contracts = getEthereumContracts(signer, evmNetwork) + const contracts = getEthereumContracts(signer, ethereumNetwork) const tbtc = await Tbtc.initialize( signer, - evmNetwork, + ethereumNetwork, tbtcApiUrl, contracts.bitcoinDepositor, ) diff --git a/sdk/src/modules/staking/index.ts b/sdk/src/modules/staking/index.ts index 1ecead708..fa019d62c 100644 --- a/sdk/src/modules/staking/index.ts +++ b/sdk/src/modules/staking/index.ts @@ -67,8 +67,7 @@ class StakingModule { * @returns Object represents the deposit process. */ async initializeStake(referral: number, bitcoinRecoveryAddress?: string) { - const depositorOwnerBitcoinAddress = - await this.#bitcoinProvider.getAddress() + 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 @@ -76,13 +75,13 @@ class StakingModule { // and `lib`. Currently we support only `Ethereum` so here we force to // `EthereumAddress`. const depositOwnerEvmAddress = EthereumAddress.from( - await this.#orangeKit.predictAddress(depositorOwnerBitcoinAddress), + await this.#orangeKit.predictAddress(depositOwnerBitcoinAddress), ) // tBTC-v2 SDK will handle Bitcoin address validation and throw an error if // address is not supported. const finalBitcoinRecoveryAddress = - bitcoinRecoveryAddress ?? depositorOwnerBitcoinAddress + bitcoinRecoveryAddress ?? depositOwnerBitcoinAddress const tbtcDeposit = await this.#tbtc.initiateDeposit( depositOwnerEvmAddress, From f0fecf061c8a422aeab094083dbbe16a7f4d595b Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 29 May 2024 13:28:39 +0200 Subject: [PATCH 45/45] Leave a `TODO` in the `BitcoinProvider` interface This is just a temporary interface that should be replaced by the `OrangeKitBitcoinWalletProvider` from the `orangekit` library. --- sdk/src/lib/bitcoin/providers/provider.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/src/lib/bitcoin/providers/provider.ts b/sdk/src/lib/bitcoin/providers/provider.ts index 97a3cbf1e..ef71d25f3 100644 --- a/sdk/src/lib/bitcoin/providers/provider.ts +++ b/sdk/src/lib/bitcoin/providers/provider.ts @@ -1,3 +1,5 @@ +// TODO: This is just a temporary interface that should be replaced by the +// `OrangeKitBitcoinWalletProvider` from the `orangekit` library. /** * Interface for communication with Bitcoin Wallet. */