Skip to content

Commit 71d7a6b

Browse files
committed
API Server, allow spending of locked UTXOs from mempool txs with future block tolerance
1 parent f1801bc commit 71d7a6b

16 files changed

Lines changed: 358 additions & 145 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,6 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/
4242
database during initial block download or bootstrapping, which significantly increases
4343
its speed at the cost of a potential db corruption if the system crashes in the meantime.
4444

45-
- API Server:
46-
- new endpoints for addresses and transactions from the mempool:
47-
- `/mempool/transaction`
48-
- `/mempool/transaction/:id`
49-
- `/mempool/transaction/:id/output/:idx`
50-
- `/mempool/address/:address`
51-
- `/mempool/address/:address/all-utxos`
52-
5345
### Changed
5446
- Wallet RPC:
5547
- `wallet_info`: the structure of the returned field `extra_info` was changed.

api-server/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/
77
## [Unreleased]
88

99
### Added
10-
- New endpoint was added: `/v2/transaction/{id}/output/{idx}`.
10+
- New endpoint was added: `/v2/transaction/{id}/output/{idx}`, including an optional `with_mempool` parameter that controls the display of mempool spent status.
1111
- New endpoint was added: `/v2/token/{id}/transactions` will return all transactions related to a token.\
1212
Pagination works like the new absolute mode in `/v2/transaction`, using `offset` and `items`.
13+
- New endpoints for addresses and transactions from the mempool:
14+
- `/v2/mempool/transaction`
15+
- `/v2/mempool/transaction/:id`
16+
- `/v2/mempool/address/:address`
17+
- `/v2/mempool/address/:address/all-utxos`
1318

1419
### Changed
1520
- `/v2/token/ticker/{ticker}` will now return all tokens whose ticker has the specified `{ticker}`

