Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
literal 0
HcmV?d00001
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..fd81e88
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,27 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+ }
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ rgb(var(--background-end-rgb))
+ )
+ rgb(var(--background-start-rgb));
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 0000000..a3d3c13
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,22 @@
+import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import './globals.css'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export const metadata: Metadata = {
+ title: 'sBTC Deposit WebUI',
+ description: 'Deposit sBTC to your wallet',
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/app/lib.tsx b/src/app/lib.tsx
new file mode 100644
index 0000000..e021058
--- /dev/null
+++ b/src/app/lib.tsx
@@ -0,0 +1,88 @@
+import { type UtxoWithTx } from 'sbtc';
+import { type Transaction } from '@scure/btc-signer';
+import { NETWORK, MEMPOOLTXAPIURL, STACKSAPIURL, MEMPOOLURLADDRESS, STACKSURLADDRESS, STACKSURLADDRESSPOST, MEMPOOLTXURL } from './netconfig';
+
+export const getBalanceSBTC = async (addressSTX: string): Promise => {
+ const response = await fetch(`${STACKSAPIURL}${addressSTX}/balances`);
+ const balances = await response.json();
+ try {
+ return balances.fungible_tokens[`${addressSTX}.asset::sbtc`].balance;
+ } catch {
+ return 0;
+ }
+}
+
+export const transactionConfirmed = async (txid: string): Promise => {
+ const response = await fetch(`${MEMPOOLTXAPIURL}${txid}/status`);
+ const status = await response.json();
+ return status.confirmed
+}
+
+export const getURLAddressBTC = (addressBTC: string): string => {
+ return `${MEMPOOLURLADDRESS}${addressBTC}`;
+}
+
+export const getURLAddressSTX = (addressSTX: string): string => {
+ return `${STACKSURLADDRESS}${addressSTX}${STACKSURLADDRESSPOST}`;
+}
+
+export const getURLTxBTC = (txid: string): string => {
+ return `${MEMPOOLTXURL}${txid}`;
+}
+
+export const getFeeRate = async (): Promise => {
+ try {
+ return await NETWORK.estimateFeeRate('low');
+ } catch {
+ console.log("Failed to estimate fee rate!");
+ return 1;
+ }
+}
+
+export type stateType = "DISCONNECTED" | "CONNECTING" | "READY" | "INSUFFICIENT_FUNDS" | "REQUEST_SENT" | "CONFIRMED";
+
+export type walletType = {
+ decentralizedId: string | undefined,
+ addressBTC: string | undefined,
+ balanceBTC: number | undefined,
+ publicKeyBTC: string | undefined,
+ balanceSBTC: number | undefined,
+ addressSTX: string | undefined,
+ utxos: UtxoWithTx[] | undefined,
+}
+
+export type depositInfoType = {
+ addressPeg: string | undefined,
+ feeRate: number | undefined,
+ tx: Transaction | undefined,
+ finalTx: string | undefined,
+}
+
+export const emptyWallet: walletType = {
+ decentralizedId: undefined,
+ addressBTC: undefined,
+ balanceBTC: undefined,
+ publicKeyBTC: undefined,
+ balanceSBTC: undefined,
+ addressSTX: undefined,
+ utxos: undefined,
+}
+
+export const emptyDepositInfo: depositInfoType = {
+ addressPeg: undefined,
+ feeRate: undefined,
+ tx: undefined,
+ finalTx: undefined,
+}
+
+export const humanReadableNumber = (number: number): string => {
+ if (1_000_000_000 <= number) {
+ return `${(number/1_000_000_000).toLocaleString()}B`;
+ } else if (1_000_000 <= number) {
+ return `${(number/1_000_000).toLocaleString()}M`;
+ } else if (1_000 <= number) {
+ return `${(number/1_000).toLocaleString()}K`;
+ } else {
+ return `${number.toLocaleString()}`;
+ }
+}
diff --git a/src/app/logwindow.tsx b/src/app/logwindow.tsx
new file mode 100644
index 0000000..0ef4037
--- /dev/null
+++ b/src/app/logwindow.tsx
@@ -0,0 +1,41 @@
+import type { stateType, walletType, depositInfoType } from './lib';
+export function LogWindow({wallet, depositInfo, state}: {wallet: walletType, depositInfo: depositInfoType, state: stateType}) {
+ return(
+
+ State: {state}
+ {
+ wallet.decentralizedId ? ( Decentralized ID: {wallet.decentralizedId} ) : ( Fetching Decentralized ID... )
+ }
+ {
+ wallet.addressBTC ? ( BTC Address: {wallet.addressBTC} ) : ( Fetching BTC Address... )
+ }
+ {
+ wallet.addressSTX ? ( STX Address: {wallet.addressSTX} ) : ( Fetching STX Address... )
+ }
+ {
+ wallet.publicKeyBTC ? ( BTC Public Key: {wallet.publicKeyBTC} ) : ( Fetching BTC Public Key... )
+ }
+ {
+ wallet.balanceBTC ? ( BTC Balance: {wallet.balanceBTC} ) : ( Fetching BTC Balance... )
+ }
+ {
+ wallet.balanceSBTC ? ( sBTC Balance: {wallet.balanceSBTC} ) : ( Fetching sBTC Balance... )
+ }
+ {
+ wallet.utxos ? ( Number of UTXOs: {wallet.utxos.length} ) : ( Fetching UTXOs... )
+ }
+ {
+ depositInfo.addressPeg ? ( sBTC Peg Address: {depositInfo.addressPeg} ) : ( Fetching sBTC Peg Address... )
+ }
+ {
+ depositInfo.feeRate ? ( Fee Rate: {depositInfo.feeRate} ) : ( Fetching Fee Rate... )
+ }
+ {
+ depositInfo.tx ? ( Transaction prepared ) : ( Transaction not prepared... )
+ }
+ {
+ depositInfo.finalTx ? ( Transaction finalized: {depositInfo.finalTx} ) : ( Transaction not finalized... )
+ }
+
+ );
+}
diff --git a/src/app/netconfig.tsx b/src/app/netconfig.tsx
new file mode 100644
index 0000000..279b6a2
--- /dev/null
+++ b/src/app/netconfig.tsx
@@ -0,0 +1,10 @@
+// import { TestnetHelper } from 'sbtc';
+// const network = Object.freeze(new TestnetHelper());
+import { DevEnvHelper } from 'sbtc';
+export const NETWORK = Object.freeze(new DevEnvHelper());
+export const MEMPOOLTXURL = "http://127.0.0.1:8083/tx/";
+export const MEMPOOLURLADDRESS = "http://127.0.0.1:8083/address/";
+export const MEMPOOLTXAPIURL = "http://127.0.0.1:8083/api/tx/";
+export const STACKSAPIURL = "http://localhost:3999/extended/v1/address/";
+export const STACKSURLADDRESS= "http://127.0.0.1:3020/address/";
+export const STACKSURLADDRESSPOST = "?chain=testnet&api=http://localhost:3999";
diff --git a/src/app/page.tsx b/src/app/page.tsx
new file mode 100644
index 0000000..6c85c28
--- /dev/null
+++ b/src/app/page.tsx
@@ -0,0 +1,428 @@
+"use client";
+import { useState, useEffect } from 'react';
+import type { UserData } from '@stacks/connect';
+import { AppConfig, UserSession, showConnect, } from "@stacks/connect";
+import { StacksTestnet } from "@stacks/network";
+import { bytesToHex, hexToBytes } from '@stacks/common';
+import { sbtcDepositHelper, UtxoWithTx, WALLET_00 } from 'sbtc';
+import * as btc from '@scure/btc-signer';
+
+// Network configuration
+import { NETWORK } from './netconfig';
+
+// Library
+import type { stateType, walletType, depositInfoType } from './lib';
+import { emptyWallet, emptyDepositInfo } from './lib';
+import { getBalanceSBTC, transactionConfirmed, getURLAddressBTC,
+ getURLAddressSTX, getURLTxBTC, getFeeRate } from './lib';
+import { humanReadableNumber as hrn } from './lib';
+
+// UI
+import { LogWindow } from './logwindow';
+import { Alert, Badge, Banner, Button, Card, Spinner } from 'flowbite-react';
+
+// Setting: How much to deposit
+const DEPOSITAMOUNT : number = 10_000;
+
+// Main component
+export default function Home() {
+ // State and wallet
+ const [state, setState] = useState("DISCONNECTED");
+ const [wallet, setWallet] = useState(emptyWallet);
+ const [depositInfo, setDepositInfo] = useState(emptyDepositInfo);
+ const [userData, setUserData] = useState(null);
+
+ // Log current state for debug purposes
+ // useEffect(() => {
+ // console.log("NEW STATE: ", state);
+ // console.log("WALLET: ", wallet);
+ // console.log("DEPOSIT INFO: ", depositInfo);
+ // }, [state]);
+
+ // Reset application
+ const reset = () : void => {
+ setState("DISCONNECTED");
+ setWallet(emptyWallet);
+ setDepositInfo(emptyDepositInfo);
+ setUserData(null);
+ if (userSession) {
+ userSession.signUserOut();
+ }
+ }
+
+ // Connect with Leather/Hiro Wallet
+ const appConfig = new AppConfig();
+ const userSession = new UserSession({ appConfig });
+
+ useEffect(() => {
+ if (userSession.isSignInPending()) {
+ userSession.handlePendingSignIn().then((userData) => {
+ setUserData(userData);
+ });
+ } else if (userSession.isUserSignedIn()) {
+ setUserData(userSession.loadUserData());
+ }
+ }, []);
+
+ // Retrieve necessary information from the wallet and from the network
+ // This method depends on the network we are on. For now, it is implemented
+ // for the local Development Network. Also, there are a few issues:
+ //
+ // setBtcAddress(userData.profile.btcAddress.p2wpkh.testnet);
+ // setBtcPublicKey(userData.profile.btcPublicKey.p2wpkh);
+ // Because of some quirks with Leather, we need to pull our BTC wallet using
+ // the helper if we are on devnet
+ // The following, as noted in the documentation, fails.
+ // According to Leather, the STX Address is the same as on the testnet.
+ // In fact, it coincides with the SBTC_FT_ADDRESS_DEVENV in the constants
+ // file (sbc).
+ // setStxAddress(bitcoinAccountA.tr.address);
+ // setFeeRate(await network.estimateFeeRate('low'));
+ // setSbtcPegAddress(await network.getSbtcPegAddress());
+ const getWalletAndDepositDetails = async (userData:UserData) => {
+ const bitcoinAccountA = await NETWORK.getBitcoinAccount(WALLET_00);
+ const addressBTC = bitcoinAccountA.wpkh.address;
+ const addressSTX = userData.profile.stxAddress.testnet;
+ const balanceBTC = await NETWORK.getBalance(addressBTC);
+ setWallet({ ...wallet,
+ decentralizedId: userData.decentralizedID ,
+ addressSTX: addressSTX,
+ addressBTC: addressBTC,
+ publicKeyBTC: bitcoinAccountA.publicKey.buffer.toString(),
+ balanceBTC: balanceBTC,
+ balanceSBTC: await getBalanceSBTC(addressSTX),
+ utxos: await NETWORK.fetchUtxos(addressBTC),
+ });
+ // Deposit Information
+ // const feeRate = 1;
+ const feeRate = await getFeeRate();
+ // const feeRates = await NETWORK.estimateFeeRates();
+ // console.log(feeRates);
+ // const feeRate = await NETWORK.estimateFeeRate('low');
+ setDepositInfo({ ...depositInfo,
+ addressPeg: await NETWORK.getSbtcPegAddress(),
+ feeRate: feeRate,
+ });
+ if ((balanceBTC + feeRate * 1_000) > DEPOSITAMOUNT) {
+ setState("READY");
+ } else {
+ setState("INSUFFICIENT_FUNDS");
+ }
+ }
+
+ // Hook to get wallet and network information.
+ useEffect(() => {
+ if (userData) {
+ if(!!userData.profile) {
+ setState("CONNECTING");
+ getWalletAndDepositDetails(userData);
+ }
+ }
+ }, [userData]);
+
+ // Hook to connect to the Leather wallet
+ const connectWallet = () => {
+ showConnect({
+ userSession,
+ network: StacksTestnet,
+ appDetails: {
+ name: "sBTC Deposit",
+ icon: "https://freesvg.org/img/bitcoin.png",
+ },
+ onFinish: () => {
+ window.location.reload();
+ },
+ onCancel: () => {
+ reset();
+ },
+ });
+ }
+
+ // Continue fetching sBTC and BTC balance
+ const fetchBalanceForever = async () => {
+ const balanceBTC = await NETWORK.getBalance(wallet.addressBTC as string);
+ const balanceSBTC = await getBalanceSBTC(wallet.addressSTX as string);
+ setWallet({ ...wallet, balanceBTC: balanceBTC, balanceSBTC: balanceSBTC });
+ }
+
+ // Check transaction
+ const waitUntilConfirmed = async (txid : string, intervalId : NodeJS.Timeout) => {
+ const confirmed = await transactionConfirmed(txid);
+ if (confirmed) {
+ setState("CONFIRMED");
+ clearInterval(intervalId);
+ setInterval(() => {
+ fetchBalanceForever();
+ }, 10000);
+ }
+ }
+
+ // Hook to check for confirmations
+ const waitForConfirmation = (txid : string) => {
+ const intervalId = setInterval(() => {
+ waitUntilConfirmed(txid, intervalId);
+ // fetch(`${mempoolTxAPIUrl}${txid}/status`,{mode: 'no-cors'})
+ // .then((response) => response.json())
+ // .then((status) => {
+ // if (status.confirmed) {
+ // console.log("checkTX: CONFIRMED!");
+ // setConfirmed(true);
+ // clearInterval(intervalId);
+ // }
+ // })
+ // .catch((err) => console.error(err));
+ }, 10000);
+ }
+
+ // Hook to start deposit
+ const deposit = async () => {
+ const tx = await sbtcDepositHelper({
+ // network: TESTNET,
+ // pegAddress: sbtcPegAddress,
+ stacksAddress: wallet.addressSTX as string,
+ amountSats: DEPOSITAMOUNT,
+ feeRate: depositInfo.feeRate as number,
+ utxos: wallet.utxos as UtxoWithTx[],
+ bitcoinChangeAddress: wallet.addressBTC as string,
+ });
+ setDepositInfo({ ...depositInfo, tx: tx });
+ // Sign and broadcast
+ const psbt = tx.toPSBT();
+ const requestParams = {
+ publicKey: wallet.publicKeyBTC as string,
+ hex: bytesToHex(psbt),
+ };
+ const txResponse = await window.btc.request("signPsbt", requestParams);
+ const formattedTx = btc.Transaction.fromPSBT(
+ hexToBytes(txResponse.result.hex)
+ );
+ formattedTx.finalize();
+ const finalTx : string = await NETWORK.broadcastTx(formattedTx);
+ setDepositInfo({ ...depositInfo, finalTx: finalTx });
+ // Wait for confirmatins
+ setState("REQUEST_SENT");
+ waitForConfirmation(finalTx);
+ }
+
+ // Main component
+ return (
+
+
+
+
+
+
+ {
+ (state != "DISCONNECTED" && state != "CONNECTING") ? (
+ <>You currently hold {hrn(wallet.balanceBTC as number)} BTCs and {hrn(wallet.balanceSBTC as number)} sBTCs.>
+ ) : (
+ (state == "DISCONNECTED") ? (
+ <>Connect to proceed>
+ ) : (
+ <>Loading ...>
+ )
+ )
+ }
+
+
+
+
+
+
+
+
+
+
+ Deposit your satoshis.
+
+
+
+
+ Transfer {hrn(DEPOSITAMOUNT)} satoshis to the peg-in.
+
+
+
+ {
+ (state == "DISCONNECTED") ? (
+
+ ) : null
+ }
+ {
+ (state == "CONNECTING") ? (
+
+
+
+
+
+
+
+ Loading necessary data from your wallet and the chain...
+
+
+
+ ) : null
+ }
+ {
+ (state == "READY") ? (
+ <>
+
+
+
+ The sats will be sent from your
+
+ BTC address
+
+ to the
+
+ peg address.
+
+ You will recieve the equal amount of sBTC to your
+
+ STX Address.
+
+
+
+
+ {depositInfo.feeRate as number} sat/byte fee
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : null
+ }
+ {
+ (state == "INSUFFICIENT_FUNDS") ? (
+ <>
+
+
+
+ Your BTC account does not contain enough Satoshis.
+ Top it up before proceeding.
+
+
+
+
+
+
+ >
+ ) : null
+ }
+ {
+ (state == "REQUEST_SENT") ? (
+
+
+
+
+
+
+
+ Waiting for confirmations (see
+
+ transaction details
+
+ )
+
+
+
+ ) : null
+ }
+ {
+ (state == "CONFIRMED") ? (
+ <>
+
+
+
+
+ Transaction confirmed (see
+
+ transaction details
+
+ )
+
+
+
+
+
+
+
+ >
+ ) : null
+ }
+
+
+
+
+
+
+ )
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..3ddf43d
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,23 @@
+import type { Config } from 'tailwindcss'
+
+const config: Config = {
+ content: [
+ './node_modules/flowbite-react/**/*.js',
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
+ ],
+ theme: {
+ extend: {
+ backgroundImage: {
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+ 'gradient-conic':
+ 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
+ },
+ },
+ },
+ plugins: [
+ require("flowbite/plugin")
+ ],
+}
+export default config
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e59724b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
|