Zebec Bridge SDK
This sdk can be use to transfer assets across chains and to interact with the Zebec's xchain bridge smart contracts
for passing message from EVM chain to solana specially to utilize the features of Zebec Streaming and Zebec Multisig
Streaming protocol.
Note: Currently, streaming xchain assets is limited within the source chain, means the asset streamer and the asset
receiver must be of same chain despite of the assets being xchain.
Folders | Description |
---|
evm | Contains evm contract and client factory classes needed to interact with Zebec Evm bridge contract |
solana | Contains solana contract factory and client classes to interact with Zebec Solana bridge contract |
portalTransfer | Contains all the functions for operation of token transfer from EVM chains to Solana and vice versa |
utils | Contains all the necessary constants, and other utility functions. |
Usage
Deposit in Zebec Vault
To deposit token native to source evm chain, you must be attest the token beforehand in
solana chain. If the token is already an attested token imported from solana to the
source evm chain, you can proceed for depositing to zebec vault.
Deposit process in completed in two step:
- Migrate evm token to Proxy Account in solana through token portal
- Deposit token from Proxy Account to Zebec Vault.
1. Migrate token via Token Portal
import { CHAIN_ID_BSC, CHAIN_ID_SOLANA, tryNativeToUint8Array } from "@certusone/wormhole-sdk";
import {
getBridgeAddressForChain,
getTokenBridgeAddressForChain,
transferEvm,
ZebecSolBridgeClient,
} from "@lucoadam/zebec-wormhole-sdk";
import { ethers } from "ethers";
const evmSecretKey = "<your evm account private key>";
const url = "https://data-seed-prebsc-1-s1.binance.org:8545";
const provider = new ethers.providers.JsonRpcProvider(url);
const signer = new ethers.Wallet(evmSecretKey, provider);
const sourceChain = CHAIN_ID_BSC;
const targetChain = CHAIN_ID_SOLANA;
const depositor = signer.address;
const depositorU8Array = tryNativeToUint8Array(depositor, sourceChain);
const proxyAccount = ZebecSolBridgeClient.getXChainUserKey(depositorU8Array, sourceChain);
const tokenAddress = "0x14a8F6b7Df911c0067D973a16947df2d884f05db";
const amount = "0.1";
const transferReceipt = await transferEvm(
signer,
tokenAddress,
sourceChain,
amount,
targetChain,
proxyAccount.toString(),
"0.01",
);
After this, it takes some time for your token to reach solana chain. During this time, a vaa is created which is
then verified and signed by the wormhole validators called guardians. You can obtain the vaa in following way.
import {
getEmitterAddressEth,
getIsTransferCompletedSolana,
getSignedVAAWithRetry,
parseSequenceFromLogEth,
} from "@certusone/wormhole-sdk";
import {
getBridgeAddressForChain,
getTokenBridgeAddressForChain,
WORMHOLE_RPC_HOSTS,
} from "@lucoadam/zebec-wormhole-sdk";
const sequence = parseSequenceFromLogEth(transferReceipt, getBridgeAddressForChain(sourceChain));
const transferEmitterAddress = getEmitterAddressEth(getTokenBridgeAddressForChain(sourceChain));
const { vaaBytes: transferVaa } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
sourceChain,
transferEmitterAddress,
sequence,
);
The vaa then can be used to posted on solana chain and redeem the token transferred. Zebec provides specialized
relayer supporting certain tokens that automatically relay your tokens using small amount of fee so this part
may be optional. However, if you want to manually relay you can accomplish it in following way.
import { setDefaultWasm, postVaaSolanaWithRetry, redeemOnSolana } from "@certusone/wormhole-sdk";
import { Connection, Keypair } from "@solana/web3.js";
import { MAX_VAA_UPLOAD_RETRIES_SOLANA, SOLANA_HOST } from "@lucoadam/zebec-wormhole-sdk";
import bs58 from "bs58";
setDefaultWasm("node");
const connection = new Connection(SOLANA_HOST, "finalized");
const secretKey = "54dVEu8m4mcKdkWyGB3CjPz4FPpqoXff6cLf3wedg5jLaWMnV3YDGNTGfhK8zALzNVn8UQkcVvPteVKUUKxce35b";
const keypair = Keypair.fromSecretKey(bs58.decode(secretKey));
const signTransaction = async (transaction) => {
transaction.partialSign(keypair);
return transaction;
};
const payerAddress = keypair.publicKey.toString();
const bridgeAddress = getBridgeAddressForChain(targetChain);
const tokenBridgeAddress = getTokenBridgeAddressForChain(targetChain);
const vaaBuf = Buffer.from(transferVaa);
await postVaaSolanaWithRetry(
connection,
signTransaction,
bridgeAddress,
payerAddress,
vaaBuf,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
);
let unsignedTransaction = await redeemOnSolana(
connection,
bridgeAddress,
tokenBridgeAddress,
payerAddress,
signedVaaArray,
);
unsignedTransaction.partialSign(keypair);
const txid = await connection.sendRawTransaction(unsignedTransaction.serialize());
await connection.confirmTransaction(txid);
If vaa is supposed to be relayed and token is redeemed by a relayer, in that case you can check and wait for
token to be redeemed by the relayer in following way.
import { getIsTransferCompletedSolana } from "@certusone/wormhole-sdk";
let success = false;
let retry = 0;
do {
if (retry > 12) throw new Error("Transfer failed!");
retry++;
success = await getIsTransferCompletedSolana(tokenBridgeAddress, transferVaa, connection);
await new Promise((r) => setTimeout(r, 5000));
} while (!success);
console.log("transfer successful");
2. Deposit token to zebec vault
Depositing token from proxy accounts to zebec vault can be done in following way.
We first need to pass message to solana chain using evm bridge contracts for deposit tokens.
import { ZebecEthBridgeClient, BSC_ZEBEC_BRIDGE_ADDRESS, getTargetAsset } from "@lucoadam/zebec-wormhole-sdk";
const contractAddress = BSC_ZEBEC_BRIDGE_ADDRESS;
const tokenAddressSol = await getTargetAsset(signer, tokenAddress, CHAIN_ID_BSC, CHAIN_ID_SOLANA);
const zebecEthClient = new ZebecEthBridgeClient(contractAddress, signer, <EVMChainId>sourceChain);
const depositReceipt = await zebecEthClient.depositToken(amount, tokenAddressSol, depositor);
Then the vaa obtained after passing message is required to be posted in solana chain to create accounts associated
with vaa which then helps to verify and deposit token from proxy accounts to its associated vaults.
const depositSequence = parseSequenceFromLogEth(depositReceipt, getBridgeAddressForChain(sourceChain));
const emitterAddress = getEmitterAddressEth(BSC_ZEBEC_BRIDGE_ADDRESS);
const { vaaBytes: depositVaa } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
sourceChain,
emitterAddress,
depositSequence,
);
Zebec provides specialized spy relayer for relaying the message passed by its evm bridge contracts to solana bridge
program as well however, that doesn't imply that they cannot be relayed manually. The sdk provides required client
interfaces, factory classes and utililities for this. The spy relayer uses same sdk to relay the incomming message.
import { importCoreWasm } from "@certusone/wormhole-sdk";
import { parseZebecPayload, IsTokenDepositPayload, ZebecSolBridgeClient } from "@lucoadam/zebec-wormhole-sdk";
import * as anchor from "@project-serum/anchor";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
const depositVaaBuf = Buffer.from(depositVaa);
await postVaaSolanaWithRetry(
connection,
signTransaction,
bridgeAddress,
payerAddress,
depositVaaBuf,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
);
const { parse_vaa } = await importCoreWasm();
const parsedDepositVaa = await parse_vaa(depositVaa);
const depositPayload = parseZebecPayload(Buffer.from(parsedDepositVaa.payload));
if(!IsTokenDepositPayload(depositPayload)) {
throw new Error("Invalid payload")
}
const anchorProvider = new anchor.AnchorProvider(connection, new anchor.Wallet(keypair);
const options = {
skipPreflight: true,
commitment: "processed",
preflightCommitment: "processed",
};
const zebecSolClient = new ZebecSolBridgeClient(anchorProvider, options);
const depositResult = await zebecSolClient.depositToken(depositVaa, depositPayload);
if (depositResult.status === "success") {
console.log("Deposited successfully.");
} else {
throw new Error(depositResult.message);
}
One thing you need to make sure before interacting with zebec solana bridge program, you should have initialize
and register
the emitter address of xchain emitter (contracts), otherwise the bridge program cannot verify accounts
and data sent in the transaction with vaa emitted by the xchain emitter. You may use some methods in client to
do your job.
await zebecClient.initialize();
await zebecClient.registerEmitterAddress(parsedDepositVaa.emitter_address, parsedDepositVaa.emitter_chain);
Start Stream
const startTime = Math.floor(Date.now() / 1000) + 10);
const endTime = startTime + 20;
const sender = signer.address;
const receiver = "0x91845D534744Ef350695CF98393d23acC9639024";
const steamAmount = "10";
const canCancel = true;
const canUpdate = true;
const streamReceipt = zebecEthClient.startTokenStream(
startTime.toString(),
endTime.toString(),
steamAmount,
receiver,
sender,
canCanel,
canUpdate,
tokenAddressSol
);
The payload for stream is automatically relayed by specialized relayer but you need to relay manually, you may do as followed.
const streamSequence = parseSequenceFromLogEth(tx, BSC_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(BSC_ZEBEC_BRIDGE_ADDRESS);
const { vaaBytes: streamVaa } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
sourceChain,
emitterAddress,
streamSequence,
);
const streamVaaBuf = Buffer.from(streamVaa);
await postVaaSolanaWithRetry(
connection,
signTransaction,
bridgeAddress,
payerAddress,
streamVaaBuf,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
);
const parsedStreamVaa = await parse_vaa(depositVaa);
const streamPayload = parseZebecPayload(Buffer.from(parsedStreamVaa.payload));
const streamResult = await zebecSolClient.initializeStream(signedVaaArray, payload);
let dataAccount: string | undefined;
if (streamResult.status === "success") {
console.log("Stream started successfully");
dataAccount = streamResult.data?.dataAccount?.toString();
}
Withdraw Stream
Stream Data account (Stream Escrow account) is unique for each stream and is
supposed to be selected from the all of stream data that are associated the
sender and receiver. And user may select the stream they had just streamed
similar to flow of CRUD app where SelectAll and Select One is performed to get
unique stream. For this example let's use the same dataAccount that we cached in a variable.
if (!dataAccount) {
throw new Error("Stream Data Account is undefined. May be stream was not success");
}
const withdrawStreamReceipt = await messengerContract.withdrawFromTokenStream(
sender,
receiver,
tokenAddressSol,
dataAccount,
);
For relayer part, to relay the message to solana:
const withdrawStreamSequence = parseSequenceFromLogEth(tx, BSC_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(BSC_ZEBEC_BRIDGE_ADDRESS);
const { vaaBytes: withdrawStreamVaa } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
sourceChain,
emitterAddress,
streamSequence,
);
const withdrawStreamVaaBuf = Buffer.from(withdrawStreamVaa);
await postVaaSolanaWithRetry(
connection,
signTransaction,
bridgeAddress,
payerAddress,
streamVaaBuf,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
);
const parsedWithdrawStream = await parse_vaa(withdrawStreamVaa);
const withdrawStreamPayload = parseZebecPayload(Buffer.from(parsedWithdrawStream.payload));
const streamResult = await zebecSolClient.initializeStream(signedVaaArray, payload);
let dataAccount: string | undefined;
if (streamResult.status === "success") {
console.log("Stream started successfully");
dataAccount = streamResult.data?.dataAccount?.toString();
}