api-server/api-server-common/src/storage/impls/in_memory/mod.rs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ struct ApiServerInMemoryStorage {
8484
orders_table: BTreeMap<OrderId, BTreeMap<BlockHeight, Order>>,
8585
mempool_transaction_table: BTreeMap<Id<Transaction>, TransactionInfo>,
8686
mempool_utxo_table: BTreeMap<UtxoOutPoint, (Utxo, bool)>,
87+
mempool_locked_utxo_table: BTreeMap<UtxoOutPoint, (Utxo, bool)>,
8788
mempool_address_utxos: BTreeMap<String, BTreeSet<UtxoOutPoint>>,
8889
mempool_address_balance_table: BTreeMap<String, BTreeMap<CoinOrTokenId, AmountWithDecimals>>,
8990
mempool_locked_address_balance_table:
@@ -123,6 +124,7 @@ impl ApiServerInMemoryStorage {
123124
orders_table: BTreeMap::new(),
124125
mempool_transaction_table: BTreeMap::new(),
125126
mempool_utxo_table: BTreeMap::new(),
127+
mempool_locked_utxo_table: BTreeMap::new(),
126128
mempool_address_utxos: BTreeMap::new(),
127129
mempool_address_balance_table: BTreeMap::new(),
128130
mempool_locked_address_balance_table: BTreeMap::new(),
@@ -875,6 +877,25 @@ impl ApiServerInMemoryStorage {
875877
self.get_utxo(outpoint.clone())
876878
}
877879

880+
fn get_mempool_locked_utxo_with_fallback(
881+
&self,
882+
outpoint: &UtxoOutPoint,
883+
) -> Result<Option<Utxo>, ApiServerStorageError> {
884+
if let Some(res) =
885+
self.mempool_locked_utxo_table.get(outpoint).map(|(utxo, _)| utxo.clone())
886+
{
887+
return Ok(Some(res));
888+
}
889+
890+
Ok(self
891+
.locked_utxo_table
892+
.get(&outpoint)
893+
.and_then(|by_height| by_height.values().last())
894+
.map(|locked_utxo| {
895+
Utxo::new_with_info(locked_utxo.utxo_with_extra_info().clone(), None)
896+
}))
897+
}
898+
878899
fn get_mempool_address_balance_with_fallback(
879900
&self,
880901
address: &str,
@@ -969,17 +990,11 @@ impl ApiServerInMemoryStorage {
969990
(!utxo.spent())
970991
.then_some((outpoint.clone(), utxo.utxo_with_extra_info().clone()))
971992
} else {
972-
Some((
973-
outpoint.clone(),
974-
self.locked_utxo_table
975-
.get(outpoint)
976-
.expect("must exit")
977-
.values()
978-
.last()
979-
.expect("not empty")
980-
.utxo_with_extra_info()
981-
.clone(),
982-
))
993+
self.get_mempool_locked_utxo_with_fallback(outpoint).expect("no error").map(
994+
|locked_utxo| {
995+
(outpoint.clone(), locked_utxo.utxo_with_extra_info().clone())
996+
},
997+
)
983998
}
984999
})
9851000
.collect();
@@ -1477,6 +1492,23 @@ impl ApiServerInMemoryStorage {
14771492
Ok(())
14781493
}
14791494

1495+
fn set_mempool_locked_utxo(
1496+
&mut self,
1497+
outpoint: UtxoOutPoint,
1498+
utxo: Utxo,
1499+
addresses: &[&str],
1500+
) -> Result<(), ApiServerStorageError> {
1501+
let spent = utxo.spent();
1502+
self.mempool_locked_utxo_table.insert(outpoint.clone(), (utxo, spent));
1503+
for address in addresses {
1504+
self.mempool_address_utxos
1505+
.entry((*address).into())
1506+
.or_default()
1507+
.insert(outpoint.clone());
1508+
}
1509+
Ok(())
1510+
}
1511+
14801512
fn set_mempool_address_balance(
14811513
&mut self,
14821514
address: &str,

api-server/api-server-common/src/storage/impls/in_memory/transactional/read.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,13 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRo<'_> {
325325
self.transaction.get_utxo_mempool_with_fallback(outpoint)
326326
}
327327

328+
async fn get_mempool_locked_utxo_with_fallback(
329+
&self,
330+
outpoint: &UtxoOutPoint,
331+
) -> Result<Option<Utxo>, ApiServerStorageError> {
332+
self.transaction.get_mempool_locked_utxo_with_fallback(outpoint)
333+
}
334+
328335
async fn get_mempool_address_balance_with_fallback(
329336
&self,
330337
address: &str,

api-server/api-server-common/src/storage/impls/in_memory/transactional/write.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,15 @@ impl ApiServerStorageWrite for ApiServerInMemoryStorageTransactionalRw<'_> {
329329
self.transaction.set_mempool_utxo(outpoint, utxo, addresses)
330330
}
331331

332+
async fn set_mempool_locked_utxo(
333+
&mut self,
334+
outpoint: UtxoOutPoint,
335+
utxo: Utxo,
336+
addresses: &[&str],
337+
) -> Result<(), ApiServerStorageError> {
338+
self.transaction.set_mempool_locked_utxo(outpoint, utxo, addresses)
339+
}
340+
332341
async fn set_mempool_address_balance(
333342
&mut self,
334343
address: &str,
@@ -702,6 +711,13 @@ impl ApiServerStorageRead for ApiServerInMemoryStorageTransactionalRw<'_> {
702711
self.transaction.get_utxo_mempool_with_fallback(outpoint)
703712
}
704713

714+
async fn get_mempool_locked_utxo_with_fallback(
715+
&self,
716+
outpoint: &UtxoOutPoint,
717+
) -> Result<Option<Utxo>, ApiServerStorageError> {
718+
self.transaction.get_mempool_locked_utxo_with_fallback(outpoint)
719+
}
720+
705721
async fn get_mempool_address_balance_with_fallback(
706722
&self,
707723
address: &str,

api-server/api-server-common/src/storage/impls/postgres/queries.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,15 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
10651065
)
10661066
.await?;
10671067

1068+
self.just_execute(
1069+
"CREATE TABLE ml.mempool_locked_utxo (
1070+
outpoint bytea PRIMARY KEY,
1071+
utxo bytea NOT NULL,
1072+
spent BOOLEAN NOT NULL DEFAULT FALSE
1073+
);",
1074+
)
1075+
.await?;
1076+
10681077
self.just_execute(
10691078
"CREATE TABLE ml.mempool_utxo_addresses (
10701079
outpoint bytea NOT NULL,
@@ -3299,6 +3308,43 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
32993308
self.get_utxo(outpoint.clone()).await
33003309
}
33013310

