feat(near): QTC into the intents account and back out to Quantus
ci / gate (push) Failing after 1m35s

The intents panel now moves QTC across the PoA bridge (#75).

In: the bridge's permanent Quantus deposit address for the account,
checked as a Quantus address before it is shown. "Send QTC here" opens
Send with it filled in (Send takes ?to= now), so a deposit is an
ordinary transfer reviewed and signed like any other.

Out: a withdrawal to one of the wallet's own Quantus accounts. It is the
ft_withdraw #72 proved, qtc.omft.near to itself with the WITHDRAW_TO:
memo:
- qtc_withdraw_call in wallet-core builds it and refuses anything under
  the bridge's 0.01 QTC minimum.
- The command checks the Intents balance and the sponsor's terms, signs
  a delegate action with the phrase's NEAR key (nonce one past the key's,
  valid for about 120 blocks), and hands it to the sponsor, which relays
  it and pays the gas.
- The bridge keeps 0.002 QTC, and the panel says so.

NearRpc::final_block now returns the height with the hash, for the
delegate's expiry. QtcBridge is the new port and PoaBridge its adapter.

Checked in a dev run with the dev-phrase account:
- the deposit address appeared (qzkgR6o1…);
- Send opened with it filled in;
- a 0.05 QTC withdrawal was refused because the account holds no QTC.
A live test (ignored) confirms the bridge gives #72's account the
address it used on mainnet. A full round trip with real QTC is still to
do before #75 closes.

