From 119335b693c122da7d26df680bdcc667311bcdc6 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Thu, 14 Mar 2024 10:39:34 +0100 Subject: [PATCH] Cross-chain deposit flow Here we expose a possibility to initiate cross-chain deposit that targets an L2 chain other than the L1 chain the tBTC system is deployed on. This is achieved by exposing the `initiateCrossChainDeposit` method on the `DepositsService` component. --- typescript/src/lib/base/index.ts | 14 ++- .../src/lib/base/l2-bitcoin-depositor.ts | 79 ++++++++++++- typescript/src/lib/contracts/chain.ts | 6 +- typescript/src/lib/contracts/cross-chain.ts | 85 ++++++++++++++ .../src/lib/ethereum/l1-bitcoin-depositor.ts | 86 +++++++++++++- .../src/services/deposits/cross-chain.ts | 110 ++++++++++++++++++ .../src/services/deposits/deposits-service.ts | 68 ++++++++++- typescript/src/services/tbtc.ts | 71 ++++++++--- 8 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 typescript/src/services/deposits/cross-chain.ts diff --git a/typescript/src/lib/base/index.ts b/typescript/src/lib/base/index.ts index 08f1740f1..c34cab9fd 100644 --- a/typescript/src/lib/base/index.ts +++ b/typescript/src/lib/base/index.ts @@ -1,4 +1,8 @@ -import { chainIdFromSigner, EthereumSigner } from "../ethereum" +import { + chainIdFromSigner, + ethereumAddressFromSigner, + EthereumSigner, +} from "../ethereum" import { BaseL2BitcoinDepositor } from "./l2-bitcoin-depositor" import { BaseL2TBTCToken } from "./l2-tbtc-token" import { Chains, L2CrossChainContracts } from "../contracts" @@ -9,11 +13,11 @@ export * from "./l2-tbtc-token" /** * Loads Base implementation of tBTC cross-chain contracts for the given Base * chain ID and attaches the given signer there. - * @param signer Signer that should be attached to tBTC contracts. + * @param signer Signer that should be attached to the contracts. * @param chainId Base chain ID. - * @returns Handle to tBTC cross-chain contracts. + * @returns Handle to the contracts. * @throws Throws an error if the signer's Base chain ID is other than - * the one used to load tBTC contracts. + * the one used to load contracts. */ export async function loadBaseCrossChainContracts( signer: EthereumSigner, @@ -30,6 +34,8 @@ export async function loadBaseCrossChainContracts( { signerOrProvider: signer }, chainId ) + l2BitcoinDepositor.setDepositOwner(await ethereumAddressFromSigner(signer)) + const l2TbtcToken = new BaseL2TBTCToken({ signerOrProvider: signer }, chainId) return { diff --git a/typescript/src/lib/base/l2-bitcoin-depositor.ts b/typescript/src/lib/base/l2-bitcoin-depositor.ts index 358069f5c..2419ce0bc 100644 --- a/typescript/src/lib/base/l2-bitcoin-depositor.ts +++ b/typescript/src/lib/base/l2-bitcoin-depositor.ts @@ -4,12 +4,24 @@ import { EthersContractHandle, } from "../ethereum/adapter" import { L2BitcoinDepositor as L2BitcoinDepositorTypechain } from "../../../typechain/L2BitcoinDepositor" -import { ChainIdentifier, Chains, L2BitcoinDepositor } from "../contracts" -import { EthereumAddress } from "../ethereum" +import { + ChainIdentifier, + Chains, + CrossChainExtraDataEncoder, + DepositReceipt, + L2BitcoinDepositor, +} from "../contracts" +import { + EthereumAddress, + EthereumCrossChainExtraDataEncoder, + packRevealDepositParameters, +} from "../ethereum" // TODO: Uncomment once BaseL2BitcoinDepositor is available on Base mainnet. // import BaseL2BitcoinDepositorDeployment from "./artifacts/base/BaseL2BitcoinDepositor.json" import BaseSepoliaL2BitcoinDepositorDeployment from "./artifacts/baseSepolia/BaseL2BitcoinDepositor.json" +import { BitcoinRawTxVectors } from "../bitcoin" +import { Hex } from "../utils" /** * Implementation of the Base L2BitcoinDepositor handle. @@ -19,6 +31,9 @@ export class BaseL2BitcoinDepositor extends EthersContractHandle implements L2BitcoinDepositor { + readonly #extraDataEncoder: CrossChainExtraDataEncoder + #depositOwner: ChainIdentifier | undefined + constructor(config: EthersContractConfig, chainId: Chains.Base) { let deployment: EthersContractDeployment @@ -35,6 +50,8 @@ export class BaseL2BitcoinDepositor } super(config, deployment) + + this.#extraDataEncoder = new EthereumCrossChainExtraDataEncoder() } // eslint-disable-next-line valid-jsdoc @@ -44,4 +61,62 @@ export class BaseL2BitcoinDepositor getChainIdentifier(): ChainIdentifier { return EthereumAddress.from(this._instance.address) } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {L2BitcoinDepositor#getDepositOwner} + */ + getDepositOwner(): ChainIdentifier | undefined { + return this.#depositOwner + } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {L2BitcoinDepositor#setDepositOwner} + */ + setDepositOwner(depositOwner: ChainIdentifier | undefined) { + this.#depositOwner = depositOwner + } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {L2BitcoinDepositor#extraDataEncoder} + */ + extraDataEncoder(): CrossChainExtraDataEncoder { + return this.#extraDataEncoder + } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {L2BitcoinDepositor#initializeDeposit} + */ + async initializeDeposit( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise { + const { fundingTx, reveal } = packRevealDepositParameters( + depositTx, + depositOutputIndex, + deposit, + vault + ) + + if (!deposit.extraData) { + throw new Error("Extra data is required") + } + + const l2DepositOwner = this.extraDataEncoder().decodeDepositOwner( + deposit.extraData + ) + + const tx = await this._instance.initializeDeposit( + fundingTx, + reveal, + `0x${l2DepositOwner.identifierHex}` + ) + + return Hex.from(tx.hash) + } } diff --git a/typescript/src/lib/contracts/chain.ts b/typescript/src/lib/contracts/chain.ts index bfeea9b30..1393e3acd 100644 --- a/typescript/src/lib/contracts/chain.ts +++ b/typescript/src/lib/contracts/chain.ts @@ -1,6 +1,6 @@ /* eslint-disable no-unused-vars */ /** - * Chains supported by tBTC v2 SDK. + * Chains supported by tBTC v2 contracts. */ export namespace Chains { export enum Ethereum { @@ -16,7 +16,7 @@ export namespace Chains { } /** - * Layer 2 chains supported by tBTC v2 SDK. + * Layer 2 chains supported by tBTC v2 contracts. */ export type L2Chain = Exclude @@ -35,7 +35,7 @@ export type ChainMapping = { } /** - * List of chain mappings supported by tBTC v2 SDK. + * List of chain mappings supported by tBTC v2 contracts. */ export const ChainMappings: ChainMapping[] = [ { diff --git a/typescript/src/lib/contracts/cross-chain.ts b/typescript/src/lib/contracts/cross-chain.ts index 5f7f432e9..869e9201b 100644 --- a/typescript/src/lib/contracts/cross-chain.ts +++ b/typescript/src/lib/contracts/cross-chain.ts @@ -1,6 +1,9 @@ import { ChainIdentifier } from "./chain-identifier" import { BigNumber } from "ethers" import { ChainMapping, L2Chain } from "./chain" +import { BitcoinRawTxVectors } from "../bitcoin" +import { DepositReceipt } from "./bridge" +import { Hex } from "../utils" /** * Convenience type aggregating TBTC cross-chain contracts forming a connector @@ -67,6 +70,43 @@ export interface L2BitcoinDepositor { * Gets the chain-specific identifier of this contract. */ getChainIdentifier(): ChainIdentifier + + /** + * Gets the identifier that should be used as the owner of the deposits + * issued by this contract. + * @returns The identifier of the deposit owner or undefined if not set. + */ + getDepositOwner(): ChainIdentifier | undefined + + /** + * Sets the identifier that should be used as the owner of the deposits + * issued by this contract. + * @param depositOwner Identifier of the deposit owner or undefined to clear. + */ + setDepositOwner(depositOwner: ChainIdentifier): void + + /** + * @returns Extra data encoder for this contract. The encoder is used to + * encode and decode the extra data included in the cross-chain deposit script. + */ + extraDataEncoder(): CrossChainExtraDataEncoder + + /** + * Initializes the cross-chain deposit indirectly through the given L2 chain. + * @param depositTx Deposit transaction data + * @param depositOutputIndex Index of the deposit transaction output that + * funds the revealed deposit + * @param deposit Data of the revealed deposit + * @param vault Optional parameter denoting the vault the given deposit + * should be routed to + * @returns Transaction hash of the reveal deposit transaction. + */ + initializeDeposit( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise } /** @@ -78,4 +118,49 @@ export interface L1BitcoinDepositor { * Gets the chain-specific identifier of this contract. */ getChainIdentifier(): ChainIdentifier + + /** + * @returns Extra data encoder for this contract. The encoder is used to + * encode and decode the extra data included in the cross-chain deposit script. + */ + extraDataEncoder(): CrossChainExtraDataEncoder + + /** + * Initializes the cross-chain deposit directly on the given L1 chain. + * @param depositTx Deposit transaction data + * @param depositOutputIndex Index of the deposit transaction output that + * funds the revealed deposit + * @param deposit Data of the revealed deposit + * @param vault Optional parameter denoting the vault the given deposit + * should be routed to + * @returns Transaction hash of the reveal deposit transaction. + */ + initializeDeposit( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise +} + +/** + * Interface for encoding and decoding the extra data included in the + * cross-chain deposit script. + */ +export interface CrossChainExtraDataEncoder { + /** + * Encodes the given deposit owner identifier into the extra data. + * @param depositOwner Identifier of the deposit owner to encode. + * For cross-chain deposits, the deposit owner is typically an + * identifier on the L2 chain. + * @returns Encoded extra data. + */ + encodeDepositOwner(depositOwner: ChainIdentifier): Hex + + /** + * Decodes the extra data into the deposit owner identifier. + * @param extraData Extra data to decode. + * @returns Identifier of the deposit owner. + */ + decodeDepositOwner(extraData: Hex): ChainIdentifier } diff --git a/typescript/src/lib/ethereum/l1-bitcoin-depositor.ts b/typescript/src/lib/ethereum/l1-bitcoin-depositor.ts index 59dfe3444..d29fe0f8e 100644 --- a/typescript/src/lib/ethereum/l1-bitcoin-depositor.ts +++ b/typescript/src/lib/ethereum/l1-bitcoin-depositor.ts @@ -7,10 +7,14 @@ import { L1BitcoinDepositor as L1BitcoinDepositorTypechain } from "../../../type import { ChainIdentifier, Chains, + CrossChainExtraDataEncoder, + DepositReceipt, L1BitcoinDepositor, L2Chain, } from "../contracts" -import { EthereumAddress } from "./index" +import { EthereumAddress, packRevealDepositParameters } from "./index" +import { BitcoinRawTxVectors } from "../bitcoin" +import { Hex } from "../utils" // TODO: Uncomment once BaseL1BitcoinDepositor is available on Ethereum mainnet. // import MainnetBaseL1BitcoinDepositorDeployment from "./artifacts/mainnet/BaseL1BitcoinDepositor.json" @@ -46,6 +50,8 @@ export class EthereumL1BitcoinDepositor extends EthersContractHandle implements L1BitcoinDepositor { + readonly #extraDataEncoder: CrossChainExtraDataEncoder + constructor( config: EthersContractConfig, chainId: Chains.Ethereum, @@ -65,6 +71,8 @@ export class EthereumL1BitcoinDepositor } super(config, deployment) + + this.#extraDataEncoder = new EthereumCrossChainExtraDataEncoder() } // eslint-disable-next-line valid-jsdoc @@ -74,4 +82,80 @@ export class EthereumL1BitcoinDepositor getChainIdentifier(): ChainIdentifier { return EthereumAddress.from(this._instance.address) } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {L1BitcoinDepositor#extraDataEncoder} + */ + extraDataEncoder(): CrossChainExtraDataEncoder { + return this.#extraDataEncoder + } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {L1BitcoinDepositor#initializeDeposit} + */ + async initializeDeposit( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise { + const { fundingTx, reveal } = packRevealDepositParameters( + depositTx, + depositOutputIndex, + deposit, + vault + ) + + if (!deposit.extraData) { + throw new Error("Extra data is required") + } + + const l2DepositOwner = this.extraDataEncoder().decodeDepositOwner( + deposit.extraData + ) + + const tx = await this._instance.initializeDeposit( + fundingTx, + reveal, + `0x${l2DepositOwner.identifierHex}` + ) + + return Hex.from(tx.hash) + } +} + +/** + * Implementation of the Ethereum CrossChainExtraDataEncoder. + * @see {CrossChainExtraDataEncoder} for reference. + */ +export class EthereumCrossChainExtraDataEncoder + implements CrossChainExtraDataEncoder +{ + // eslint-disable-next-line valid-jsdoc + /** + * @see {CrossChainExtraDataEncoder#encodeDepositOwner} + */ + encodeDepositOwner(depositOwner: ChainIdentifier): Hex { + // Make sure we are dealing with an Ethereum address. If not, this + // call will throw. + const address = EthereumAddress.from(depositOwner.identifierHex) + + // Extra data must be 32-byte so prefix the 20-byte address with + // 12 zero bytes. + return Hex.from(`000000000000000000000000${address.identifierHex}`) + } + + // eslint-disable-next-line valid-jsdoc + /** + * @see {CrossChainExtraDataEncoder#decodeDepositOwner} + */ + decodeDepositOwner(extraData: Hex): ChainIdentifier { + // Cut the first 12 zero bytes of the extra data and convert the rest to + + return EthereumAddress.from( + Hex.from(extraData.toBuffer().subarray(12)).toString() + ) + } } diff --git a/typescript/src/services/deposits/cross-chain.ts b/typescript/src/services/deposits/cross-chain.ts new file mode 100644 index 000000000..07294361d --- /dev/null +++ b/typescript/src/services/deposits/cross-chain.ts @@ -0,0 +1,110 @@ +import { + ChainIdentifier, + CrossChainContracts, + CrossChainExtraDataEncoder, + DepositorProxy, + DepositReceipt, +} from "../../lib/contracts" +import { BitcoinRawTxVectors } from "../../lib/bitcoin" +import { Hex } from "../../lib/utils" + +/** + * Mode of operation for the cross-chain depositor proxy: + * - [L2Transaction]: The proxy will reveal the deposit using a transaction on + * the L2 chain. The tBTC system is responsible for relaying the deposit to + * the tBTC L1 chain. + * - [L1Transaction]: The proxy will directly reveal the deposit using a + * transaction on the tBTC L1 chain. + */ +export type CrossChainDepositorMode = "L2Transaction" | "L1Transaction" + +/** + * Implementation of the cross chain depositor proxy. This component is used to + * reveal cross-chain deposits whose target chain is not the same as the L1 + * chain the tBTC system is deployed on. + * @see {DepositorProxy} for reference. + */ +export class CrossChainDepositor implements DepositorProxy { + readonly #crossChainContracts: CrossChainContracts + readonly #revealMode: CrossChainDepositorMode + + constructor( + crossChainContracts: CrossChainContracts, + revealMode: CrossChainDepositorMode = "L2Transaction" + ) { + this.#crossChainContracts = crossChainContracts + this.#revealMode = revealMode + } + + /** + * @returns The chain-specific identifier of the contract that will be + * used as the actual L1 depositor embedded in the deposit script. + * In this case, the depositor must be the L1BitcoinDepositor contract + * corresponding to the given L2 chain the deposit is targeting. + * This is because the L1BitcoinDepositor contract reveals the deposit to + * the Bridge contract (on L1) and transfers minted TBTC token to the + * target L2 chain once the deposit is processed. + * @see {DepositorProxy#getChainIdentifier} + */ + getChainIdentifier(): ChainIdentifier { + return this.#crossChainContracts.l1BitcoinDepositor.getChainIdentifier() + } + + /** + * @returns Extra data for the cross-chain deposit script. Actually, this is + * the L2 deposit owner identifier took from the L2BitcoinDepositor + * contract. + * @throws Throws if the L2 deposit owner cannot be resolved. This + * typically happens if the L2BitcoinDepositor operates with + * a read-only signer whose address cannot be resolved. + */ + extraData(): Hex { + const depositOwner = + this.#crossChainContracts.l2BitcoinDepositor.getDepositOwner() + + if (!depositOwner) { + throw new Error("Cannot resolve L2 deposit owner") + } + + return this.#extraDataEncoder().encodeDepositOwner(depositOwner) + } + + #extraDataEncoder(): CrossChainExtraDataEncoder { + switch (this.#revealMode) { + case "L2Transaction": + return this.#crossChainContracts.l2BitcoinDepositor.extraDataEncoder() + case "L1Transaction": + return this.#crossChainContracts.l1BitcoinDepositor.extraDataEncoder() + } + } + + // eslint-disable-next-line valid-jsdoc + /** + * Reveals the given deposit depending on the reveal mode. + * @see {CrossChainDepositorMode} for reveal modes description. + * @see {DepositorProxy#revealDeposit} + */ + revealDeposit( + depositTx: BitcoinRawTxVectors, + depositOutputIndex: number, + deposit: DepositReceipt, + vault?: ChainIdentifier + ): Promise { + switch (this.#revealMode) { + case "L2Transaction": + return this.#crossChainContracts.l2BitcoinDepositor.initializeDeposit( + depositTx, + depositOutputIndex, + deposit, + vault + ) + case "L1Transaction": + return this.#crossChainContracts.l1BitcoinDepositor.initializeDeposit( + depositTx, + depositOutputIndex, + deposit, + vault + ) + } + } +} diff --git a/typescript/src/services/deposits/deposits-service.ts b/typescript/src/services/deposits/deposits-service.ts index 974708941..097caffff 100644 --- a/typescript/src/services/deposits/deposits-service.ts +++ b/typescript/src/services/deposits/deposits-service.ts @@ -1,7 +1,9 @@ import { ChainIdentifier, + CrossChainContracts, DepositorProxy, DepositReceipt, + L2Chain, TBTCContracts, } from "../../lib/contracts" import { @@ -14,6 +16,7 @@ import { import { Hex } from "../../lib/utils" import { Deposit } from "./deposit" import * as crypto from "crypto" +import { CrossChainDepositor } from "./cross-chain" /** * Service exposing features related to tBTC v2 deposits. @@ -36,11 +39,23 @@ export class DepositsService { * Chain-specific identifier of the default depositor used for deposits * initiated by this service. */ - private defaultDepositor: ChainIdentifier | undefined + #defaultDepositor: ChainIdentifier | undefined + /** + * Gets cross-chain contracts for the given supported L2 chain. + * @param _ Name of the L2 chain for which to get cross-chain contracts. + * @returns Cross-chain contracts for the given L2 chain or + * undefined if not initialized. + */ + readonly #crossChainContracts: (_: L2Chain) => CrossChainContracts | undefined - constructor(tbtcContracts: TBTCContracts, bitcoinClient: BitcoinClient) { + constructor( + tbtcContracts: TBTCContracts, + bitcoinClient: BitcoinClient, + crossChainContracts: (_: L2Chain) => CrossChainContracts | undefined + ) { this.tbtcContracts = tbtcContracts this.bitcoinClient = bitcoinClient + this.#crossChainContracts = crossChainContracts } /** @@ -62,7 +77,7 @@ export class DepositsService { bitcoinRecoveryAddress: string, extraData?: Hex ): Promise { - if (this.defaultDepositor === undefined) { + if (this.#defaultDepositor === undefined) { throw new Error( "Default depositor is not set; use setDefaultDepositor first" ) @@ -70,7 +85,7 @@ export class DepositsService { const receipt = await this.generateDepositReceipt( bitcoinRecoveryAddress, - this.defaultDepositor, + this.#defaultDepositor, extraData ) @@ -116,6 +131,49 @@ export class DepositsService { ) } + /** + * Initiates the tBTC v2 cross-chain deposit process. A cross-chain deposit + * is a deposit that targets an L2 chain other than the L1 chain the tBTC + * system is deployed on. Such a deposit is initiated using a transaction + * on the L2 chain. To make it happen, the given L2 cross-chain contracts + * must be initialized along with a L2 signer first. + * @param bitcoinRecoveryAddress P2PKH or P2WPKH Bitcoin address that can + * be used for emergency recovery of the + * deposited funds. + * @param l2ChainName Name of the L2 chain the deposit is targeting. + * @returns Handle to the initiated deposit process. + * @throws Throws an error if one of the following occurs: + * - There are no active wallet in the Bridge contract + * - The Bitcoin recovery address is not a valid P2(W)PKH + * - The cross-chain contracts for the given L2 chain are not + * initialized + * - The L2 deposit owner cannot be resolved. This typically + * happens if the L2 cross-chain contracts operate with a + * read-only signer whose address cannot be resolved. + * @see {TBTC#initializeCrossChain} for cross-chain contracts initialization. + * @dev This is actually a call to initiateDepositWithProxy with a built-in + * depositor proxy. + */ + async initiateCrossChainDeposit( + bitcoinRecoveryAddress: string, + l2ChainName: L2Chain + ): Promise { + const crossChainContracts = this.#crossChainContracts(l2ChainName) + if (!crossChainContracts) { + throw new Error( + `Cross-chain contracts for ${l2ChainName} not initialized` + ) + } + + const depositorProxy = new CrossChainDepositor(crossChainContracts) + + return this.initiateDepositWithProxy( + bitcoinRecoveryAddress, + depositorProxy, + depositorProxy.extraData() + ) + } + private async generateDepositReceipt( bitcoinRecoveryAddress: string, depositor: ChainIdentifier, @@ -195,6 +253,6 @@ export class DepositsService { * Make sure you know what you are doing while using this method. */ setDefaultDepositor(defaultDepositor: ChainIdentifier) { - this.defaultDepositor = defaultDepositor + this.#defaultDepositor = defaultDepositor } } diff --git a/typescript/src/services/tbtc.ts b/typescript/src/services/tbtc.ts index a4428a8df..9e82eaf09 100644 --- a/typescript/src/services/tbtc.ts +++ b/typescript/src/services/tbtc.ts @@ -5,7 +5,9 @@ import { Chains, CrossChainContracts, CrossChainContractsLoader, + L1CrossChainContracts, L2Chain, + L2CrossChainContracts, TBTCContracts, } from "../lib/contracts" import { BitcoinClient, BitcoinNetwork } from "../lib/bitcoin" @@ -46,19 +48,30 @@ export class TBTC { /** * Reference to the cross-chain contracts loader. */ - private readonly crossChainContractsLoader?: CrossChainContractsLoader + readonly #crossChainContractsLoader?: CrossChainContractsLoader + /** + * Mapping of cross-chain contracts for different supported L2 chains. + * Each set of cross-chain contracts must be first initialized using + * the `initializeCrossChain` method. + */ + readonly #crossChainContracts: Map private constructor( tbtcContracts: TBTCContracts, bitcoinClient: BitcoinClient, crossChainContractsLoader?: CrossChainContractsLoader ) { - this.deposits = new DepositsService(tbtcContracts, bitcoinClient) + this.deposits = new DepositsService( + tbtcContracts, + bitcoinClient, + (l2ChainName) => this.crossChainContracts(l2ChainName) + ) this.maintenance = new MaintenanceService(tbtcContracts, bitcoinClient) this.redemptions = new RedemptionsService(tbtcContracts, bitcoinClient) this.tbtcContracts = tbtcContracts this.bitcoinClient = bitcoinClient - this.crossChainContractsLoader = crossChainContractsLoader + this.#crossChainContractsLoader = crossChainContractsLoader + this.#crossChainContracts = new Map() } /** @@ -155,35 +168,38 @@ export class TBTC { } /** - * Returns cross-chain contracts for the given L2 chain. - * @param l2ChainName Name of the L2 chain for which to load cross-chain contracts. - * @param l2Signer Signer for the L2 chain contracts. - * @returns Cross-chain contracts for the given L2 chain. + * Initializes cross-chain contracts for the given L2 chain, using the + * given signer. Updates the signer on subsequent calls. + * @param l2ChainName Name of the L2 chain for which to initialize + * cross-chain contracts. + * @param l2Signer Signer to use with the L2 chain contracts. + * @returns Void promise. * @throws Throws an error if: * - Cross-chain contracts loader is not available for this TBTC SDK instance, - * - Chain mapping between L1 and L2 chains is not defined. + * - Chain mapping between the L1 and the given L2 chain is not defined. * @dev In case this function needs to support non-EVM L2 chains that can't * use EthereumSigner as a signer type, the l2Signer parameter should * probably be turned into a union of multiple supported types or * generalized in some other way. */ - async crossChainContracts( + async initializeCrossChain( l2ChainName: L2Chain, l2Signer: EthereumSigner - ): Promise { - if (!this.crossChainContractsLoader) { + ): Promise { + if (!this.#crossChainContractsLoader) { throw new Error( "Cross-chain contracts loader not available for this instance" ) } - const chainMapping = this.crossChainContractsLoader.loadChainMapping() + const chainMapping = this.#crossChainContractsLoader.loadChainMapping() if (!chainMapping) { throw new Error("Chain mapping between L1 and L2 chains not defined") } - const L1CrossChainContracts = - await this.crossChainContractsLoader.loadL1Contracts(l2ChainName) + const l1CrossChainContracts: L1CrossChainContracts = + await this.#crossChainContractsLoader.loadL1Contracts(l2ChainName) + let l2CrossChainContracts: L2CrossChainContracts switch (l2ChainName) { case "Base": @@ -191,13 +207,30 @@ export class TBTC { if (!baseChainId) { throw new Error("Base chain ID not available in chain mapping") } - - return { - ...L1CrossChainContracts, - ...(await loadBaseCrossChainContracts(l2Signer, baseChainId)), - } + l2CrossChainContracts = await loadBaseCrossChainContracts( + l2Signer, + baseChainId + ) + break default: throw new Error("Unsupported L2 chain") } + + this.#crossChainContracts.set(l2ChainName, { + ...l1CrossChainContracts, + ...l2CrossChainContracts, + }) + } + + /** + * Gets cross-chain contracts for the given supported L2 chain. + * The given L2 chain contracts must be first initialized using the + * `initializeCrossChain` method. + * @param l2ChainName Name of the L2 chain for which to get cross-chain contracts. + * @returns Cross-chain contracts for the given L2 chain or + * undefined if not initialized. + */ + crossChainContracts(l2ChainName: L2Chain): CrossChainContracts | undefined { + return this.#crossChainContracts.get(l2ChainName) } }