Next.js Demo - Application • Next.js Demo - Repository
This TypeScript SDK is designed for building token bridge applications between Tezos (L1) and Etherlink (L2). It allows developers to easily:
The SDK is isomorphic, enabling developers to create bridge applications on the frontend using popular frameworks or Vanilla JavaScript, as well as on the server-side using Node.js.
Install the SDK package:
npm install @baking-bad/tezos-etherlink-bridge-sdk
or
yarn add @baking-bad/tezos-etherlink-bridge-sdk
Install additional dependencies:
The SDK requires additional libraries for interacting with the blockchains. These packages are marked as optional peerDependencies
, so depending on your preference, you also need to install and configure the appropriate package for each blockchain manually in your application.
Tezos
Taquito. The SDK supports both the Wallet API and Contract API:
The Wallet API. If your application needs to interact with wallets using the Beacon SDK
npm install @taquito/taquito @taquito/beacon-wallet
or
yarn add @taquito/taquito @taquito/beacon-wallet
Contract API.
npm install @taquito/taquito
or
yarn add @taquito/taquito
Etherlink. Choose one of these packages for interfacing with the Etherlink blockchain, depending on your preference. You only need one of these packages, not both.
Install ws for Node.js applications (optional):
If you are developing a Node.js application, you also need to install the ws package as Node.js doesn't have a native WebSocket implementation.
npm install ws
or
yarn add ws
Depending on the blockchain libraries you choose to use, you will need to configure them to sign data and transactions.
import { BeaconWallet } from '@taquito/beacon-wallet';
import { TezosToolkit } from '@taquito/taquito';
const tezosRpcUrl = 'https://rpc.tzkt.io/oxfordnet';
const tezosToolkit = new TezosToolkit(tezosRpcUrl);
const beaconWallet = new BeaconWallet({
name: 'Your dApp name',
network: { type: 'custom', rpcUrl: tezosRpcUrl }
});
tezosToolkit.setWalletProvider(beaconWallet);
import { InMemorySigner } from '@taquito/signer';
import { TezosToolkit } from '@taquito/taquito';
const tezosRpcUrl = 'https://rpc.tzkt.io/oxfordnet';
const tezosToolkit = new TezosToolkit(tezosRpcUrl);
const signer = new InMemorySigner('<your secret key>');
tezosToolkit.setSignerProvider(signer);
import Web3 from 'web3';
// Use MetaMask
const web3 = new Web3(window.ethereum);
ℹ️ Currently in development ℹ️
The SDK only allows registered (listed) tokens to be transferred between Tezos and Etherlink (see details). Configure a list of token pairs or the corresponding provider:
Once token pairs are configured, creating an instance of TokenBridge
is a type through which you can deposit, withdraw tokens, and receive token transfers between layers. There two approaches to create the TokenBridge
instance.
The first one is to use the createDefaultTokenBridge
method, which creates an instance with default options and the Default Data Provider that provides token transfers, balances, and registered tokens to TokenBridge
:
import { createDefaultTokenBridge } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const tokenBridge = createDefaultTokenBridge({
tezos: {
toolkit: tezosToolkit,
rollupAddress: 'sr1T4XVcVtBRzYy52edVTdgup9Kip4Wrmn97'
},
etherlink: {
toolkit: web3
},
dipDup: {
baseUrl: 'https://etherlink-indexer.dipdup.net',
webSocketApiBaseUrl: 'wss://etherlink-indexer.dipdup.net'
},
tzKTApiBaseUrl: 'https://api.oxfordnet.tzkt.io',
etherlinkRpcUrl: 'https://etherlink.dipdup.net',
tokenPairs,
});
The second approach is to create an instance directly (new TokenBridge(<options>)
) and configure all options and data providers manually:
import {
TokenBridge, DefaultDataProvider,
defaultEtherlinkKernelAddress, defaultEtherlinkWithdrawPrecompileAddress
} from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const defaultDataProvider = new DefaultDataProvider({
dipDup: {
baseUrl: 'https://etherlink-indexer.dipdup.net/',
webSocketApiBaseUrl: 'wss://etherlink-indexer.dipdup.net'
},
tzKTApiBaseUrl: 'https://api.oxfordnet.tzkt.io',
etherlinkRpcUrl: 'https://etherlink.dipdup.net',
tokenPairs
});
const tokenBridge = new TokenBridge({
tezos: {
toolkit: tezosToolkit,
bridgeOptions: {
rollupAddress: 'sr1T4XVcVtBRzYy52edVTdgup9Kip4Wrmn97'
}
},
etherlink: {
toolkit: web3,
bridgeOptions: {
kernelAddress: defaultEtherlinkKernelAddress,
withdrawPrecompileAddress: defaultEtherlinkWithdrawPrecompileAddress
}
},
bridgeDataProviders: {
tokens: defaultDataProvider,
balances: defaultDataProvider,
transfers: defaultDataProvider
}
});
Your application is ready to deposit/withdraw user tokens and receive their corresponding transfers.
There are two type of token transfers:
withdraw
method of the Etherlink kernel (sending a transaction in Etherlink)To deposit tokens, use the asynchronous TokenBridge.deposit
method, passing amount in raw type (not divided by token decimals) and Tezos token.
The method returns an object that includes two fields:
tokenTransfer
. Represents the bridge transfer with BridgeTokenTransferKind.Deposit
type and BridgeTokenTransferStatus.Pending
status.depositOperation
. An operation object in the format compatible with the chosen Tezos Blockchain Component, representing the Tezos operation for the deposit.import { BridgeTokenTransferStatus, type FA12TezosToken } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const ctezTezosToken: FA12TezosToken = {
type: 'fa1.2',
address: 'KT1GM2AnBAJWdzrChp3hTYFTSb6Dmh61peBP'
};
// Deposit 1 Ctez (6 decimals) to Etherlink (L2)
const { tokenTransfer, depositOperation } = await tokenBridge.deposit(1_000_000n, ctezTezosToken);
console.dir(tokenTransfer, { depth: null });
The Deposit transfer has the following statuses (BridgeTokenTransferStatus
):
After initiating a deposit by sending a Tezos operation, you need a way to track its progress and completion. The SDK offers two approaches:
Using the asynchronous TokenBridge.waitForStatus
method. This method allows you to wait until the specified token transfer reaches the specified status or a higher one.
This method subscribes to real-time updates for the specified token transfer. Once the transfer reaches the specified status (BridgeTokenTransferStatus
) or a higher one, the method unsubscribes and returns a resolved promise containing the updated token transfer:
import { BridgeTokenTransferStatus, type FA12TezosToken } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const ctezTezosToken: FA12TezosToken = {
type: 'fa1.2',
address: 'KT1GM2AnBAJWdzrChp3hTYFTSb6Dmh61peBP'
};
// Deposit 1 Ctez (6 decimals) to Etherlink (L2)
const { tokenTransfer, depositOperation } = await tokenBridge.deposit(1_000_000n, ctezTezosToken);
console.dir(tokenTransfer, { depth: null });
// Wait until the deposit status is Finished
const finishedBridgeTokenDeposit = await tokenBridge.waitForStatus(
tokenTransfer,
BridgeTokenTransferStatus.Finished
);
console.dir(finishedBridgeTokenDeposit, { depth: null });
By default, the deposit
method uses the signer's address from the Etherlink Blockchain Component as the recipient address on the Etherlink (L2) blockchain. However, you can specify a custom recipient address for the deposit by passing it as the third argument to the deposit
method:
// ...
const etherlinkReceiverAddress = '0x...';
const { tokenTransfer, depositOperation } = await tokenBridge.deposit(1_000_000n, ctezTezosToken, etherlinkReceiverAddress);
Additionally, the SDK automatically adds token approval operations. However, if needed, you can disable this feature by passing the useApprove: false
option flag:
await tokenBridge.deposit(amount, token, { useApprove: false });
// or
await tokenBridge.deposit(amount, token, etherlinkReceiverAddress, { useApprove: false });
The SDK also automatically resets token approval for FA1.2 tokens by adding an additional operation with approve 0 amount. If you don't need this behavior, you can disable it by passing the resetFA12Approve: false
option flag:
await tokenBridge.deposit(amount, token, { resetFA12Approve: false });
// or
await tokenBridge.deposit(amount, token, etherlinkReceiverAddress, { resetFA12Approve: false });
The withdrawal process involves two stages: initiating the withdrawal on the Etherlink (L2) blockchain and completing it on the Tezos (L1) blockchain.
Initiating Withdrawal (Etherlink, L2):
To start withdraw tokens call the asynchronous TokenBridge.startWithdraw
method, passing amount in raw type (not divided by token decimals) and Etherlink token.
The method returns an object that includes two fields:
tokenTransfer
. Represents the bridge transfer with BridgeTokenTransferKind.Withdrawal
type and BridgeTokenTransferStatus.Pending
status.startWithdrawOperation
. An operation object in the format compatible with the chosen Etherlink Blockchain Component, representing the Etherlink transaction for the withdrawal.import { BridgeTokenTransferStatus, type ERC20EtherlinkToken } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const ctezEtherlinkToken: ERC20EtherlinkToken = {
type: 'erc20',
address: '0x8554cd57c0c3e5ab9d1782c9063279fa9bfa4680'
};
// Withdraw 1 Ctez (6 decimals) from Etherlink (L2)
// The first stage
const { tokenTransfer, startWithdrawOperation } = await tokenBridge.startWithdraw(1_000_000n, ctezEtherlinkToken);
console.dir(startWithdrawResult.tokenTransfer, { depth: null });
Completing Withdrawal (Tezos, L1):
Once the corresponding commitment in Tezos is cemented, the token transfer status will change to BridgeTokenTransferStatus.Sealed
. At this stage, the transfer will include additional rollup data (commitment
and proof
) needed for completion.
To complete the withdrawal, call the asynchronous TokenBridge.finishWithdraw
method, passing the transfer with BridgeTokenTransferStatus.Sealed
status.
The method returns an object that includes two fields:
tokenTransfer
. Represents the bridge transfer with BridgeTokenTransferKind.Withdrawal
type and BridgeTokenTransferStatus.Sealed
status.finishWithdrawOperation
. An operation object in the format compatible with the chosen Tezos Blockchain Component, representing the Tezos operation for the execution of smart rollup outbox message.const sealedBridgeTokenWithdrawal: SealedBridgeTokenWithdrawal = // ...
const { tokenTransfer, finishWithdrawOperation } = await tokenBridge.finishWithdraw(sealedBridgeTokenWithdrawal);
The Withdrawal transfer has the following statuses (BridgeTokenTransferStatus
):
Similar to deposits, the SDK offers two approaches to track the withdrawal progress:
Using the asynchronous TokenBridge.waitForStatus
method. This method allows you to wait until the specified token transfer reaches the specified status or a higher one.
This method subscribes to real-time updates for the specified token transfer. Once the transfer reaches the specified status (BridgeTokenTransferStatus
) or a higher one, the method unsubscribes and returns a resolved promise containing the updated token transfer:
import { BridgeTokenTransferStatus, type ERC20EtherlinkToken } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const ctezEtherlinkToken: ERC20EtherlinkToken = {
type: 'erc20',
address: '0x8554cd57c0c3e5ab9d1782c9063279fa9bfa4680'
};
// Withdraw 1 Ctez (6 decimals) from Etherlink (L2)
// The first stage
const startWithdrawResult = await tokenBridge.startWithdraw(1_000_000n, ctezEtherlinkToken);
console.dir(startWithdrawResult.tokenTransfer, { depth: null });
// Wait until the withdrawal status is Sealed
const sealedBridgeTokenWithdrawal = await tokenBridge.waitForStatus(
startWithdrawResult.tokenTransfer,
BridgeTokenTransferStatus.Sealed
);
console.dir(sealedBridgeTokenWithdrawal, { depth: null });
// The second stage
const finishWithdrawResult = await tokenBridge.finishWithdraw(sealedBridgeTokenWithdrawal);
// Wait until the withdrawal status is Finished
const finishedBridgeTokenWithdrawal = await tokenBridge.waitForStatus(
finishWithdrawResult.tokenTransfer,
BridgeTokenTransferStatus.Finished
);
console.dir(finishedBridgeTokenWithdrawal, { depth: null });
The SDK offers the Stream API (accessed through TokenBridge.stream
) for subscribing to real-time updates on token transfers. You can track token transfers and react to them accordingly using the following events:
tokenTransferCreated
. This event is triggered when a new token transfer is:
TokenBridge.deposit
or TokenBridge.withdraw
).ℹ️ This event does not directly correlate with the
BridgeTokenTransferStatus.Created
status. ThetokenTransferCreated
event can be emitted for token transfers with any status. For example, the event can be emitted for a non-local deposit with theBridgeTokenTransferStatus.Finished
status when the deposit can be completed immediately.
tokenTransferUpdated
. This event is triggered whenever an existing token transfer is updated.// ...
// const tokenBridge: TokenBridge = ...
// ...
tokenBridge.addEventListener('tokenTransferCreated', tokenTransfer => {
console.log('A new token transfer has been created:');
console.dir(tokenTransfer, { depth: null });
});
tokenBridge.addEventListener('tokenTransferUpdated', tokenTransfer => {
console.log('A token transfer has been updated:');
console.dir(tokenTransfer, { depth: null });
});
The TokenBridge.stream.subscribeToTokenTransfer
method allows you to subscribe to real-time updates of a specific token transfer by providing either its token transfer object or its operation/transaction hash:
By token transfer object:
const tokenTransfer: BridgeTokenTransfer = // ...
tokenBridge.stream.subscribeToTokenTransfer(tokenTransfer);
By operation/transaction hash:
// Subscribe to token transfer by Tezos operation hash
tokenBridge.stream.subscribeToTokenTransfer('o...');
// Subscribe to token transfer by Etherlink transaction hash
tokenBridge.stream.subscribeToTokenTransfer('0x...');
When you no longer need to track a specific token transfer, you can unsubscribe from it using the TokenBridge.stream.unsubscribeFromTokenTransfer
method with the same parameters used for subscribing with TokenBridge.stream.subscribeToTokenTransfer
. This ensures that your application doesn't continue to receive updates for that particular token transfer once it's no longer needed.
The TokenBridge.stream.subscribeToTokenTransfer
method is useful when you need to update the data of a specific token transfer in different parts of your code, such as within the UI or other components.
import { BridgeTokenTransferStatus, type BridgeTokenTransfer } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const handleTokenTransferUpdated = (tokenTransfer: BridgeTokenTransfer) => {
console.dir(tokenTransfer, { depth: null });
if (tokenTransfer.status === BridgeTokenTransferStatus.Finished) {
// If the token transfer is finished, unsubscribe from it as we no longer need to track it
tokenBridge.stream.unsubscribeFromTokenTransfer(tokenTransfer);
}
};
tokenBridge.addEventListener('tokenTransferCreated', handleTokenTransferUpdated);
tokenBridge.addEventListener('tokenTransferUpdated', handleTokenTransferUpdated);
// ...
const { tokenTransfer } = await tokenBridge.deposit(300_000n, { type: 'fa1.2', address: 'KT1GM2AnBAJWdzrChp3hTYFTSb6Dmh61peBP' });
tokenBridge.stream.subscribeToTokenTransfer(tokenTransfer);
import type { BridgeTokenTransfer } from '../../src';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const handleTokenTransferCreated = (tokenTransfer: BridgeTokenTransfer) => {
console.log('A new token transfer has been created:');
console.dir(tokenTransfer, { depth: null });
};
const handleTokenTransferUpdated = (tokenTransfer: BridgeTokenTransfer) => {
console.log('A token transfer has been updated:');
console.dir(tokenTransfer, { depth: null });
};
tokenBridge.addEventListener('tokenTransferCreated', handleTokenTransferCreated);
tokenBridge.addEventListener('tokenTransferUpdated', handleTokenTransferUpdated);
tokenBridge.stream.subscribeToAccountTokenTransfers(['tz1...', '0x...']);
import type { BridgeTokenTransfer } from '../../src';
// ...
// const tokenBridge: TokenBridge = ...
// ...
const handleTokenTransferCreated = (tokenTransfer: BridgeTokenTransfer) => {
console.log('A new token transfer has been created:');
console.dir(tokenTransfer, { depth: null });
};
const handleTokenTransferUpdated = (tokenTransfer: BridgeTokenTransfer) => {
console.log('A token transfer has been updated:');
console.dir(tokenTransfer, { depth: null });
};
tokenBridge.addEventListener('tokenTransferCreated', handleTokenTransferCreated);
tokenBridge.addEventListener('tokenTransferUpdated', handleTokenTransferUpdated);
tokenBridge.stream.subscribeToTokenTransfers();
// ...
// const tokenBridge: TokenBridge = ...
// ...
tokenBridge.stream.unsubscribeFromAllSubscriptions();
With the SDK, you can access useful data such as token transfers and token balances.
To receive all token transfers over the Etherlink bridge:
// ...
// const tokenBridge: TokenBridge = ...
// ...
const tokenTransfers = tokenBridge.data.getTokenTransfers();
Since the number of token transfers can be large, use the offset
and limit
parameters to specify the number of entries you want to load:
// ...
// const tokenBridge: TokenBridge = ...
// ...
// Load last 100 token transfers
let tokenTransfers = tokenBridge.data.getTokenTransfers(0, 100);
To receive token transfers for specific accounts:
// ...
// const tokenBridge: TokenBridge = ...
// ...
let tokenTransfers = tokenBridge.data.getAccountTokenTransfers(['tz1...']);
tokenTransfers = tokenBridge.data.getAccountTokenTransfers(['tz1...', '0x...']);
tokenTransfers = tokenBridge.data.getAccountTokenTransfers(['tz1...', 'tz1...', '0x...']);
You can also use the offset
and limit
parameters to specify the number of entries you want to load:
// ...
// const tokenBridge: TokenBridge = ...
// ...
// Load last 100 token transfers
let tokenTransfers = tokenBridge.data.getAccountTokenTransfers(['tz1...', '0x...'], 0, 100);
// skip the last 300 token transfers and load 50
tokenTransfers = tokenBridge.data.getAccountTokenTransfers(['tz1...', '0x...'], 300, 50);
To find a transfer by Tezos or Etherlink operation hash, use the getTokenTransfer
method:
// ...
// const tokenBridge: TokenBridge = ...
// ...
let tokenTransfer = tokenBridge.data.getTokenTransfer('o...');
tokenTransfer = tokenBridge.data.getTokenTransfer('0x...');