diff --git a/.env b/.env index 21bf2d2d..7503da5a 100644 --- a/.env +++ b/.env @@ -16,3 +16,4 @@ NEXT_PUBLIC_REGTEST_DEVNET_ARES_URL=https://bitcoin-api-gateway-regtest-devnet.z NEXT_PUBLIC_REGTEST_DEVNET_AEGLE_URL=https://api-regtest-devnet.apollobyzeus.space +NEXT_PUBLIC_DEVNET_REDEEM_ADDRESS=Acjdw9qoQnDZMgEMtN5UvPFHQdg9zoWwKX3Tc3QQHpy8 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..b9380a9b --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +NEXT_PUBLIC_SOLANA_DEVNET_RPC= + +SOLANA_DEVNET_RPC= + +NEXT_PUBLIC_ZEUS_SCAN_URL= + +NEXT_PUBLIC_REGTEST_BITCOIN_EXPLORER_URL= + +NEXT_PUBLIC_DEVNET_BOOTSTRAPPER_PROGRAM_ID= + +NEXT_PUBLIC_REGTEST_DEVNET_TWO_WAY_PEG_GUARDIAN_SETTING= + +NEXT_PUBLIC_REGTEST_DEVNET_HERMES_URL= + +NEXT_PUBLIC_REGTEST_DEVNET_ARES_URL= + +NEXT_PUBLIC_REGTEST_DEVNET_AEGLE_URL= + +NEXT_PUBLIC_DEVNET_REDEEM_ADDRESS=2wUg8UcNpbSWr8p3VKkXKmZYVonHX6Y3tKpuPXgp5czq diff --git a/.gitignore b/.gitignore index 8948dab9..7728c84f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ next-env.d.ts # Sentry Config File .env.sentry-build-plugin +.env diff --git a/README.md b/README.md index 3ed54205..36941dfb 100644 --- a/README.md +++ b/README.md @@ -249,15 +249,6 @@ export default function getTransactions({ solanaPubkey, bitcoinWallet }) { const { feeRate } = useTwoWayPegConfiguration(); const config = useNetworkConfig(); - // Fetch deposit transactions (combined onchain transaction and transaction in browser indexed db) - const { combinedInteractions: depositTransactions } = - useDepositInteractionsWithCache({ - solanaAddress: solanaPubkey?.toBase58(), - bitcoinXOnlyPubkey: bitcoinWallet - ? toXOnly(Buffer.from(bitcoinWallet.pubkey, "hex")).toString("hex") - : undefined, - }); - // Fetch withdrawal transactions const { data: withdrawalTransactions, @@ -309,6 +300,14 @@ export default function getTransactions({ solanaPubkey, bitcoinWallet }) { // withdrawal_request_pda: "YOUR_WITHDRAWAL_REQUEST_PDA" // Or you can specify the interaction id and fetch the interaction detail from our indexer API + const { combinedInteractions: depositTransactions } = + useDepositInteractionsWithCache({ + solanaAddress: solanaPubkey?.toBase58(), + bitcoinXOnlyPubkey: bitcoinWallet + ? toXOnly(Buffer.from(bitcoinWallet.pubkey, "hex")).toString("hex") + : undefined, + }); + const targetTx = depositTransactions[0]; // choose the first transaction as example const interactionSteps = await hermesFetcher( `/api/v1/raw/layer/interactions/${targetTx.interaction_id}/steps`, @@ -343,7 +342,7 @@ The ZPL defines several interaction types and statuses to track the progress of 1. `BitcoinDepositToHotReserve`: Initial deposit detected on Bitcoin network from user address to our hot reserve address 2. `VerifyDepositToHotReserveTransaction`: BitcoinSPV program verifying the deposit transaction -3. `SolanaDepositToHotReserve`: Deposit confirmed on Solana by TwoWayPeg program +3. `SolanaDepositToHotReserve`: Deposit confirmed and updated status on Solana by TwoWayPeg program 4. `AddLockToColdReserveProposal`: Zeus node send transaction to move BTC from hot reserve to cold reserve 5. `BitcoinLockToColdReserve`: Move BTC from hot reserve to cold reserve transaction is observed on Bitcoin network 6. `VerifyLockToColdReserveTransaction`: BitcoinSPV program verifying cold reserve transaction @@ -577,7 +576,9 @@ The ZPL provides functionality for users to store zBTC in a custodial vault and  -Orpheus allows you to modify the retrieval address to designate an alternative Escrow token account managed by your application. By implementing this change, redemption transactions initiated by users of your application will direct funds to the application-controlled escrow rather than the user’s individual wallet. This functionality unlocks a range of decentralized finance (DeFi) use cases, including money markets, neutral trading strategies, liquidity provisioning, or the development of a Bitcoin-backed stablecoin. +By flexibly cascading sdk in Orpheus, you can set custom retrieval address to designate an alternative Escrow token account managed by your application. By implementinng this operation, redemption transactions initiated by users of your application will go to the application-controlled escrow rather than the user’s individual wallet. This functionality unlocks a range of decentralized finance (DeFi) use cases, including money markets, neutral trading strategies, liquidity provisioning, or the development of a Bitcoin-backed stablecoin. + +Below is a sample implementation of creating retrieve instruction: ```typescript constructRetrieveIx( @@ -622,11 +623,13 @@ For guidance on constructing a Solana escrow, developers may consult reference i - https://github.com/ironaddicteddog/anchor-escrow - https://github.com/deanmlittle/native-escrow-2024 +Then by ingeeniously cascade a transfer instruction, you can redeem the zBTC to a custodial escrow. + #### Implementation ```typescript import { useState } from "react"; -import { useWallet } from "@solana/wallet-adapter-react"; +import { useWallet, useConnection } from "@solana/wallet-adapter-react"; import { useZplClient } from "@/contexts/ZplClientProvider"; import usePositions from "@/hooks/zpl/usePositions"; import { useNetworkConfig } from "@/hooks/misc/useNetworkConfig"; @@ -634,6 +637,12 @@ import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import BigNumber from "bignumber.js"; import { BN } from "bn.js"; import { BTC_DECIMALS } from "@/utils/constant"; +import { + getAssociatedTokenAddressSync, + createTransferInstruction, + createAssociatedTokenAccountInstruction, +} from "@solana/spl-token"; + export default function Home() { const [redeemAmount, setRedeemAmount] = useState(0); @@ -641,6 +650,7 @@ export default function Home() { const zplClient = useZplClient(); const { publicKey: solanaPubkey } = useWallet(); const { data: positions } = usePositions(solanaPubkey); + const { connection } = useConnection(); const handleRedeem = async () => { if (!redeemAmount || !zplClient) return; @@ -680,6 +690,37 @@ export default function Home() { ); ixs.push(retrieveIx); + + const escrow_address = process.env.NEXT_PUBLIC_ESCROW_ADDRESS; // set your escrow address here or other method you like + if(escrow_address) { + const targetAddress = new PublicKey(escrow_address); + const toATA = getAssociatedTokenAddressSync( + new PublicKey(config.assetMint), + targetAddress, + true // allow off curve to approve PDA + ); + // check if the associated token account of target address initialized + const info = await connection.getAccountInfo(toATA); + if (!info) { + // if not, create one + const createIx = createAssociatedTokenAccountInstruction( + solanaPubkey, + toATA, + targetAddress, + new PublicKey(config.assetMint) + ); + ixs.push(createIx); + } + // add a transfer instruction to transfer the tokens to the receive_address + const transferIx = createTransferInstruction( + receiverAta, + toATA, + solanaPubkey, + BigInt(amountToRedeem.toString()) + ); + ixs.push(transferIx); + } + remainingAmount = remainingAmount.sub(amountToRedeem); if (remainingAmount.eq(new BN(0))) break; diff --git a/package-lock.json b/package-lock.json index 1d0b4291..976ca8da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "orpheus", - "version": "0.1.2", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "orpheus", - "version": "0.1.2", + "version": "0.1.3", "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", "@cloudflare/next-on-pages": "^1.13.10", diff --git a/package.json b/package.json index e72217ce..ee7a654b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "orpheus", - "version": "0.1.2", + "version": "0.1.3", "engines": { "node": "20.18.3" }, diff --git a/src/components/PortfolioV2/Modals/Redeem.tsx b/src/components/PortfolioV2/Modals/Redeem.tsx index 8d02d77f..4f37fb19 100644 --- a/src/components/PortfolioV2/Modals/Redeem.tsx +++ b/src/components/PortfolioV2/Modals/Redeem.tsx @@ -1,7 +1,11 @@ import { captureException } from "@sentry/nextjs"; -import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + getAssociatedTokenAddressSync, + createTransferInstruction, + createAssociatedTokenAccountInstruction, +} from "@solana/spl-token"; import { WalletSignTransactionError } from "@solana/wallet-adapter-base"; -import { useWallet } from "@solana/wallet-adapter-react"; +import { useWallet, useConnection } from "@solana/wallet-adapter-react"; import { PublicKey, TransactionInstruction } from "@solana/web3.js"; import BigNumber from "bignumber.js"; import { BN } from "bn.js"; @@ -63,6 +67,7 @@ export default function RedeemModal({ const { publicKey: solanaPubkey } = useWallet(); const { mutate: mutateBalance } = useBalance(solanaPubkey); const { mutate: mutatePositions } = usePositions(solanaPubkey); + const { connection } = useConnection(); const [isRedeeming, setIsRedeeming] = useState(false); const [redeemAmount, setRedeemAmount] = useState(""); @@ -143,7 +148,6 @@ export default function RedeemModal({ if (!twoWayPegGuardianSetting) throw new Error("Two way peg guardian setting not found"); - // TODO: You can customize the retrieve address here const receiverAta = getAssociatedTokenAddressSync( new PublicKey(config.assetMint), solanaPubkey, @@ -155,8 +159,40 @@ export default function RedeemModal({ new PublicKey(twoWayPegGuardianSetting), receiverAta ); - ixs.push(retrieveIx); + + // TODO: You can customize the retrieve address here + if (process.env.NEXT_PUBLIC_DEVNET_REDEEM_ADDRESS) { + const targetAddress = new PublicKey( + process.env.NEXT_PUBLIC_DEVNET_REDEEM_ADDRESS + ); + const toATA = getAssociatedTokenAddressSync( + new PublicKey(config.assetMint), + targetAddress, + true + ); + // check if the target address has an associated token account + const info = await connection.getAccountInfo(toATA); + if (!info) { + // if not, create one + const createIx = createAssociatedTokenAccountInstruction( + solanaPubkey, + toATA, + targetAddress, + new PublicKey(config.assetMint) + ); + ixs.push(createIx); + } + // add a transfer instruction to transfer the tokens to the receive_address + const transferIx = createTransferInstruction( + receiverAta, + toATA, + solanaPubkey, + BigInt(amountToRedeem.toString()) + ); + ixs.push(transferIx); + } + remainingAmount = remainingAmount.sub(amountToRedeem); if (remainingAmount.eq(new BN(0))) break; @@ -179,6 +215,7 @@ export default function RedeemModal({ } else { notifyError("Error in redeeming, please try again"); captureException(error); + console.error("Error in redeeming", error); } } finally { setIsRedeeming(false); diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 6413d230..0d69f2a3 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -16,9 +16,9 @@ export const getSolanaExplorerUrl = ( switch (solanaNetwork) { case SolanaNetwork.Devnet: - return `https://solana.fm/${type}/${target}?cluster=devnet-alpha`; + return `https://explorer.solana.com/${type}/${target}?cluster=devnet`; default: - return `https://solana.fm/${type}/${target}?cluster=devnet-alpha`; + return `https://explorer.solana.com/${type}/${target}`; } }; diff --git a/src/utils/notification.tsx b/src/utils/notification.tsx index 368c0d61..24878586 100644 --- a/src/utils/notification.tsx +++ b/src/utils/notification.tsx @@ -25,7 +25,7 @@ const TxSuccessMsg = ({ {txId && solanaNetwork ? (