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>";
// bsc testnet url used for example
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);
/**
* Note: Signer can be any object instance that extends 'ethers.Signer' of 'ethers' package;
* For frontend app, signer is provided by wallet 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);
/*
* This is a wrapped token attested in bsc for a spltoken '6XSp58Mz6LAi91XKenjQfj9D1MxPEGYtgBkggzYvE8jY'
**/
const tokenAddress = "0x14a8F6b7Df911c0067D973a16947df2d884f05db";
const amount = "0.1";
// transfer from evm to solana
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.
// append imports as needed
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.
// append imports as needed
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"); // use 'bundler' if in browser
const connection = new Connection(SOLANA_HOST, "finalized");
/**
* For frontend app, signTransaction function is provided by solana wallet provider, so no need to manually make one.
**/
const secretKey = "54dVEu8m4mcKdkWyGB3CjPz4FPpqoXff6cLf3wedg5jLaWMnV3YDGNTGfhK8zALzNVn8UQkcVvPteVKUUKxce35b"; // base58 secret key
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);
// posting vaa in solana
await postVaaSolanaWithRetry(
connection,
signTransaction,
bridgeAddress,
payerAddress,
vaaBuf,
MAX_VAA_UPLOAD_RETRIES_SOLANA,
);
// redeeming token
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.
// append imports as needed
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);
// Create evm client instance
const zebecEthClient = new ZebecEthBridgeClient(contractAddress, signer, <EVMChainId>sourceChain);
// deposit token
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.
// append imports as needed
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);
// posting vaa in solana
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));
// type guard
if(!IsTokenDepositPayload(depositPayload)) {
throw new Error("Invalid payload")
}
/**
* Use solana wallet context if in browser for the wallet. In case you need to use anchor wallet,
* make sure you have set 'ANCHOR_BROWSER' = false in your env file.
*/
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); // cast to deposit payload
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.
// initialize program
await zebecClient.initialize();
// register xchain emitters
await zebecClient.registerEmitterAddress(parsedDepositVaa.emitter_address, parsedDepositVaa.emitter_chain);
Start Stream
// startTime set to 5 sec later than current timestamp
const startTime = Math.floor(Date.now() / 1000) + 10);
// endTime set to 20 sec after startTime
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);
// posting vaa in solana
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); // cast token stream 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.
// we know it is not undefined but you may do this just to guard undefined type.
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);
// posting vaa in solana
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();
}