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:
- Deposit native (XTZ) and FA tokens from Tezos to Etherlink.
- Withdraw native and ERC20 tokens from Etherlink back to Tezos.
- Retrieve information on all token transfers (deposits and withdrawals) or by operation/transaction hash, or addresses.
- Receive actual token balances for specific addresses.
- Obtain real-time updates on existing or new token transfers for specific addresses or all transfers.
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/ghostnet';
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/ghostnet';
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);
import { ethers } from 'ethers';
// Use MetaMask
const provider = new ethers.BrowserProvider(window.ethereum)
const signer = await provider.getSigner();
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:
Example token pair configuration code
import type { TokenPair } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const tokenPairs: TokenPair[] = [
// Native
{
tezos: {
type: 'native',
ticketHelperContractAddress: 'KT1VEjeQfDBSfpDH5WeBM5LukHPGM2htYEh3',
},
etherlink: {
type: 'native',
}
},
// tzBTC
{
tezos: {
type: 'fa1.2',
address: 'KT1HmyazXfKDbo8XjwtWPXcoyHcmNPDCvZyb',
ticketerContractAddress: 'KT1H7if3gSZE1pZSK48W3NzGpKmbWyBxWDHe',
ticketHelperContractAddress: 'KT1KUAaaRMeMS5TJJyGTQJANcpSR4egvHBUk',
},
etherlink: {
type: 'erc20',
address: '0x8e73aE3CF688Fbd8368c99520d26F9eF1B4d3BCa',
}
},
// USDt
{
tezos: {
type: 'fa2',
address: 'KT1V2ak1MfNd3w4oyKD64ehYU7K4CrpUcDGR',
tokenId: '0',
ticketerContractAddress: 'KT1S6Nf9MnafAgSUWLKcsySPNFLUxxqSkQCw',
ticketHelperContractAddress: 'KT1JLZe4qTa76y6Us2aDoRNUgZyssSDUr6F5',
},
etherlink: {
type: 'erc20',
address: '0xf68997eCC03751cb99B5B36712B213f11342452b',
}
}
];
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.
import {
DefaultDataProvider,
TokenBridge,
TaquitoWalletTezosBridgeBlockchainService,
Web3EtherlinkBridgeBlockchainService,
} from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const defaultDataProvider = new DefaultDataProvider({
tokenPairs,
dipDup: {
baseUrl: 'https://testnet.bridge.indexer.etherlink.com',
webSocketApiBaseUrl: 'wss://testnet.bridge.indexer.etherlink.com'
},
tzKTApiBaseUrl: 'https://api.ghostnet.tzkt.io',
etherlinkRpcUrl: 'https://node.ghostnet.etherlink.com',
})
const tokenBridge = new TokenBridge({
tezosBridgeBlockchainService: new TaquitoWalletTezosBridgeBlockchainService({
tezosToolkit,
smartRollupAddress: 'sr18wx6ezkeRjt1SZSeZ2UQzQN3Uc3YLMLqg'
}),
etherlinkBridgeBlockchainService: new Web3EtherlinkBridgeBlockchainService({
web3
}),
// or for ethers
// etherlinkBridgeBlockchainService: new EthersEtherlinkBridgeBlockchainService({
// ethers,
// signer
// }),
bridgeDataProviders: {
transfers: defaultDataProvider,
balances: defaultDataProvider,
tokens: defaultDataProvider,
}
});
The entire code
import {
DefaultDataProvider,
TokenBridge,
TaquitoWalletTezosBridgeBlockchainService,
Web3EtherlinkBridgeBlockchainService,
type TokenPair
} from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const tokenPairs: TokenPair[] = [
// Native
{
tezos: {
type: 'native',
ticketHelperContractAddress: 'KT1VEjeQfDBSfpDH5WeBM5LukHPGM2htYEh3',
},
etherlink: {
type: 'native',
}
},
// tzBTC
{
tezos: {
type: 'fa1.2',
address: 'KT1HmyazXfKDbo8XjwtWPXcoyHcmNPDCvZyb',
ticketerContractAddress: 'KT1H7if3gSZE1pZSK48W3NzGpKmbWyBxWDHe',
ticketHelperContractAddress: 'KT1KUAaaRMeMS5TJJyGTQJANcpSR4egvHBUk',
},
etherlink: {
type: 'erc20',
address: '0x8e73aE3CF688Fbd8368c99520d26F9eF1B4d3BCa',
}
},
// USDt
{
tezos: {
type: 'fa2',
address: 'KT1V2ak1MfNd3w4oyKD64ehYU7K4CrpUcDGR',
tokenId: '0',
ticketerContractAddress: 'KT1S6Nf9MnafAgSUWLKcsySPNFLUxxqSkQCw',
ticketHelperContractAddress: 'KT1JLZe4qTa76y6Us2aDoRNUgZyssSDUr6F5',
},
etherlink: {
type: 'erc20',
address: '0xf68997eCC03751cb99B5B36712B213f11342452b',
}
}
];
const defaultDataProvider = new DefaultDataProvider({
dipDup: {
baseUrl: 'https://testnet.bridge.indexer.etherlink.com',
webSocketApiBaseUrl: 'wss://testnet.bridge.indexer.etherlink.com'
},
tzKTApiBaseUrl: 'https://api.ghostnet.tzkt.io',
etherlinkRpcUrl: 'https://node.ghostnet.etherlink.com',
tokenPairs
})
const tokenBridge = new TokenBridge({
tezosBridgeBlockchainService: new TaquitoWalletTezosBridgeBlockchainService({
tezosToolkit: tezosToolkit,
smartRollupAddress: 'sr18wx6ezkeRjt1SZSeZ2UQzQN3Uc3YLMLqg'
}),
etherlinkBridgeBlockchainService: new Web3EtherlinkBridgeBlockchainService({
web3
}),
bridgeDataProviders: {
transfers: defaultDataProvider,
balances: defaultDataProvider,
tokens: defaultDataProvider,
}
});
Your application is ready to deposit/withdraw user tokens and receive their corresponding transfers.
There are two types of token transfers:
- Deposit: This is a token transfer from Tezos (L1) to Etherlink (L2). The transfer requires only one operation in Tezos.
- Withdrawal: This is a token transfer from Etherlink (L2) to Tezos (L1). The transfer occurs in two stages:
- Call the
withdraw
method of the Etherlink kernel (sending a transaction in Etherlink). - Call the outbox message of Tezos rollup when the corresponding commitment is cemented (sending an operation in Tezos).
- Call the
To deposit tokens, use the asynchronous TokenBridge.deposit
method, passing the amount in raw type (not divided by token decimals) and the Tezos token.
The method returns an object that includes two fields:
tokenTransfer
: Represents the bridge transfer withBridgeTokenTransferKind.Deposit
type andBridgeTokenTransferStatus.Pending
status.operationResult
: 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 tzbtcTezosToken: FA12TezosToken = {
type: 'fa1.2',
address: 'KT1HmyazXfKDbo8XjwtWPXcoyHcmNPDCvZyb',
};
// Deposit 0.01 tzBTC (8 decimals) to Etherlink (L2)
const { tokenTransfer, operationResult } = await tokenBridge.deposit(1_000_000n, tzbtcTezosToken);
console.dir(tokenTransfer, { depth: null });
The Deposit transfer has the following statuses (BridgeTokenTransferStatus
):
- Pending [Code: 0]: The deposit operation has been sent to the Tezos blockchain but not confirmed yet.
- Created [Code: 100]: The deposit operation has been confirmed on Tezos.
- Finished [Code: 300]: The Etherlink transaction has been created and confirmed, successfully completing the deposit.
- Failed [Code: 400]: The deposit failed for any reason.
After initiating a deposit by sending a Tezos operation, you need a way to track its progress and completion. The SDK offers two approaches:
-
Stream API and Events. See details.
-
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 tzbtcTezosToken: FA12TezosToken = { type: 'fa1.2', address: 'KT1HmyazXfKDbo8XjwtWPXcoyHcmNPDCvZyb', }; // Deposit 0.01 tzBTC (8 decimals) to Etherlink (L2) const { tokenTransfer, operationResult } = await tokenBridge.deposit(1_000_000n, tzbtcTezosToken); 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, operationResult } = await tokenBridge.deposit(1_000_000n, tzbtcTezosToken, 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 withdrawing tokens, call the asynchronous
TokenBridge.startWithdraw
method, passing the amount in raw type (not divided by token decimals) and the Etherlink token. The method returns an object that includes two fields:tokenTransfer
: Represents the bridge transfer withBridgeTokenTransferKind.Withdrawal
type andBridgeTokenTransferStatus.Pending
status.operationResult
: 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 tzbtcEtherlinkToken: ERC20EtherlinkToken = { type: 'erc20', address: '0x8e73aE3CF688Fbd8368c99520d26F9eF1B4d3BCa' }; // Withdraw 0.01 tzBTC (8 decimals) from Etherlink (L2) // The first stage const { tokenTransfer, operationResult } = await tokenBridge.startWithdraw(1_000_000n, tzbtcEtherlinkToken); console.dir(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
andproof
) needed for completion.To complete the withdrawal, call the asynchronous
TokenBridge.finishWithdraw
method, passing the transfer withBridgeTokenTransferStatus.Sealed
status. The method returns an object that includes two fields:tokenTransfer
: Represents the bridge transfer withBridgeTokenTransferKind.Withdrawal
type andBridgeTokenTransferStatus.Sealed
status.operationResult
: 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, operationResult } = await tokenBridge.finishWithdraw(sealedBridgeTokenWithdrawal);
The Withdrawal transfer has the following statuses (BridgeTokenTransferStatus
):
- Pending [Code: 0]: The withdrawal transaction has been sent to the Etherlink blockchain but not confirmed yet.
- Created [Code: 100]: The withdrawal transaction has been confirmed on Etherlink.
- Sealed [Code: 200]: The withdrawal is ready for the second stage: the corresponding commitment is cemented, and an outbox message is ready for execution.
- Finished [Code: 300]: The Tezos transaction for executing the outbox message has been created and confirmed, successfully completing the withdrawal.
- Failed [Code: 400]: The withdrawal process failed for any reason.
Similar to deposits, the SDK offers two approaches to track the withdrawal progress:
-
Stream API and Events. See details.
-
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 tzbtcEtherlinkToken: ERC20EtherlinkToken = { type: 'erc20', address: '0x8e73aE3CF688Fbd8368c99520d26F9eF1B4d3BCa' }; // Withdraw 0.01 tzBTC (8 decimals) from Etherlink (L2) // The first stage const { tokenTransfer, operationResult } = await tokenBridge.startWithdraw(1_000_000n, tzbtcEtherlinkToken); console.dir(tokenTransfer, { depth: null }); // Wait until the withdrawal status is Sealed const sealedBridgeTokenWithdrawal = await tokenBridge.waitForStatus( 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:- Created locally within your application (initiated through
TokenBridge.deposit
orTokenBridge.startWithdraw
). - Discovered by the data provider (indicating a new transfer on the bridge).
ℹ️ 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. - Created locally within your application (initiated through
-
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.subscribeToOperationTokenTransfers
method allows you to subscribe to real-time updates of a specific token transfer by providing its token transfer object or operation/transaction hash:
-
By token transfer object:
const tokenTransfer: BridgeTokenTransfer = // ... tokenBridge.stream.subscribeToOperationTokenTransfers(tokenTransfer);
-
By operation/transaction hash:
// Subscribe to token transfer by Tezos operation hash tokenBridge.stream.subscribeToOperationTokenTransfers('o...'); // Subscribe to token transfer by Etherlink transaction hash tokenBridge.stream.subscribeToOperationTokenTransfers('0x...');
When you no longer need to track a specific token transfer operation, you can unsubscribe from it using the TokenBridge.stream.unsubscribeFromOperationTokenTransfers
method with the same parameters used for subscribing with TokenBridge.stream.subscribeToOperationTokenTransfers
. 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.subscribeToOperationTokenTransfers
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.unsubscribeFromOperationTokenTransfers(tokenTransfer);
}
};
tokenBridge.addEventListener('tokenTransferCreated', handleTokenTransferUpdated);
tokenBridge.addEventListener('tokenTransferUpdated', handleTokenTransferUpdated);
// ...
const { tokenTransfer } = await tokenBridge.deposit(300_000n, { type: 'fa1.2', address: 'KT1HmyazXfKDbo8XjwtWPXcoyHcmNPDCvZyb' });
tokenBridge.stream.subscribeToOperationTokenTransfers(tokenTransfer);
import type { BridgeTokenTransfer } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// 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 '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
// 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 = await 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
const tokenTransfers = await tokenBridge.data.getTokenTransfers({ offset: 0, limit: 100 });
To receive token transfers for specific accounts:
// ...
// const tokenBridge: TokenBridge = ...
// ...
let tokenTransfers = await tokenBridge.data.getAccountTokenTransfers('tz1...');
tokenTransfers = await tokenBridge.data.getAccountTokenTransfers(['tz1...', '0x...']);
tokenTransfers = await 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 = await tokenBridge.data.getAccountTokenTransfers(['tz1...', '0x...'], { offset: 0, limit: 100 });
// skip the last 300 token transfers and load 50
tokenTransfers = await tokenBridge.data.getAccountTokenTransfers(['tz1...', '0x...'], { offset: 300, limit: 50 });
To find a transfer by Tezos or Etherlink operation hash, use the getOperationTokenTransfers
method:
// ...
// const tokenBridge: TokenBridge = ...
// ...
let tokenTransfers = await tokenBridge.data.getOperationTokenTransfers('o...');
tokenTransfers = await tokenBridge.data.getOperationTokenTransfers('0x...');