3311+
pub async fn get_mempool_locked_utxo_with_fallback(
3312+
&self,
3313+
outpoint: &UtxoOutPoint,
3314+
) -> Result<Option<Utxo>, ApiServerStorageError> {
3315+
let mempool_row = self
3316+
.tx
3317+
.query_opt(
3318+
r#"
3319+
(SELECT utxo, spent FROM ml.mempool_locked_utxo WHERE outpoint = $1 LIMIT 1)
3320+
UNION ALL
3321+
(SELECT utxo, false as spent FROM ml.locked_utxo WHERE outpoint = $1 LIMIT 1)
3322+
LIMIT 1
3323+
"#,
3324+
&[&outpoint.encode()],
3325+
)
3326+
.await
3327+
.map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?;
3328+
3329+
if let Some(row) = mempool_row {
3330+
let serialized_data: Vec<u8> = row.get(0);
3331+
let spent: bool = row.get(1);
3332+
3333+
let output =
3334+
UtxoWithExtraInfo::decode_all(&mut serialized_data.as_slice()).map_err(|e| {
3335+
ApiServerStorageError::DeserializationError(format!(
3336+
"Mempool Utxo deserialization failed: {}",
3337+
e
3338+
))
3339+
})?;
3340+
3341+
let spent = spent.then_some(UtxoSpent::InMempool);
3342+
return Ok(Some(Utxo::new_with_info(output, spent)));
3343+
}
3344+
3345+
return Ok(None);
3346+
}
3347+
33023348
pub async fn set_mempool_utxo(
33033349
&mut self,
33043350
outpoint: UtxoOutPoint,
@@ -3330,6 +3376,37 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
33303376
Ok(())
33313377
}
33323378