Refs #75

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUx7SPonBnB3ofU4mH41WT
This commit is contained in:
2026-10-09 20:23:03 +03:00
co-authored by Claude Opus 5.5
parent 384c79446b
commit 52ac684661
13 changed files with 435 additions and 14 deletions
+1
View File
@@ -32,6 +32,7 @@ chrono.workspace = true
tokio.workspace = true
# Used by tauri::generate_context! for the plugins section of tauri.conf.json.
serde_json.workspace = true
bs58.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
anyhow.workspace = true
+120
View File
@@ -1093,6 +1093,126 @@ pub async fn near_account(
Ok(summary)
}
/// The open wallet's NEAR account name and key, derived from its phrase.
fn near_identity(state: &AppState, wallet: &str) -> Result<(String, String), WalletError> {
use wallet_near::{NEAR_DEFAULT_PATH, NearKey, sponsored_account};
state
.session
.with_seed(wallet, |seed| {
NearKey::derive(seed.as_bytes(), NEAR_DEFAULT_PATH).map(|k| {
(
sponsored_account(&k.handle_digest(), &state.near_network.sponsor),
k.key_string(),
)
})
})
.map_err(session_error)?
.ok_or_else(|| WalletError::Refused("this wallet has no phrase, so no NEAR key".into()))?
.map_err(|e| WalletError::Internal(e.to_string()))
}
/// Where to send QTC from Quantus to land in the open wallet's intents
/// account (#75): the account's permanent bridge deposit address, checked
/// as a Quantus address.
#[tauri::command]
pub async fn near_qtc_deposit_address(
state: State<'_, AppState>,
wallet: String,
) -> Result<String, WalletError> {
let (account, _) = near_identity(&state, &wallet)?;
state
.qtc_bridge
.deposit_address(&account)
.await
.map_err(|e| WalletError::Internal(format!("bridge: {e}")))
}
/// QTC out of the intents account and back to a Quantus address (#75): an
/// `ft_withdraw` with the bridge's memo, signed here as a delegate action
/// and relayed by the sponsor, which pays the gas. Returns the sponsor's
/// transaction hash. The bridge keeps 0.002 QTC.
#[tauri::command]
pub async fn near_withdraw_qtc(
state: State<'_, AppState>,
wallet: String,
amount: String,
to: String,
) -> Result<String, WalletError> {
use wallet_core::near::{INTENTS, QTC_DECIMALS, QTC_TOKEN, qtc_withdraw_call, usable_terms};
use wallet_near::delegate::DelegateAction;
use wallet_near::{NEAR_DEFAULT_PATH, NearKey};
let raw = Amount::parse(&amount, QTC_DECIMALS)
.map_err(WalletError::InvalidInput)?
.raw;
match ss58::decode(to.trim()) {
Ok((189, _)) => {}
_ => {
return Err(WalletError::InvalidInput(format!(
"{to} is not a Quantus address"
)));
}
}
let call = qtc_withdraw_call(to.trim(), raw).map_err(WalletError::InvalidInput)?;
let (account, key) = near_identity(&state, &wallet)?;
let near = |e: wallet_core::ports::NearError| WalletError::Internal(format!("NEAR: {e}"));
let held = state
.near
.view(
INTENTS,
"mt_batch_balance_of",
&serde_json::json!({ "account_id": account, "token_ids": [format!("nep141:{QTC_TOKEN}")] }),
)
.await
.map_err(near)?;
let held: u128 = held[0].as_str().and_then(|s| s.parse().ok()).unwrap_or(0);
if held < raw {
return Err(WalletError::InvalidInput(format!(
"the intents account holds {} QTC",
Amount::new(held, QTC_DECIMALS).to_decimal_string()
)));
}
let terms = state.near_sponsor.terms().await.map_err(near)?;
usable_terms(&terms, &state.near_network.sponsor).map_err(WalletError::Refused)?;
let nonce = state
.near
.access_key_nonce(&account, &key)
.await
.map_err(near)?
.ok_or_else(|| {
WalletError::Refused(format!("{account} does not carry this wallet's key"))
})?;
let (_, height) = state.near.final_block().await.map_err(near)?;
let signed = state
.session
.with_seed(&wallet, |seed| {
let k = NearKey::derive(seed.as_bytes(), NEAR_DEFAULT_PATH)?;
DelegateAction {
sender_id: account.clone(),
receiver_id: INTENTS.into(),
actions: vec![call],
nonce: nonce + 1,
// About two minutes of blocks for the sponsor to include it.
max_block_height: height + 120,
public_key: k.public_key().to_vec(),
}
.sign(&k)
})
.map_err(session_error)?
.ok_or_else(|| WalletError::Refused("this wallet has no phrase".into()))?
.map_err(|e| WalletError::Internal(e.to_string()))?;
let hash = state
.near_sponsor
.relay(&signed.encode())
.await
.map_err(|e| WalletError::Internal(format!("sponsor: {e}")))?;
let hash = bs58::encode(hash).into_string();
tracing::info!(%account, tx = %hash, "QTC withdrawal relayed");
Ok(hash)
}
/// Ask the sponsor to create the open wallet's NEAR Intents account (#81),
/// then read it back. The sponsor names the account from the key; a name
/// other than the one the wallet derived is refused rather than used.
+2
View File
@@ -78,6 +78,8 @@ pub fn run() {
commands::wormhole_summary,
commands::near_account,
commands::near_create_account,
commands::near_qtc_deposit_address,
commands::near_withdraw_qtc,
swap::swap_assets,
swap::swap_preview,
swap::swap_order_create,
+6 -2
View File
@@ -12,13 +12,14 @@ use wallet_core::assets::AssetRegistry;
use wallet_core::backup::BackupChallenge;
use wallet_core::near::NearNetwork;
use wallet_core::ports::{
FiatRamp, HistoryCache, HistorySource, NearRpc, NearSponsor, OrderStore, ShieldedCache,
FiatRamp, HistoryCache, HistorySource, NearRpc, NearSponsor, OrderStore, QtcBridge,
ShieldedCache,
};
use wallet_core::profiles::ProfileRegistry;
use wallet_core::session::Session;
use wallet_data::history::{ObserverHistory, SqliteHistoryCache};
use wallet_data::keystore::FileKeystore;
use wallet_data::near::{HttpNearRpc, HttpSponsor};
use wallet_data::near::{HttpNearRpc, HttpSponsor, PoaBridge};
use wallet_data::ramp::StubRamp;
use wallet_data::settings::SettingsStore;
use wallet_data::substrate::ChainManager;
@@ -66,6 +67,8 @@ pub struct AppState {
pub near: Arc<dyn NearRpc>,
/// The sponsor that creates the account and pays its gas (#81).
pub near_sponsor: Arc<dyn NearSponsor>,
/// The PoA bridge between Quantus and NEAR (#75).
pub qtc_bridge: Arc<dyn QtcBridge>,
pub near_network: NearNetwork,
}
@@ -138,6 +141,7 @@ impl AppState {
ramp: Arc::new(StubRamp::new()),
near: Arc::new(HttpNearRpc::new(near_network.clone())),
near_sponsor: Arc::new(HttpSponsor::new(&near_network.sponsor_api)),
qtc_bridge: Arc::new(PoaBridge::new(&near_network.bridge_rpc)),
near_network,
})
}
+70 -2
View File
@@ -35,6 +35,8 @@ pub struct NearNetwork {
pub sponsor: String,
/// The sponsor's API (`…/sponsor`, `…/accounts`, `…/relay`).
pub sponsor_api: String,
/// The PoA bridge's JSON-RPC (#75).
pub bridge_rpc: String,
}
impl NearNetwork {
@@ -112,6 +114,41 @@ pub fn usable_terms(
Ok((fee, recipient))
}
/// QTC on NEAR, as the PoA bridge mints it.
pub const QTC_TOKEN: &str = "qtc.omft.near";
/// QTC has 12 decimals on both sides of the bridge.
pub const QTC_DECIMALS: u8 = 12;
/// The bridge's smallest deposit or withdrawal: 0.01 QTC.
pub const QTC_BRIDGE_MIN: u128 = 10_000_000_000;
/// What the bridge keeps from a withdrawal back to Quantus: 0.002 QTC.
pub const QTC_WITHDRAW_FEE: u128 = 2_000_000_000;
/// The call that takes `amount` of QTC out of Intents and back to the
/// Quantus address `to`: an `ft_withdraw` of `qtc.omft.near` to itself with
/// the bridge's `WITHDRAW_TO:` memo, as #72 proved on mainnet. Refused
/// below the bridge's minimum; `to` must already be checked as a Quantus
/// address.
pub fn qtc_withdraw_call(to: &str, amount: u128) -> Result<wallet_near::tx::Action, String> {
if amount < QTC_BRIDGE_MIN {
return Err(format!(
"the bridge takes at least 0.01 QTC; {amount} base units ({}.{:012} QTC) is less",
amount / 10u128.pow(u32::from(QTC_DECIMALS)),
amount % 10u128.pow(u32::from(QTC_DECIMALS))
));
}
Ok(wallet_near::tx::Action::call(
"ft_withdraw",
&serde_json::json!({
"token": QTC_TOKEN,
"receiver_id": QTC_TOKEN,
"amount": amount.to_string(),
"memo": format!("WITHDRAW_TO:{to}"),
}),
100 * wallet_near::tx::TGAS,
1,
))
}
/// The NEAR Intents verifier contract.
pub const INTENTS: &str = "intents.near";
@@ -348,7 +385,7 @@ mod tests {
b.resize(n, "0");
Ok(serde_json::json!(b))
}
async fn final_block_hash(&self) -> Result<[u8; 32], NearError> {
async fn final_block(&self) -> Result<([u8; 32], u64), NearError> {
unreachable!()
}
async fn send(&self, _: &str, _: &[u8]) -> Result<[u8; 32], NearError> {
@@ -426,6 +463,37 @@ mod tests {
assert!(s.exists && !s.key_on_account);
}
#[test]
fn a_qtc_withdrawal_is_the_call_72_proved() {
let call = qtc_withdraw_call(
"qzoqjLjG3TEN9Abj71d1Ruq3Di71VctBq8pNwN2dHkh7fs1ZC",
35_000_000_000,
)
.unwrap();
let wallet_near::tx::Action::FunctionCall {
method,
args,
gas,
deposit,
} = call
else {
panic!("a function call");
};
assert_eq!(
(method.as_str(), gas, deposit),
("ft_withdraw", 100 * wallet_near::tx::TGAS, 1)
);
let a: serde_json::Value = serde_json::from_slice(&args).unwrap();
assert_eq!(a["token"], "qtc.omft.near");
assert_eq!(a["receiver_id"], "qtc.omft.near");
assert_eq!(a["amount"], "35000000000");
assert_eq!(
a["memo"],
"WITHDRAW_TO:qzoqjLjG3TEN9Abj71d1Ruq3Di71VctBq8pNwN2dHkh7fs1ZC"
);
assert!(qtc_withdraw_call("qz", QTC_BRIDGE_MIN - 1).is_err());
}
#[test]
fn terms_outside_what_the_wallet_accepts_are_refused() {
use crate::ports::SponsorTerms;
@@ -471,7 +539,7 @@ mod tests {
fn a_shard_url_without_its_placeholder_is_refused() {
assert!(
NearNetwork::from_toml(
"shard_url = \"https://x\"\nserved_shards = []\nfallback = [\"https://y\"]\nsponsor = \"s.near\"\nsponsor_api = \"https://z\""
"shard_url = \"https://x\"\nserved_shards = []\nfallback = [\"https://y\"]\nsponsor = \"s.near\"\nsponsor_api = \"https://z\"\nbridge_rpc = \"https://w\""
)
.is_err()
);
+3
View File
@@ -21,3 +21,6 @@ fallback = ["https://rpc.mainnet.fastnear.com", "https://rpc.mainnet.near.org"]
sponsor = "blackbeard-observer.near"
# Its API: GET terms, POST accounts, POST relay.
sponsor_api = "https://blackbeard.observer/v1/near"
# The PoA bridge that carries QTC between Quantus and NEAR (#75): one
# permanent Quantus deposit address per NEAR account.
bridge_rpc = "https://bridge.chaindefuser.com/rpc"
+11 -2
View File
@@ -352,8 +352,9 @@ pub trait NearRpc: Send + Sync {
args: &serde_json::Value,
) -> Result<serde_json::Value, NearError>;
/// A recent final block's hash, which a transaction must name.
async fn final_block_hash(&self) -> Result<[u8; 32], NearError>;
/// A recent final block's hash, which a transaction must name, and its
/// height, from which a delegate action's expiry is set.
async fn final_block(&self) -> Result<([u8; 32], u64), NearError>;
/// Submit a signed transaction from `signer`, returning once it is in a
/// final block (or refused). Returns its hash.
@@ -376,6 +377,14 @@ pub trait NearSponsor: Send + Sync {
async fn relay(&self, signed_delegate: &[u8]) -> Result<[u8; 32], NearError>;
}
/// The PoA bridge that carries QTC between Quantus and NEAR (#75). Each
/// NEAR account has one permanent Quantus deposit address; QTC sent there
/// is credited to the account inside Intents as `qtc.omft.near`.
#[async_trait]
pub trait QtcBridge: Send + Sync {
async fn deposit_address(&self, near_account: &str) -> Result<String, NearError>;
}
/// What the sponsor publishes about itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SponsorTerms {
+72
View File
@@ -0,0 +1,72 @@
//! The PoA bridge's JSON-RPC (#75): the permanent Quantus deposit address
//! of a NEAR account. The address is checked as a Quantus address before
//! anyone is told to send QTC to it.
use serde_json::json;
use wallet_core::ports::{NearError, QtcBridge};
pub struct PoaBridge {
client: reqwest::Client,
url: String,
}
impl PoaBridge {
pub fn new(url: &str) -> Self {
let client = reqwest::Client::builder()
.user_agent(concat!("blackbeard-wallet/", env!("CARGO_PKG_VERSION")))
.timeout(std::time::Duration::from_secs(20))
.build()
.expect("a TLS client can be built");
Self {
client,
url: url.to_string(),
}
}
}
#[async_trait::async_trait]
impl QtcBridge for PoaBridge {
async fn deposit_address(&self, near_account: &str) -> Result<String, NearError> {
let v: serde_json::Value = self
.client
.post(&self.url)
.json(
&json!({ "jsonrpc": "2.0", "id": 1, "method": "deposit_address",
"params": [{ "account_id": near_account, "chain": "qtc:mainnet" }] }),
)
.send()
.await
.map_err(|e| NearError::Unreachable(format!("bridge: {e}")))?
.json()
.await
.map_err(|e| NearError::BadAnswer(format!("bridge: {e}")))?;
let addr = v["result"]["address"].as_str().ok_or_else(|| {
NearError::BadAnswer(format!(
"bridge: no address in {}",
v.to_string().chars().take(200).collect::<String>()
))
})?;
match wallet_core::ss58::decode(addr) {
Ok((189, _)) => Ok(addr.to_string()),
_ => Err(NearError::BadAnswer(format!(
"bridge: {addr:?} is not a Quantus address; not using it"
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "reaches bridge.chaindefuser.com (reads an address only)"]
async fn the_bridge_gives_72s_account_the_address_it_used() {
let net = wallet_core::near::NearNetwork::shipped().unwrap();
let addr = PoaBridge::new(&net.bridge_rpc)
.deposit_address("bbspike72.near")
.await
.unwrap();
assert_eq!(addr, "qzm76XyesfmAeNsDjpUfcxPwdrVKuYFXtX2XMgyaDXqTVkqfn");
}
}
+2
View File
@@ -1,7 +1,9 @@
//! NEAR, the chain under the intents account (#71): RPC routed by shard.
pub mod bridge;
pub mod rpc;
pub mod sponsor;
pub use bridge::PoaBridge;
pub use rpc::{HttpNearRpc, NEAR_DECIMALS};
pub use sponsor::HttpSponsor;
+8 -4
View File
@@ -274,7 +274,7 @@ impl NearRpc for HttpNearRpc {
})
}
async fn final_block_hash(&self) -> Result<[u8; 32], NearError> {
async fn final_block(&self) -> Result<([u8; 32], u64), NearError> {
let v = self
.call(
&self.net.any_endpoints(),
@@ -282,11 +282,15 @@ impl NearRpc for HttpNearRpc {
json!({ "finality": "final" }),
)
.await?;
hash32(
let hash = hash32(
v["header"]["hash"]
.as_str()
.ok_or_else(|| NearError::BadAnswer("block without a hash".into()))?,
)
)?;
let height = v["header"]["height"]
.as_u64()
.ok_or_else(|| NearError::BadAnswer("block without a height".into()))?;
Ok((hash, height))
}
async fn send(&self, signer: &str, signed_tx: &[u8]) -> Result<[u8; 32], NearError> {
@@ -372,6 +376,6 @@ mod tests {
.await
.unwrap();
assert!(v.as_array().is_some_and(|a| a.len() == 1));
assert_eq!(rpc().final_block_hash().await.unwrap().len(), 32);
assert!(rpc().final_block().await.unwrap().1 > 219_000_000);
}
}
+11
View File
@@ -228,6 +228,17 @@ export function nearAccount(wallet: string): Promise<NearAccountSummary> {
return command<NearAccountSummary>('near_account', { wallet })
}
// QTC between Quantus and the intents account, over the PoA bridge. The
// deposit address is permanent per account; a withdrawal is signed in Rust
// and relayed by the sponsor, and returns its NEAR transaction hash.
export function nearQtcDepositAddress(wallet: string): Promise<string> {
return command<string>('near_qtc_deposit_address', { wallet })
}
export function nearWithdrawQtc(wallet: string, amount: string, to: string): Promise<string> {
return command<string>('near_withdraw_qtc', { wallet, amount, to })
}
// Ask the sponsor to create that account. Returns it, read back.
export function nearCreateAccount(wallet: string): Promise<NearAccountSummary> {
return command<NearAccountSummary>('near_create_account', { wallet })
+124 -3
View File
@@ -1,6 +1,14 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { copyText } from '../api/clipboard'
import { nearAccount, nearCreateAccount } from '../api/wallet'
import { useSession } from '../api/hooks'
import {
nearAccount,
nearCreateAccount,
nearQtcDepositAddress,
nearWithdrawQtc,
} from '../api/wallet'
import { formatAmount } from '../lib/format'
/**
@@ -22,6 +30,7 @@ export function NearIntentsPanel({ wallet }: { wallet: string }) {
onSuccess: (made) => qc.setQueryData(['near-account', wallet], made),
})
const a = account.data
const active = !!a?.exists && a.key_on_account
return (
<section className="panel">
<div className="panel-head">
@@ -68,8 +77,7 @@ export function NearIntentsPanel({ wallet }: { wallet: string }) {
)}
{a.exists && !a.key_on_account && (
<p className="error">
This account exists but this wallet’s key is not on it, so the wallet cannot use
it.
This account exists but this wallet’s key is not on it, so the wallet cannot use it.
</p>
)}
{a.exists && (
@@ -92,6 +100,7 @@ export function NearIntentsPanel({ wallet }: { wallet: string }) {
</tbody>
</table>
)}
{active && <QtcBridgeLegs wallet={wallet} onMoved={() => account.refetch()} />}
<div className="row">
<button className="button" type="button" onClick={() => copyText(a.account_id)}>
Copy account
@@ -111,3 +120,115 @@ export function NearIntentsPanel({ wallet }: { wallet: string }) {
</section>
)
}
/**
* QTC in and out of the intents account over the PoA bridge (#75). In: the
* account's permanent Quantus deposit address, paid with an ordinary send.
* Out: a withdrawal to one of this wallet's Quantus accounts, relayed by
* the sponsor; the bridge keeps 0.002 QTC.
*/
function QtcBridgeLegs({ wallet, onMoved }: { wallet: string; onMoved: () => void }) {
const navigate = useNavigate()
const session = useSession()
const mine =
session.data?.wallets
.find((w) => w.wallet.name === wallet)
?.accounts.filter((x) => x.current_scheme) ?? []
const [showDeposit, setShowDeposit] = useState(false)
const deposit = useQuery({
queryKey: ['qtc-deposit-address', wallet],
queryFn: () => nearQtcDepositAddress(wallet),
enabled: showDeposit,
staleTime: Infinity,
})
const [amount, setAmount] = useState('')
const [to, setTo] = useState('')
const withdraw = useMutation({
mutationFn: () => nearWithdrawQtc(wallet, amount, to || mine[0]?.address || ''),
onSuccess: () => {
setAmount('')
// The bridge pays out on Quantus within about a minute; the balance
// here drops as soon as the withdrawal is final.
setTimeout(onMoved, 3000)
},
})
return (
<div className="field" data-testid="qtc-bridge">
<label>QTC in and out</label>
{!showDeposit ? (
<button className="button" type="button" onClick={() => setShowDeposit(true)}>
Deposit QTC
</button>
) : (
<div>
{deposit.isPending && <p className="muted">Asking the bridge…</p>}
{deposit.isError && <p className="error">{deposit.error.message}</p>}
{deposit.data && (
<>
<div className="mono address">{deposit.data}</div>
<span className="muted" style={{ fontSize: 12 }}>
This account’s own deposit address on Quantus; it does not change. QTC sent here
arrives in NEAR Intents a few minutes later. At least 0.01 QTC.
</span>
<div className="row">
<button
className="button"
type="button"
onClick={() => navigate(`/send?to=${encodeURIComponent(deposit.data)}`)}
>
Send QTC here
</button>
<button className="button" type="button" onClick={() => copyText(deposit.data)}>
Copy
</button>
</div>
</>
)}
</div>
)}
<form
className="row"
onSubmit={(e) => {
e.preventDefault()
withdraw.mutate()
}}
>
<input
className="input numeral"
placeholder="QTC to withdraw"
value={amount}
onChange={(e) => setAmount(e.target.value)}
inputMode="decimal"
aria-label="QTC to withdraw"
/>
<select
className="input"
value={to || mine[0]?.address || ''}
onChange={(e) => setTo(e.target.value)}
aria-label="Withdraw to"
>
{mine.map((x) => (
<option key={x.address} value={x.address}>
Account {(x.account ?? 0) + 1} · {x.address.slice(0, 10)}…
</option>
))}
</select>
<button className="button" type="submit" disabled={!amount || withdraw.isPending}>
{withdraw.isPending ? 'Withdrawing…' : 'Withdraw to Quantus'}
</button>
</form>
<span className="muted" style={{ fontSize: 12 }}>
The bridge keeps 0.002 QTC of each withdrawal; at least 0.01 QTC. The wallet’s sponsor pays
the NEAR fee.
</span>
{withdraw.isError && <p className="error">{withdraw.error.message}</p>}
{withdraw.data && (
<p className="muted">
Withdrawal relayed (NEAR transaction{' '}
<span className="mono">{withdraw.data.slice(0, 12)}…</span>). The QTC reaches Quantus in
about a minute.
</p>
)}
</div>
)
}
+5 -1
View File
@@ -1,4 +1,5 @@
import { useState, type FormEvent } from 'react'
import { useSearchParams } from 'react-router-dom'
import { useBalances, useChains, useSession, useSettings } from '../api/hooks'
import type { PreparedTransferInfo } from '../api/generated/PreparedTransferInfo'
import { transferPrepare } from '../api/wallet'
@@ -27,7 +28,10 @@ export function Send() {
const accounts = session.data?.accounts ?? []
const [from, setFrom] = useState('')
const [to, setTo] = useState('')
// `?to=` prefills the recipient: the intents panel sends here to deposit
// QTC at the bridge's address for the account.
const [params] = useSearchParams()
const [to, setTo] = useState(params.get('to') ?? '')
const [amount, setAmount] = useState('')
const [window, setWindow] = useState<'none' | 'custom' | number>('none')
const [customBlocks, setCustomBlocks] = useState('')