diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 00000000..e69de29b diff --git a/specs/obridge/SUMMARY.md b/specs/obridge/SUMMARY.md new file mode 100644 index 00000000..e69de29b diff --git a/specs/obridge/background.md b/specs/obridge/background.md new file mode 100644 index 00000000..30711e01 --- /dev/null +++ b/specs/obridge/background.md @@ -0,0 +1,49 @@ +## Introduction + +**Obridge** is a cross-chain solution developed by **Oraichain**. It is designed to create a trustless bridge that enables the seamless transfer of assets between Oraichain and various blockchain ecosystems. Obridge supports EVM-compatible chains such as **Tron, BNB Chain, Ethereum**, as well as **XRPL, Bitcoin, Ton**, and the **Cosmos Ecosystem**. This innovative bridge ensures secure and efficient cross-chain communication, providing a decentralized way to move assets across different networks without relying on trusted intermediaries. + +![Obridge Connecting Oraichain to Various Blockchains](./image/obridge_universal.png) + +### How Obridge Works + +1. [Gravity Bridge](https://github.com/oraichain/Gravity-Bridge): OBridge is built on top of the Gravity Bridge, utilizing its robust framework to enable secure and efficient asset transfers between EVM-compatible networks like Ethereum, BNB Chain, and TRON, and the Cosmos ecosystem. + +2. [Inter-Blockchain Communication (IBC)](https://github.com/oraichain/ibc-bridge-wasm): OBridge integrates the IBC protocol to ensure seamless asset transfers within the Cosmos ecosystem. This integration maintains security and efficiency without the need for trusted intermediaries, allowing for a more decentralized and trustless environment. + +3. [Ton IBC](https://github.com/oraichain/tonbridge-cw-contracts): OBridge establishes an IBC bridge between Ton and Oraichain, leveraging IBC technology to facilitate secure and trustless cross-chain communication and asset transfers. + +4. [Cw-Bitcoin](https://github.com/oraichain/cw-bitcoin): The Bitcoin trustless bridge within OBridge is implemented using CosmWasm contracts. This bridge facilitates the use of Taproot Bitcoin scripts, enabling secure and decentralized Bitcoin transfers within the Oraichain ecosystem. + +5. [XRPL Bridge](https://github.com/oraichain/cw-xrpl-bridge): OBridge employs a multi-signing account on the XRPL for its bridge. This account holds tokens issued on the XRPL and manages them according to the specific workflow. Depending on the scenario, it either uses the received token balance to send tokens to XRPL accounts (if the account is not an issuer) or mints and sends tokens to XRPL accounts. Multi-signing and public keys associated with each relayer from the contract are used for transaction signing, ensuring security and trustlessness. + +### Supported Chains + +We currently support the following blockchain platforms: + +- **Cosmos-Based Chains:** + + - Cosmoshub + - Oraichain + - Osmosis + - Injective + - Noble + +- **EVM-Based Chains:** + + - Ethereum + - BNB Chain + - Tron + +- **XRP Ledger** + +- **TON - Telegram Open Network** + +- **Bitcoin** + +### Integration + +- [Orai - EVM](./orai-evm.md) +- [Orai - Cosmos](./orai-cosmos.md) +- [Orai - Bitcoin]() +- [Orai - TON](./orai-ton.md) +- [Orai - XRPL]() diff --git a/specs/obridge/image/evm_to_orai.png b/specs/obridge/image/evm_to_orai.png new file mode 100644 index 00000000..e7c0a4cd Binary files /dev/null and b/specs/obridge/image/evm_to_orai.png differ diff --git a/specs/obridge/image/obridge_universal.png b/specs/obridge/image/obridge_universal.png new file mode 100644 index 00000000..ad09dff1 Binary files /dev/null and b/specs/obridge/image/obridge_universal.png differ diff --git a/specs/obridge/image/orai_to_evm.png b/specs/obridge/image/orai_to_evm.png new file mode 100644 index 00000000..1542164c Binary files /dev/null and b/specs/obridge/image/orai_to_evm.png differ diff --git a/specs/obridge/image/orai_to_ton.png b/specs/obridge/image/orai_to_ton.png new file mode 100644 index 00000000..b3dfc65b Binary files /dev/null and b/specs/obridge/image/orai_to_ton.png differ diff --git a/specs/obridge/image/ton_to_orai.png b/specs/obridge/image/ton_to_orai.png new file mode 100644 index 00000000..de48640a Binary files /dev/null and b/specs/obridge/image/ton_to_orai.png differ diff --git a/specs/obridge/orai-cosmos.md b/specs/obridge/orai-cosmos.md new file mode 100644 index 00000000..e69de29b diff --git a/specs/obridge/orai-evm.md b/specs/obridge/orai-evm.md new file mode 100644 index 00000000..5df3f510 --- /dev/null +++ b/specs/obridge/orai-evm.md @@ -0,0 +1,175 @@ +# Oraichain <=> EVM + +## Overview + +The Oraichain-EVM bridge leverages GravityBridge and IBC Cw20-Ics 20 to enable seamless asset transfers between Oraichain and EVM chains. + +| Name | Chain | Address | +| --------------------- | --------- | --------------------------------------------------------------- | +| Ibc Bridge Wasm | Oraichain | orai195269awwnt5m6c843q6w7hp8rt0k7syfu9de4h0wz384slshuzps8y7ccm | +| OBridge Contract ETH | Ethereum | 0x09Beeedf51AA45718F46837C94712d89B157a9D3 | +| OBridge Contract BSC | BNB Chain | 0xb40C364e70bbD98E8aaab707A41a52A2eAF5733f | +| OBridge contract Tron | Tron | 0x73Ddc880916021EFC4754Cb42B53db6EAB1f9D64 | + +## Workflows + +### Send from EVM to Oraichain + +![EVM to Oraichain](./image/evm_to_orai.png) + +### Send from Oraichain to EVM + +![Oraichain to EVM](./image/orai_to_evm.png) + +## Integration + +### EVM to Oraichain + +```solidity +interface IGravity { + event SendToCosmosEvent(address indexed _tokenContract, address indexe _sender, string _destination, uint256 _amount, uint256 _eventNonce); + + function sendToCosmos(address _tokenContract, string calldata _destination, uint256 _amount) external; +} + +``` + +To bridge tokens from EVM to Oraichain using OBridge, follow these steps: + +- Increase allowance for OBridge contract address: Ensure the Gravity contract has the necessary allowance to transfer the tokens +- Call `sendToCosmos` function in OBridge Contract ETH(BSC, TRON): + +**Parameters**: + +- `_tokenContract` : The address of the token want to bridge. +- `_destination`: A string combining `${sourceChannel}/${destReceiver}:${memo}`, where **sourceChannel** is the bridge channel between OraiBridge and Oraichain, **destReceiver** is the recipient's address on Oraichain, and [memo](./universal-swap-memo.md) used for UniversalSwap +- \_amount: The amount of tokens to bridge. + +**Example**: Bridge Orai from the BNB Chain to Oraichain, swaps it for USDC, and then bridges the USDC to Ethereum. + +- Init EVM wallet + +```ts +import { EvmWallet } from "@oraichain/oraidex-common"; +import { Web3Provider } from "@ethersproject/providers"; +import { ethers } from "ethers"; + +window.tronWebDapp = walletType === "owallet" ? window.tronWeb_owallet : window.tronWeb; +window.tronLinkDapp = walletType === "owallet" ? window.tronLink_owallet : window.tronLink; +window.ethereumDapp = walletType === "owallet" ? window.eth_owallet : window.ethereum; + +export default class Evm extends EvmWallet { + private provider: Web3Provider; + + public checkEthereum() { + if (window.ethereumDapp) return true; + return false; + } + + public getSigner() { + if (!this.provider) this.provider = new ethers.providers.Web3Provider(window.ethereumDapp, "any"); + return this.provider.getSigner(); + } + + public isWindowEthereum() { + return !!window.ethereumDapp; + } + + public isTron(chainId: string | number) { + return Number(chainId) == Networks.tron; + } + + public checkTron() { + if (window.tronWebDapp && window.tronLinkDapp) return true; + return false; + } + + public async switchNetwork(chainId: string | number) { + if (this.checkEthereum()) { + await window.ethereumDapp.request!({ + method: "wallet_switchEthereumChain", + params: [{ chainId: "0x" + Number(chainId).toString(16) }] + }); + } + } + + public async getEthAddress() { + if (this.checkEthereum()) { + const [address] = await window.ethereumDapp.request({ + method: "eth_requestAccounts", + params: [] + }); + return address; + } + } +} +``` + +- Bridge: + +```ts +import { Bridge__factory } from "@oraichain/oraidex-common"; + +const gravityContractAddr = "0xb40C364e70bbD98E8aaab707A41a52A2eAF5733f"; +const oraiBscToken = "0xa325ad6d9c92b55a3fc5ad7e412b1518f96441c0"; +const destination = + "channel-1/orai1hvr9d72r5um9lvt0rpkd4r75vrsqtw6yujhqs2:CqMBCgdvcmFpZGV4GpcBCpQBCgczMDAwMDAwEogBCj9vcmFpMTl0dGcwajd3NWtyODNqczMydG13bnd4eGRxOXJrbXc0bTNkN21uMmoyaGtwdWd3d2E0dHN6d3Nua2cSBG9yYWkaP29yYWkxNXVuOG1zeDNuNXpmOWFobHhtZmVxZDJrd2E1d20wbnJweGVyMzA0bTluZDVxNnFxMGc2c2t1NXBkZBIIMTA2NTg4ODUYgN7v8N7wgvUXIokBEoYBCgpjaGFubmVsLTI5EjVldGgtbWFpbm5ldDB4OGM3RTBBODQxMjY5YTAxYzBBYjM4OUNlOEZiM0NmMTUwQTk0RTc5Nxo1ZXRoLW1haW5uZXQweEEwYjg2OTkxYzYyMThiMzZjMWQxOUQ0YTJlOUViMGNFMzYwNmVCNDgggISi7d3wgvUXKgAqK29yYWkxaHZyOWQ3MnI1dW05bHZ0MHJwa2Q0cjc1dnJzcXR3Nnl1amhxczI="; +const amount = "2024699112000000000000"; +const from = "0x655169CAc525eC903cd11F8690211C134aeD1C0F"; + +const gravityContract = Bridge__factory.connect(gravityContractAddr, evmWallet.getSigner()); +const result = await gravityContract.sendToCosmos(oraiBscToken, destination, amountVal, { from }); +``` + +### Oraichain to EVM + +`Bridging Tokens Using IbcBridgeWasm Contract` + +**1. Transfer Native Token** + +To bridge native tokens from Oraichain using the IbcBridgeWasm contract, you need to call the `TransferToRemote` message. Here’s how to do it: + +- Prepare the `TransferToRemote` message: + + - **local_channel_id**: The IBC channel on Oraichain for sending tokens. + - **remote_address**: The bech32 address of the recipient on the remote chain (Obridge). + - **remote_denom**: The denomination of the token on the remote chain, identified from the mapping pair (e.g., `oraib0x...` or `eth-mainnet0x...`). + - **timeout**: (Optional) The duration in seconds for how long the packet should live. If not specified, it defaults to `default_timeout`. + - **memo**: The destination address, which must contain the prefix of the destination chain (e.g., `eth-mainnet0x...` or `oraib0x...`). + +- **Call the IbcBridgeWasm Contract**: + - Ensure the corresponding funds match the `remote_denom` in the mapping pair. + +**2. Transfer Cw20** +To bridge CW20 tokens from Oraichain to a chain use the CW20 Send function along with the TransferToRemote message + +**Example** +Transfer Orai from Oraichain to BNB chain + +```ts +import { GasPrice, SigningStargateClient } from "@cosmjs/stargate"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import dotenv from "dotenv"; +import { ORAI } from "@oraichain/common"; +import { Cw20Ics20Client } from "@oraichain/common-contracts-sdk"; +import { TransferBackMsg } from "@oraichain/common-contracts-sdk/build/CwIcs20Latest.types"; +import { CwIcs20LatestClient } from "@oraichain/common-contracts-sdk"; + +const signer = await DirectSecp256k1HdWallet.fromMnemonic(process.env.EXAMPLES_MNEMONIC, { prefix: ORAI }); // replace your mnemonic here +const accounts = await signer.getAccounts(); +const address = accounts[0].address; +const client = await SigningStargateClient.connectWithSigner("https://rpc.orai.io", signer, { + gasPrice: GasPrice.fromString("0.001orai") +}); + +const ics20Contract = "orai195269awwnt5m6c843q6w7hp8rt0k7syfu9de4h0wz384slshuzps8y7ccm"; +const cwIcs20Client = new CwIcs20LatestClient(client, sender, ics20Contract); + +const result = await cwIcs20Client.transferToRemote({ + local_channel_id: "channel-29", + remote_address: "oraib1hvr9d72r5um9lvt0rpkd4r75vrsqtw6ytnnvpf", + remote_denom: "oraib0xA325Ad6D9c92B55A3Fc5aD7e412B1518F96441C0", + timeout: "1724320979000000000", + memo: "oraib0x8ae1874f2f9f26eeecf1855adec19f530e3cdeaa" +}); +``` diff --git a/specs/obridge/orai-ton.md b/specs/obridge/orai-ton.md new file mode 100644 index 00000000..e5733eb8 --- /dev/null +++ b/specs/obridge/orai-ton.md @@ -0,0 +1,425 @@ +# Oraichain <=> TON + +# Overview + +The Ton Bridge facilitates the transfer of tokens between the Ton blockchain and Oraichain, as well as other blockchains within the Cosmos ecosystem. + +## Ton Contract + +| Name | Address | +| -------------- | ------------------------------------------------ | +| Light Client | EQDzy_POlimFDyzrHd3OQsb9sZCngyG3O7Za4GRFzM-rrO93 | +| Bridge Adapter | EQC-aFP0rJXwTgKZQJPbPfTSpBFc8wxOgKHWD9cPvOl_DnaY | + +## Oraichain Contract + +| Name | Address | +| -------------- | --------------------------------------------------------------- | +| Light Client | orai159l8l9c5ckhqpuwdfgs9p4v599nqt3cjlfahalmtrhfuncnec2ms5mz60e | +| Bridge Adapter | orai16crw7g2rcvuga7vlnyxgwtdxtan46k8qqjjwhjqdjvjgk96n95es35q8vm | + +# Workflows: + +## Send from Ton to Oraichain + +Ton to Oraichain + + +## Send from Oraichain to Ton + +![Oraichain to Ton](./image/orai_to_ton.png) + +# Integration + +## Ton to Oraichain + +### Bridging Tokens from Ton to Oraichain + +You can bridge two types of tokens from Ton to Oraichain: + +1. **Ton (Native) Tokens**: This includes the native Ton token. +2. **Jetton Tokens**: This includes tokens like USDT, USDC, and other Jetton-based tokens. + +```ts +const TON_NATIVE = "ton"; +type JettonMasterAddress = string; +export type TonDenom = typeof TON_NATIVE | JettonMasterAddress; + +interface ITonBridgeHandler { + sendToCosmos( + cosmosRecipient: string, + amount: bigint, + denom: TonDenom, + opts: ValueOps, + timeoutTimestamp: bigint = BigInt(calculateTimeoutTimestamp(3600)), + memo: string = "" + ): Promise; +} +``` + +**Notice**: + +- If you send native ton to Oraichain, the message will send directly to **Bridge Adapter** contract. +- If you send jetton token to Oraichain, the message will send to **Jetton Wallet** contract first then it will be forwarded to **Bridge Adapter** contract after some messages. + +**Parameters**: + +- `cosmosRecipient` : recipient address on cosmos destination chain. +- `amount`: amount of token that you want to send. +- `denom`: denom of the token, which will decide it is **bridge_ton** or **bridge_jetton_token**. +- `opts`: configuration of **total ton amount** that you want to send along with the messages and the **query id** of that message. +- `timeoutTimestamp`: timeout timestamp of packet. +- `memo`: [memo](./universal-swap-memo.md) is used for UniversalSwap which will be executed when the token reached to Oraichain. + +**Example:** + +Here we use `@oraichain/tonbridge-sdk` for interaction. + +- Ton Wallet Class: + +```ts +## Ref: "https://github.com/oraichain/tonbridge-sdk/blob/main/packages/bridge-sdk/src/wallet.ts#L19" +class TonWallet { + public constructor( + public readonly tonClient: TonClient, + public readonly sender: Sender, + public readonly publicKey: Buffer + ) {} + + async waitSeqno( + seqno: number, + walletVersion: TonWalletVersion, + network: Network = "mainnet", + workchain = MAIN_WORKCHAIN + ) { + const walletContract = TonWallet.createWalletContractFromPubKey( + walletVersion, + this.publicKey, + workchain, + network + ); + const contract = this.tonClient.open(walletContract); + let currentSeqno = seqno; + while (currentSeqno == seqno) { + console.log("waiting for transaction to confirm..."); + await new Promise((resolve) => + setTimeout(resolve, SLEEP_TIME.WAIT_SEQNO) + ); + const seqno = await contract.getSeqno(); + currentSeqno = seqno; + } + console.log("transaction confirmed!"); + } + + static async create( + network: Network, + { + mnemonicData: { mnemonic, tonWalletVersion, workchain }, + tonConnector, + }: { + mnemonicData?: { + mnemonic: string[]; + tonWalletVersion: TonWalletVersion; + workchain?: number; + }; + tonConnector?: TonConnectUI; + } + ) { + const endpoint = await getHttpEndpoint({ network }); + const client = new TonClient({ endpoint }); + if (!mnemonic && !tonConnector) { + throw new Error( + "Need at least mnemonic or TonConnector to initialize the TON Wallet" + ); + } + let tonSender: Sender; + let tonPublicKey: Buffer; + if (mnemonic) { + const { publicKey, secretKey } = await this.getWalletFromMnemonic( + mnemonic + ); + tonPublicKey = publicKey; + const wallet = this.createWalletContractFromPubKey( + tonWalletVersion, + tonPublicKey, + workchain, + network + ); + + const walletContract = client.open(wallet); + tonSender = { + address: walletContract.address, + ...walletContract.sender(secretKey), + }; + } + if (tonConnector) { + const { sender, account } = this.getWalletFromConnector(tonConnector); + if (!account.publicKey) + throw new Error("TonConnector account does not have public key!"); + tonSender = sender; + tonPublicKey = Buffer.from(account.publicKey, "base64"); + } + if (!tonPublicKey) throw new Error("TON public key is null"); + return new TonWallet(client, tonSender, tonPublicKey); + } + ... +} +``` + +- Cosmos Wallet Class: + +```ts +# Ref: "https://github.com/oraichain/oraidex-sdk/blob/main/packages/oraidex-common/src/wallet.ts#L20" +abstract class CosmosWallet { + /** + * This method should return the cosmos address in bech32 form given a cosmos chain id + * Browsers should make use of the existing methods from the extension to implement this method + * @param chainId - Cosmos chain id to parse and return the correct cosmos address + */ + public abstract getKeplrAddr(chainId?: CosmosChainId): Promise; + + /** + * This method creates a new cosmos signer which is responsible for signing cosmos-based transactions. + * Browsers should use signers from the extension to implement this method + * @param chainId - Cosmos chain id + */ + public abstract createCosmosSigner(chainId: CosmosChainId): Promise; + + async getCosmWasmClient( + config: { rpc: string; chainId: CosmosChainId }, + options: SigningStargateClientOptions + ): Promise<{ + wallet: OfflineSigner; + client: SigningCosmWasmClient; + stargateClient: SigningStargateClient; + }> { + const { chainId, rpc } = config; + const wallet = await this.createCosmosSigner(chainId); + const tmClient = await Tendermint37Client.connect(rpc); + let client; + const optionsClient = { + ...options, + broadcastPollIntervalMs: BROADCAST_POLL_INTERVAL + }; + if (chainId === "injective-1") { + client = await Stargate.InjectiveSigningStargateClient.createWithSigner( + tmClient as any, + wallet, + optionsClient as any + ); + } else { + client = await SigningCosmWasmClient.createWithSigner(tmClient, wallet, optionsClient); + } + const stargateClient = await SigningStargateClient.createWithSigner(tmClient, wallet, optionsClient); + return { wallet, client, stargateClient }; + } + + async signAndBroadcast( + fromChainId: CosmosChainId, + fromRpc: string, + options: SigningStargateClientOptions, + sender: string, + encodedObjects: EncodeObject[] + ) { + // handle sign and broadcast transactions + const { client } = await this.getCosmWasmClient( + { + chainId: fromChainId as CosmosChainId, + rpc: fromRpc + }, + options + ); + return client.signAndBroadcast(sender, encodedObjects, "auto"); + } +} +``` + +You have to implement the CosmosWallet class. + +```ts +References for implement: +- Browser: "https://github.com/oraichain/oraiswap-frontend/blob/main/src/libs/keplr.ts#L11" + +- Node.JS: "https://github.com/oraichain/tonbridge-sdk/blob/main/packages/bridge-sdk/src/demo-utils.ts#L12" +``` + +- Bridge: + +```ts +import { COSMOS_CHAIN_IDS, OraiCommon, TON_NATIVE } from "@oraichain/common"; +import { toNano } from "@ton/ton"; +import { initCosmosWallet } from "./demo-utils"; // refer: https://github.com/oraichain/tonbridge-sdk/blob/main/packages/bridge-sdk/src/demo-utils.ts#L12 +import { calculateTimeoutTimestampTon, createTonBridgeHandler, TonWallet } from "@oraichain/ton-bridge-sdk"; + +export async function main() { + const oraiMnemonic = ; + const tonMnemonic = ; + const cosmosWallet = initCosmosWallet(oraiMnemonic); + const tonWallet = await TonWallet.create("mainnet", { + mnemonicData: + { + mnemonic: tonMnemonic.split(" "), + tonWalletVersion + }, + tonConnector: undefined, + }, "V4R2"); // if using on browser, use tonConnector instead of mnemonicData + const cosmosRpc = ( + await OraiCommon.initializeFromGitRaw({ + chainIds: [COSMOS_CHAIN_IDS.ORAICHAIN] + }) + ).chainInfos.cosmosChains[0].rpc; + const handler = await createTonBridgeHandler(cosmosWallet, tonWallet, { + rpc: cosmosRpc, + chainId: COSMOS_CHAIN_IDS.ORAICHAIN + }); + + await handler.sendToCosmos( + handler.wasmBridge.sender, + toNano(3), + TON_NATIVE, + { + queryId: 0, + value: toNano(0) // dont care + }, + calculateTimeoutTimestampTon(3600), + "" + ); +} + +main(); +``` + +**Parameters**: + +- `data` : data for building bridge jetton cell. +- `responseAddress`: address for receiving redundant fee ton. +- `queryId`: **query id** of that message. + +**Examples:** + +```tsx +import { useTonConnectUI } from "@tonconnect/ui-react"; +... +const [tonConnectUI] = useTonConnectUI(); +... +const memo = beginCell().storeStringRefTail().endCell(); +const getNativeBridgePayload = () => + BridgeAdapter.buildBridgeTonBody( + { + amount: BigInt(fmtAmount.toString()), + memo, + remoteReceiver: oraiAddress, + timeout, + }, + oraiAddressBech32, + { + queryId: 0, + value: toNano(0), // don't care this + } + ).toBoc(); + + const getOtherBridgeTokenPayload = () => + JettonWallet.buildSendTransferPacket( + Address.parse(tonAddress), + { + fwdAmount: FWD_AMOUNT, + jettonAmount: BigInt(fmtAmount.toString()), + jettonMaster: Address.parse(token.contractAddress), + remoteReceiver: oraiAddress, + timeout, + memo, + toAddress: bridgeAdapterAddress, + }, + 0 + ).toBoc(); + + const boc = isNativeTon + ? getNativeBridgePayload() + : getOtherBridgeTokenPayload(); + + const tx = await tonConnectUI.sendTransaction({ + validUntil: TON_MESSAGE_VALID_UNTIL, + messages: [ + { + address: toAddress, // dia chi token + amount: gasAmount, // gas + payload: Base64.encode(boc), + }, + ], + }); +``` + +## Oraichain to Ton + +```ts +const TON_NATIVE = "ton"; +type JettonMasterAddress = string; +export type TonDenom = typeof TON_NATIVE | JettonMasterAddress; + +interface ITonBridgeHandler { + sendToTon( + tonRecipient: string, + amount: bigint, + tokenDenomOnTon: string, + timeoutTimestamp: bigint = BigInt(calculateTimeoutTimestampTon(3600)) + ): Promise; +} +``` + +**Parameters**: + +- `tonRecipient` : ton recipient address. +- `amount`: amount of token that you want to send. +- `tokenDenomOnTon`: token denom address on ton. With ton native token it will be **ton zero address** and with jetton token, it will be **jetton master address**. +- `timeoutTimestamp`: timeout timestamp of packet. + +**Examples:** + +Here we use `@oraichain/tonbridge-sdk` for interaction. + +- Bridge: + +```ts +import { COSMOS_CHAIN_IDS, OraiCommon } from "@oraichain/common"; +import { toNano } from "@ton/ton"; +import { TON_ZERO_ADDRESS } from "./constants"; +import { initCosmosWallet } from "./demo-utils"; https://github.com/oraichain/tonbridge-sdk/blob/main/packages/bridge-sdk/src/demo-utils.ts#L12 +import { createTonBridgeHandler } from "./utils"; + +export async function main() { + const oraiMnemonic = ; + const tonMnemonic = ; + const cosmosWallet = initCosmosWallet(oraiMnemonic); + const tonWallet = await TonWallet.create("mainnet", { + mnemonicData: + { + mnemonic: tonMnemonic.split(" "), + tonWalletVersion + }, + tonConnector: undefined, + }, "V4R2"); + const cosmosRpc = ( + await OraiCommon.initializeFromGitRaw({ + chainIds: [COSMOS_CHAIN_IDS.ORAICHAIN], + }) + ).chainInfos.cosmosChains[0].rpc; + const handler = await createTonBridgeHandler( + cosmosWallet, + tonWallet, + { rpc: cosmosRpc, chainId: COSMOS_CHAIN_IDS.ORAICHAIN } + ); + const tonReceiveAddress = handler.tonSender.address.toString({ + urlSafe: true, + bounceable: false, + }); + console.log(tonReceiveAddress); + const result = await handler.sendToTon( + tonReceiveAddress, + toNano(3), + TON_ZERO_ADDRESS + ); + console.log(result); +} + +main(); +``` diff --git a/specs/obridge/universal-swap-memo.md b/specs/obridge/universal-swap-memo.md new file mode 100644 index 00000000..f998e169 --- /dev/null +++ b/specs/obridge/universal-swap-memo.md @@ -0,0 +1,225 @@ +# Universal swap memo + +## Overview + +When tokens are bridged to Oraichain, a memo is utilized to invoke the [UniversalSwap](https://github.com/oraichain/osor-api-contracts/blob/14d852cedc0b6adf62a910d889791f2bbfffecb7/contracts/entry-point/src/contract.rs#L222) function in the [Osor-api-contracts](https://github.com/oraichain/osor-api-contracts). This contract plays a crucial role in facilitating any-to-any token swaps, seamlessly integrating into multi-chain workflows. The memo is encoded using the protobuf standard, ensuring compatibility and efficiency in the swap process. + +```proto +syntax = "proto3"; + +message Memo { + + UserSwap user_swap = 1; + // string because the minimum receive may be very high due to decimal points + string minimum_receive = 2; + uint64 timeout_timestamp = 3; + PostAction post_swap_action = 4; + string recovery_addr = 5; + + // we dont need swap amount since it will be sent via cw20 or native, and we + // use that + message SwapExactAssetIn { repeated SwapOperation operations = 1; } + + message SmartSwapExactAssetIn { repeated Route routes = 1; } + + message Route { + string offer_amount = 1; + repeated SwapOperation operations = 2; + } + + message SwapOperation { + string poolId = 1; + string denomIn = 2; + string denomOut = 3; + } + + // if none is provided -> error, if more than one attributes are provided -> + // error + message UserSwap { + // or adapter name so that the smart router can redirect to the right swap + // router. + string swap_venue_name = 1; + optional SwapExactAssetIn swap_exact_asset_in = 2; + optional SmartSwapExactAssetIn smart_swap_exact_asset_in = 3; + } + + // Can possibly have both? -> if both then always contract_call first then ibc + // transfer + message PostAction { + optional IbcTransfer ibc_transfer_msg = 1; + optional IbcWasmTransfer ibc_wasm_transfer_msg = 2; + optional ContractCall contract_call = 3; + optional Transfer transfer_msg = 4; + } + + message IbcTransfer { + string source_channel = 1; + string source_port = 2; + string receiver = 3; + string memo = 4; + string recover_address = 5; + } + + message IbcWasmTransfer { + /// the local ibc endpoint you want to send tokens back on + string local_channel_id = 1; + string remote_address = 2; // can be 0x or bech32 + /// remote denom so that we know what denom to filter when we query based on + /// the asset info. Most likely be: oraib0x... or eth0x... + string remote_denom = 3; + /// How long the packet lives in seconds. If not specified, use + /// default_timeout + optional uint64 timeout = 4; + /// metadata of the transfer to suit the new fungible token transfer + optional string memo = 5; + } + + message ContractCall { + string contract_address = 1; + string msg = 2; + } + + message Transfer { + string to_address = 1; + } +} +``` + +## Memo Message Structure + +The `Memo` message is the primary structure for handling swap operations and associated actions. + +### `Memo` + +- **`user_swap` (1)**: Contains details about the swap operation. +- **`minimum_receive` (2)**: A string representing the minimum amount to be received from the swap (used for high precision due to decimal points). +- **`timeout_timestamp` (3)**: A `uint64` representing the deadline for the swap in Unix timestamp format. +- **`post_swap_action` (4)**: Specifies actions to take after the swap, such as transfers or contract calls. +- **`recovery_addr` (5)**: A string for an address used for recovery purposes. + +### UserSwap Message + +Details about the swap operation. + +- **`swap_venue_name` (1)**: The name of the swap venue or adapter used to route the swap. +- **`swap_exact_asset_in` (2)**: Optional. Defines a swap where a specific amount of an asset is used. +- **`smart_swap_exact_asset_in` (3)**: Optional. Defines a smart swap involving routes to perform the swap. + +### SwapExactAssetIn Message + +Used for swaps where the exact amount of an asset is specified. + +- **`operations` (1)**: A repeated field of `SwapOperation` messages, describing each operation in the swap. + +### SmartSwapExactAssetIn Message + +Used for smart swaps that involve routing. + +- **`routes` (1)**: A repeated field of `Route` messages, describing each route for the swap. + +### Route Message + +Details a route in a smart swap. + +- **`offer_amount` (1)**: The amount being offered in this route. +- **`operations` (2)**: A repeated field of `SwapOperation` messages for operations along this route. + +[Build a Swap Route](./universal-swap/swap-route.md) + +### SwapOperation Message + +Describes a single swap operation. + +- **`poolId` (1)**: Identifier for the pool involved in the swap. +- **`denomIn` (2)**: The denomination of the input asset. +- **`denomOut` (3)**: The denomination of the output asset. + +### PostAction Message + +Defines actions to take after the swap. + +- **`ibc_transfer_msg` (1)**: Optional. Message for an IBC (Inter-Blockchain Communication) transfer to Cosmos networks. +- **`ibc_wasm_transfer_msg` (2)**: Optional. Message for an IBC transfer involving WASM, used for bridging from Oraichain to EVM, Noble, and Bitcoin. +- **`contract_call` (3)**: Optional. Details for calling a smart contract after the swap. +- **`transfer_msg` (4)**: Optional. General transfer message. + +### IbcTransfer Message + +Details for an IBC transfer to Cosmos networks. + +- **`source_channel` (1)**: Source channel for the transfer. +- **`source_port` (2)**: Source port for the transfer. +- **`receiver` (3)**: Address of the receiver. +- **`memo` (4)**: Optional. Memo field for additional information. +- **`recover_address` (5)**: Optional. Address used for recovery. + +### IbcWasmTransfer Message + +Details for an IBC transfer involving WASM, used for bridging from Oraichain to EVM, Noble, and Bitcoin. + +- **`local_channel_id` (1)**: The local IBC endpoint for the transfer. +- **`remote_address` (2)**: Remote address for the transfer, can be in different formats. +- **`remote_denom` (3)**: Denomination of the asset in the remote chain. +- **`timeout` (4)**: Optional. Timeout for the transfer packet. +- **`memo` (5)**: Optional. Memo for additional metadata. + +### ContractCall Message + +Details for a contract call. + +- **`contract_address` (1)**: Address of the contract to call. +- **`msg` (2)**: Message or data to send to the contract. + +### Transfer Message + +Details for a simple transfer. + +- **`to_address` (1)**: Address to send the transfer to. + +## Example + +This example illustrates a scenario where a user transfers Orai from the BNB Chain to Oraichain, swaps it for USDC, and then bridges the USDC to Ethereum. + +```proto +Memo { + user_swap: { + swap_venue_name: "oraidex", + swap_exact_asset_in: None, + smart_swap_exact_asset_in: { + routes: [ + { + offer_amount: "3000000", + operations: [ + { + poolId: "orai19ttg0j7w5kr83js32tmwnwxxdq9rkmw4m3d7mn2j2hkpugwwa4tszwsnkg", + denomIn: "orai", + denomOut: "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd" + } + ] + } + ] + } + }, + minimum_receive: "10658885", + timeout_timestamp: 1723202477276000000, + post_swap_action: { + ibc_transfer_msg: None, + ibc_wasm_transfer_msg: { + local_channel_id: "channel-29", + remote_address: "eth-mainnet0x8c7E0A841269a01c0Ab389Ce8Fb3Cf150A94E797", + remote_denom: "eth-mainnet0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + timeout: Some(1723202477000000000), + memo: Some("") + }, + contract_call: None, + transfer_msg: None + }, + recovery_addr: "orai1hvr9d72r5um9lvt0rpkd4r75vrsqtw6yujhqs2" +} + +``` + +The encoded value you provided represents a serialized Memo message using Protocol Buffers: +`CqMBCgdvcmFpZGV4GpcBCpQBCgczMDAwMDAwEogBCj9vcmFpMTl0dGcwajd3NWtyODNqczMydG13bnd4eGRxOXJrbXc0bTNkN21uMmoyaGtwdWd3d2E0dHN6d3Nua2cSBG9yYWkaP29yYWkxNXVuOG1zeDNuNXpmOWFobHhtZmVxZDJrd2E1d20wbnJweGVyMzA0bTluZDVxNnFxMGc2c2t1NXBkZBIIMTA2NTg4ODUYgN7v8N7wgvUXIokBEoYBCgpjaGFubmVsLTI5EjVldGgtbWFpbm5ldDB4OGM3RTBBODQxMjY5YTAxYzBBYjM4OUNlOEZiM0NmMTUwQTk0RTc5Nxo1ZXRoLW1haW5uZXQweEEwYjg2OTkxYzYyMThiMzZjMWQxOUQ0YTJlOUViMGNFMzYwNmVCNDgggISi7d3wgvUXKgAqK29yYWkxaHZyOWQ3MnI1dW05bHZ0MHJwa2Q0cjc1dnJzcXR3Nnl1amhxczI=` + +[How to encode memo](./universal-swap/encode-memo.md) diff --git a/specs/obridge/universal-swap/encode-memo.md b/specs/obridge/universal-swap/encode-memo.md new file mode 100644 index 00000000..16332406 --- /dev/null +++ b/specs/obridge/universal-swap/encode-memo.md @@ -0,0 +1,29 @@ +# Encode Memo + +## Steps to Encode a Memo + +### 1. Generate TypeScript Definitions from Proto File + +- Use the Protocol Buffers compiler (`protoc`) to generate TypeScript definitions from your `.proto` file. This will create TypeScript classes/interfaces corresponding to the protobuf messages defined in your proto file. + +```bash +# gen for ts +protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_opt=esModuleInterop=true --ts_proto_out . --proto_path=./protos universal_swap_memo.proto + +``` + +### 2. Create the Memo Object + +- Import the generated TypeScript class for the memo. +- Create an instance of the memo class. +- Populate the necessary fields within the memo object according to your requirements. Each field should be filled with appropriate data as specified by your protobuf schema. + +### 3. Encode the Memo to a Base64 String + +- Encode the populated memo object using the `Memo.encode(memo).finish()` method. This will return a `Uint8Array` representing the serialized memo. +- Convert the `Uint8Array` to a base64 string using `Buffer.from(encodedMemo).toString("base64");`. + +```typescript +const encodedMemo = Memo.encode(memo).finish(); +return Buffer.from(encodedMemo).toString("base64"); +``` diff --git a/specs/obridge/universal-swap/swap-route.md b/specs/obridge/universal-swap/swap-route.md new file mode 100644 index 00000000..d45a301a --- /dev/null +++ b/specs/obridge/universal-swap/swap-route.md @@ -0,0 +1,203 @@ +# Swap route + +## Overview + +A **Swap Route** is an essential component for executing token swaps on Oraidex. The Swap Route defines the path through which an asset (the **offer asset**) is exchanged for another asset (the **destination asset**) using potentially multiple intermediary steps, known as **multi-hop** swaps. + +In each route, the `offer_amount` specifies the amount of the initial asset being swapped. The route consists of a series of `SwapOperations`, where each operation involves converting one token (tokenIn) into another (tokenOut). Importantly, within a single route, the output token of one operation must match the input token of the next operation, ensuring a seamless transition from the initial asset to the final destination asset. + +The destination token received at the end of the swap route is then used for the subsequent actions in the broader transaction process, whether it be further swaps, transfers, or any other specified operation. This setup allows for complex, efficient trading strategies that can optimize for the best possible outcome across different pools and trading pairs on Oraidex. + +```proto +message SmartSwapExactAssetIn { repeated Route routes = 1; } + +message Route { + string offer_amount = 1; + repeated SwapOperation operations = 2; + } + + message SwapOperation { + string poolId = 1; + string denomIn = 2; + string denomOut = 3; + } +``` + +## How to Build a Swap Route + +To build a Swap Route on Oraidex, you can utilize the API provided by Oraidex's smart router. This API helps determine the most efficient route for your swap. Below is a step-by-step guide: + +Start by calling the **Smart Route API** available at `https://osor.oraidex.io/smart-router/alpha-router`. This API returns the optimal swap route based on the assets and parameters you provide. + +Suppose you've bridged Orai from BNB Chain to Oraichain and now want to swap it for USDT on Oraichain. You would call the Smart Route API with the following request body: + +```json +POST https://osor.oraidex.io/smart-router/alpha-router +{ + "sourceAsset": "orai", // source asset + "sourceChainId": "Oraichain", // source chain ID + "destAsset": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh", // destination asset + "destChainId": "Oraichain", // destination chain id + "offerAmount": "1000000000", // offer amount to swap + "swapOptions": { + "protocols": ["Oraidex", "OraidexV3"] // protocols used for swapping + } +} +``` + +Sample response: + +In the sample response provided by the Smart Route API, we are given a swap plan that divides 1000 Orai into two separate routes—one that swaps 700 Orai and another that swaps 300 Orai. Below is an example of how to interpret the response and use it to build your swap operations. + +```json +{ + "swapAmount": "1000000000", + "returnAmount": "5601376843", + "routes": [ + { + "swapAmount": "700000000", + "returnAmount": "3921206817", + "paths": [ + { + "chainId": "Oraichain", + "tokenIn": "orai", + "tokenInAmount": "700000000", + "tokenOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh", + "tokenOutAmount": "3921206817", + "tokenOutChainId": "Oraichain", + "actions": [ + { + "type": "Swap", + "protocol": "OraidexV3", + "tokenIn": "orai", + "tokenInAmount": "700000000", + "tokenOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh", + "tokenOutAmount": "3921206817", + "swapInfo": [ + { + "poolId": "orai-orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh-3000000000-100", + "tokenOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + ] + } + ] + } + ] + }, + { + "swapAmount": "300000000", + "returnAmount": "1680170026", + "paths": [ + { + "chainId": "Oraichain", + "tokenIn": "orai", + "tokenInAmount": "300000000", + "tokenOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh", + "tokenOutAmount": "1680170026", + "tokenOutChainId": "Oraichain", + "actions": [ + { + "type": "Swap", + "protocol": "OraidexV3", + "tokenIn": "orai", + "tokenInAmount": "300000000", + "tokenOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh", + "tokenOutAmount": "1680170026", + "swapInfo": [ + { + "poolId": "orai-orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd-3000000000-100", + "tokenOut": "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd" + }, + { + "poolId": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh-orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd-500000000-10", + "tokenOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + ] + } + ] + } + ] + } + ] +} +``` + +From the response, you can see that 1000 Orai is split into two routes: + +1. First route: + +- Swap 700 Orai to USDT via a single pool. +- Use the swapInfo in this route to build the swap operations + +```json +{ + "offer_amount": "700000000", + "operations": [ + { + "poolId": "orai-orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh-3000000000-100", + "denomIn": "orai", + "denomOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + ] +} +``` + +2. Second Route: + +- Swap 300 Orai to USDT, but this time through a multi-hop process. +- The first swap is from Orai to an intermediate token. +- The second swap converts the intermediate token to USDT + +```json +{ + "offer_amount": "700000000", + "operations": [ + { + "poolId": "orai-orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd-3000000000-100", + "denomIn": "orai", + "denomOut": "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd" + }, + { + "poolId": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh-orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd-500000000-10", + "denomIn": "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd", + "denomOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + ] +} +``` + +To combine the two routes from the sample response into a single smart_swap_exact_asset_in structure, the resulting swap routes are as follows + +```json +{ + "smart_swap_exact_asset_in": { + "routes": [ + { + "offer_amount": "700000000", + "operations": [ + { + "poolId": "orai-orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh-3000000000-100", + "denomIn": "orai", + "denomOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + ] + }, + { + "offer_amount": "300000000", + "operations": [ + { + "poolId": "orai-orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd-3000000000-100", + "denomIn": "orai", + "denomOut": "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd" + }, + { + "poolId": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh-orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd-500000000-10", + "denomIn": "orai15un8msx3n5zf9ahlxmfeqd2kwa5wm0nrpxer304m9nd5q6qq0g6sku5pdd", + "denomOut": "orai12hzjxfh77wl572gdzct2fxv2arxcwh6gykc7qh" + } + ] + } + ] + } +} +```