3379+
pub async fn set_mempool_locked_utxo(
3380+
&mut self,
3381+
outpoint: UtxoOutPoint,
3382+
utxo: Utxo,
3383+
addresses: &[&str],
3384+
) -> Result<(), ApiServerStorageError> {
3385+
let spent = utxo.spent();
3386+
self.tx
3387+
.execute(
3388+
"INSERT INTO ml.mempool_locked_utxo (outpoint, utxo, spent)
3389+
VALUES ($1, $2, $3)
3390+
ON CONFLICT (outpoint) DO UPDATE SET spent = EXCLUDED.spent, utxo = EXCLUDED.utxo;",
3391+
&[&outpoint.encode(), &utxo.utxo_with_extra_info().encode(), &spent],
3392+
)
3393+
.await
3394+
.map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?;
3395+
3396+
for address in addresses {
3397+
self.tx
3398+
.execute(
3399+
"INSERT INTO ml.mempool_utxo_addresses (outpoint, address) VALUES ($1, $2)
3400+
ON CONFLICT (outpoint, address) DO NOTHING;",
3401+
&[&outpoint.encode(), &address],
3402+
)
3403+
.await
3404+
.map_err(|e| ApiServerStorageError::LowLevelStorageError(e.to_string()))?;
3405+
}
3406+
3407+
Ok(())
3408+
}
3409+
33333410
pub async fn get_mempool_address_balance_with_fallback(
33343411
&self,
33353412
address: &str,
@@ -3790,6 +3867,7 @@ impl<'a, 'b> QueryFromConnection<'a, 'b> {
37903867
"TRUNCATE TABLE
37913868
ml.mempool_transactions,
37923869
ml.mempool_utxo,
3870+
ml.mempool_locked_utxo,
37933871
ml.mempool_utxo_addresses,
37943872
ml.mempool_address_balance,
37953873
ml.mempool_locked_address_balance,

api-server/api-server-common/src/storage/impls/postgres/transactional/read.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,16 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRo<'_> {
459459
Ok(res)
460460
}
461461

462+
async fn get_mempool_locked_utxo_with_fallback(
463+
&self,
464+
outpoint: &UtxoOutPoint,
465+
) -> Result<Option<Utxo>, ApiServerStorageError> {
466+
let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
467+
let res = conn.get_mempool_locked_utxo_with_fallback(outpoint).await?;
468+
469+
Ok(res)
470+
}
471+
462472
async fn get_mempool_address_balance_with_fallback(
463473
&self,
464474
address: &str,

api-server/api-server-common/src/storage/impls/postgres/transactional/write.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,18 @@ impl ApiServerStorageWrite for ApiServerPostgresTransactionalRw<'_> {
427427
Ok(())
428428
}
429429

430+
async fn set_mempool_locked_utxo(
431+
&mut self,
432+
outpoint: UtxoOutPoint,
433+
utxo: Utxo,
434+
addresses: &[&str],
435+
) -> Result<(), ApiServerStorageError> {
436+
let mut conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
437+
conn.set_mempool_locked_utxo(outpoint, utxo, addresses).await?;
438+
439+
Ok(())
440+
}
441+
430442
async fn set_mempool_address_balance(
431443
&mut self,
432444
address: &str,
@@ -948,6 +960,16 @@ impl ApiServerStorageRead for ApiServerPostgresTransactionalRw<'_> {
948960
Ok(res)
949961
}
950962

963+
async fn get_mempool_locked_utxo_with_fallback(
964+
&self,
965+
outpoint: &UtxoOutPoint,
966+
) -> Result<Option<Utxo>, ApiServerStorageError> {
967+
let conn = QueryFromConnection::new(self.connection.as_ref().expect(CONN_ERR));
968+
let res = conn.get_mempool_locked_utxo_with_fallback(outpoint).await?;
969+
970+
Ok(res)
971+
}
972+
951973
async fn get_mempool_address_balance_with_fallback(
952974
&self,
953975
address: &str,

api-server/api-server-common/src/storage/storage_api/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,10 @@ impl Utxo {
468468
self.spent.is_some()
469469
}
470470

471+
pub fn spent_in_mempool(&self) -> bool {
472+
matches!(self.spent, Some(UtxoSpent::InMempool))
473+
}
474+
471475
pub fn into_spent_in_mempool(mut self) -> Self {
472476
self.spent = Some(UtxoSpent::InMempool);
473477
self
@@ -822,6 +826,13 @@ pub trait ApiServerStorageRead: Sync {
822826
outpoint: &UtxoOutPoint,
823827
) -> Result<Option<Utxo>, ApiServerStorageError>;
824828

829+
// Returns the UTXO from the mempool if it exists,
830+
// otherwise fallbacks to getting it from the confirmed UTXOs.
831+
async fn get_mempool_locked_utxo_with_fallback(
832+
&self,
833+
outpoint: &UtxoOutPoint,
834+
) -> Result<Option<Utxo>, ApiServerStorageError>;
835+
825836
// Returns the balance of a coin or token for a given address from the mempool
826837
// if it exists, otherwise fallbacks to getting it from the confirmed address balances.
827838
async fn get_mempool_address_balance_with_fallback(
@@ -1088,6 +1099,13 @@ pub trait ApiServerStorageWrite: ApiServerStorageRead {
10881099
addresses: &[&str],
10891100
) -> Result<(), ApiServerStorageError>;
10901101

1102+
async fn set_mempool_locked_utxo(
1103+
&mut self,
1104+
outpoint: UtxoOutPoint,
1105+
utxo: Utxo,
1106+
addresses: &[&str],
1107+
) -> Result<(), ApiServerStorageError>;
1108+
10911109
async fn set_mempool_address_balance(
10921110
&mut self,
10931111
address: &str,

0 commit comments

Comments
 (0)