Merge branch 'moria-v11-test18' into 'master'
moria v1-1: TEST18 categories and riftenlabs-defi 0.5.1 See merge request riftenlabs/riftenlabs-indexer!103
This commit is contained in:
commit
fb6f417759
6 changed files with 254 additions and 41 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -2283,9 +2283,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "riftenlabs-defi"
|
||||
version = "0.4.6"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcf89dd770556a74ecd88b6971e7fa151f58bcee5b1a400b836c485c668973b"
|
||||
checksum = "e7541d5d2101a03925b9de89846c1e9a66bcdcdf6a4435a7bcf30bf294bb236f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin_hashes",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ serde_json = "1.0.133"
|
|||
rocket = { version = "0.5.1", features = ["json"] }
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
rayon = "1.10.0"
|
||||
riftenlabs-defi = "0.4.6"
|
||||
riftenlabs-defi = "0.5.1"
|
||||
rocket_cors = "0.6.0"
|
||||
log = "0.4"
|
||||
stderrlog = "0.6.0"
|
||||
|
|
|
|||
|
|
@ -3,40 +3,41 @@
|
|||
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
||||
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
//! Per-network Moria v1-1 deployment constants.
|
||||
//! Per-network Moria v1-1 TEST18 deployment constants.
|
||||
//!
|
||||
//! Mainnet is unconfigured until a v1-1 deployment exists (fail-closed).
|
||||
//! Chipnet is TEST18. Mainnet is unconfigured until a v1-1 deployment exists
|
||||
//! (fail-closed).
|
||||
|
||||
use bitcoincash::{Network, TokenID};
|
||||
use riftenlabs_defi::moria_v1_1::MoriaV11TokenIds;
|
||||
|
||||
/// Unique tail of the moria v1-1 redeem script. Partial scriptsig match —
|
||||
/// every moria method spends this body.
|
||||
/// Unique tail of the moria v1-1 TEST18 redeem script. Partial scriptsig match —
|
||||
/// every moria method spends this body. Last 20 bytes of REDEEMSCRIPT_PROD.
|
||||
const REDEEM_TAIL: [u8; 20] = [
|
||||
0x51, 0xd2, 0x51, 0xcf, 0x88, 0x52, 0xd1, 0x00, 0x88, 0xc4, 0x53, 0x9c, 0x77, 0x77, 0x77, 0x68,
|
||||
0xd2, 0x51, 0xcf, 0x88, 0x52, 0xd1, 0x00, 0x88, 0xc4, 0x53, 0x9d, 0x6d, 0x6d, 0x51, 0x68, 0x68,
|
||||
0x68, 0x68, 0x68, 0x68,
|
||||
];
|
||||
|
||||
/// Chipnet v1-1 token categories and loan P2SH32, or `None` on other networks.
|
||||
/// Chipnet TEST18 token categories and loan P2SH32, or `None` on other networks.
|
||||
pub fn token_ids(network: Option<Network>) -> Option<MoriaV11TokenIds> {
|
||||
match network {
|
||||
Some(Network::Chipnet) => Some(MoriaV11TokenIds {
|
||||
moria: "56d909e16cd3b4cecae685a7ca45baa292bf7550205c2ad59320a342c73754de"
|
||||
moria: "1c6dc7c7ad3e7e37fa1c84e715cd22a28177f52a2c49853e6e0b638505c29637"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid chipnet moria v1-1 token_id"),
|
||||
bp_oracle: "a0628bc21d10211a23f986fd12770e836bae43878db93db8b164ab8a5ab33c96"
|
||||
.expect("valid chipnet TEST18 moria token_id"),
|
||||
bp_oracle: "640d2c75cb1d5d15dbaf26022b37661d3578485a6788736893c1331ed2384905"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid chipnet bp_oracle v1-1 token_id"),
|
||||
pool: "a533b7e96435af18136b534b43e465dd759db3ac33e8490b9bfd254b8c27ae48"
|
||||
.expect("valid chipnet TEST18 bp_oracle token_id"),
|
||||
pool: "8aaa8012b206f45b33472a55b5c3c328bae0eb33e4ee466f7f9cbade4750225c"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid chipnet pool v1-1 token_id"),
|
||||
bar: "bc98b2733ca1c2070586cce1a4216418830ccd9b19ff77b5d43d47542bb3b50d"
|
||||
.expect("valid chipnet TEST18 pool token_id"),
|
||||
bar: "779df3e91141f0ea414c477f61d08a3c55b69cd91eb58e42953d8c7eaec84bb9"
|
||||
.parse::<TokenID>()
|
||||
.expect("valid chipnet bar v1-1 token_id"),
|
||||
.expect("valid chipnet TEST18 bar token_id"),
|
||||
loan_locking_bytecode: hex::decode(
|
||||
"aa20415812e068086c90686635a813d77b6c412427e574e89b16e79feeaa48cf0f0787",
|
||||
"aa208bbc3b3f65b354e69a93175c70f3cbb619154ae2902878521c17edcbd83cc12f87",
|
||||
)
|
||||
.expect("valid chipnet loan locking bytecode"),
|
||||
.expect("valid chipnet TEST18 loan locking bytecode"),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -544,7 +544,7 @@ pub async fn index_moria_v11(
|
|||
action.loan_commitment = Some(LoanCommitmentV11 {
|
||||
borrower_nft_hash: borrower,
|
||||
delegate_nft_hash: delegate,
|
||||
principal: principal as u16,
|
||||
principal: principal as u32,
|
||||
annual_interest_rate_bp: interest_rate as u16,
|
||||
timestamp: loan_ts as u64,
|
||||
});
|
||||
|
|
@ -565,7 +565,10 @@ pub async fn index_moria_v11(
|
|||
}
|
||||
}
|
||||
|
||||
// Create / replace continuing loan UTXO.
|
||||
// Create / replace continuing loan UTXO. Refinance, borrow, retarget,
|
||||
// add-collateral, and partial redeem/repay (continuation NFT via
|
||||
// `new_loan_commitment` + `loan_output_index`) all land here. Full
|
||||
// close has no continuation and the spent UTXO stays deleted.
|
||||
if let Some(output_idx) = action.loan_output_index {
|
||||
if let Some(commitment) = action
|
||||
.new_loan_commitment
|
||||
|
|
@ -581,7 +584,6 @@ pub async fn index_moria_v11(
|
|||
blockhash,
|
||||
)
|
||||
.await?;
|
||||
// Policy lives on the allowance, revealed at first spend.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,8 @@ pub struct MoriaV11Entry {
|
|||
pub action_type: &'static str,
|
||||
pub borrower_hash: Option<String>,
|
||||
pub delegate_hash: Option<String>,
|
||||
/// Encoded principal in whole MUSD (× [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units).
|
||||
/// Whole-MUSD principal (`i64`, not `u16`; 128_000 fits). Multiply by
|
||||
/// [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units.
|
||||
pub principal: Option<i64>,
|
||||
pub interest_rate: Option<i64>,
|
||||
pub loan_timestamp: Option<i64>,
|
||||
|
|
@ -71,7 +72,8 @@ pub struct MoriaV11LoanUtxo {
|
|||
pub vout: i64,
|
||||
pub borrower_hash: String,
|
||||
pub delegate_hash: String,
|
||||
/// Encoded principal in whole MUSD (× [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units).
|
||||
/// Whole-MUSD principal (`i64`, not `u16`; 128_000 fits). Multiply by
|
||||
/// [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units.
|
||||
pub principal: i64,
|
||||
pub interest_rate: i64,
|
||||
pub loan_timestamp: i64,
|
||||
|
|
@ -117,7 +119,8 @@ pub struct MoriaV11Crankable {
|
|||
pub vout: i64,
|
||||
pub borrower_hash: String,
|
||||
pub delegate_hash: String,
|
||||
/// Encoded principal in whole MUSD (× [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units).
|
||||
/// Whole-MUSD principal (`i64`, not `u16`; 128_000 fits). Multiply by
|
||||
/// [`MUSD_BASE_UNITS_PER_MUSD`](riftenlabs_defi::moria_v1_1::MUSD_BASE_UNITS_PER_MUSD) for base units.
|
||||
pub principal: i64,
|
||||
pub interest_rate: i64,
|
||||
pub loan_timestamp: i64,
|
||||
|
|
|
|||
|
|
@ -15,16 +15,17 @@ use riftenlabs_defi::moria_v1_1::{
|
|||
crank_allowance_locking_bytecode, crank_hint_to_policy, decode_crank_hint,
|
||||
encode_loan_commitment_v11, principal_to_base_units, CrankPolicy, LoanCommitmentV11,
|
||||
MoriaV11TokenIds, ALLOWANCE_METHOD_CLOSE, ALLOWANCE_METHOD_CRANK, DEFAULT_DOWN_LEG_SECONDS,
|
||||
LOAN_METHOD_RETARGET,
|
||||
LOAN_METHOD_REDEEM, LOAN_METHOD_REPAY, LOAN_METHOD_RETARGET,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
/// Whole-MUSD → base units as i64 (DB/API). Scale lives only in defi `MUSD_BASE_UNITS_PER_MUSD`.
|
||||
fn principal_base_units(principal_musd: u16) -> i64 {
|
||||
fn principal_base_units(principal_musd: u32) -> i64 {
|
||||
principal_to_base_units(principal_musd) as i64
|
||||
}
|
||||
|
||||
const MORIA_METHOD_BORROW: u8 = 2;
|
||||
const MORIA_METHOD_REPAY: u8 = 0;
|
||||
const MORIA_METHOD_RETARGET: u8 = 4;
|
||||
|
||||
async fn setup(pool: SqlitePool) {
|
||||
|
|
@ -46,7 +47,7 @@ fn token_ids() -> MoriaV11TokenIds {
|
|||
}
|
||||
|
||||
fn test_loan_lock() -> Vec<u8> {
|
||||
hex::decode("aa20415812e068086c90686635a813d77b6c412427e574e89b16e79feeaa48cf0f0787").unwrap()
|
||||
hex::decode("aa208bbc3b3f65b354e69a93175c70f3cbb619154ae2902878521c17edcbd83cc12f87").unwrap()
|
||||
}
|
||||
|
||||
fn token_ids_with_loan_lock() -> MoriaV11TokenIds {
|
||||
|
|
@ -194,7 +195,7 @@ fn make_tx(inputs: Vec<TxIn>, outputs: Vec<TxOut>) -> Transaction {
|
|||
}
|
||||
}
|
||||
|
||||
fn commitment(borrower: [u8; 32], delegate: [u8; 32], principal: u16, bp: u16, ts: u64) -> Vec<u8> {
|
||||
fn commitment(borrower: [u8; 32], delegate: [u8; 32], principal: u32, bp: u16, ts: u64) -> Vec<u8> {
|
||||
encode_loan_commitment_v11(&LoanCommitmentV11 {
|
||||
borrower_nft_hash: borrower,
|
||||
delegate_nft_hash: delegate,
|
||||
|
|
@ -508,10 +509,8 @@ async fn index_crank_updates_allowance_in_place() {
|
|||
.unwrap();
|
||||
|
||||
let old = commitment(borrower(), delegate(), 1_000, 500, 1_700_000_000);
|
||||
let mut new = old.clone();
|
||||
new[66] = 0x20;
|
||||
new[67] = 0x03;
|
||||
new[68] = 0x20;
|
||||
// 75-byte: rate @67, timestamp @69 (frozen head [0,67) includes principal).
|
||||
let new = commitment(borrower(), delegate(), 1_000, 0x0320, 1_700_000_016);
|
||||
|
||||
let interest = 15i64;
|
||||
let allowance_remaining = 500i64;
|
||||
|
|
@ -667,18 +666,13 @@ async fn index_two_loan_retarget_batch() {
|
|||
let bh = blockhash(0xCC);
|
||||
|
||||
let old0 = commitment(borrower(), delegate(), 1_000, 500, 1_700_000_000);
|
||||
let mut new0 = old0.clone();
|
||||
new0[66] = 0xF4;
|
||||
new0[67] = 0x01;
|
||||
new0[68] = 0x10;
|
||||
// 75-byte layout: rate @67 and timestamp @69 may change; frozen head [0,67).
|
||||
let new0 = commitment(borrower(), delegate(), 1_000, 500, 1_700_000_016);
|
||||
|
||||
let borrower2 = [0x33; 32];
|
||||
let delegate2 = [0x44; 32];
|
||||
let old1 = commitment(borrower2, delegate2, 2_000, 600, 1_700_000_000);
|
||||
let mut new1 = old1.clone();
|
||||
new1[66] = 0x58;
|
||||
new1[67] = 0x02;
|
||||
new1[68] = 0x10;
|
||||
let new1 = commitment(borrower2, delegate2, 2_000, 600, 1_700_000_016);
|
||||
|
||||
let seed_txid0 = Txid::from_byte_array([0xA0; 32]);
|
||||
let seed_txid1 = Txid::from_byte_array([0xA1; 32]);
|
||||
|
|
@ -1086,3 +1080,216 @@ async fn mempool_eviction_drops_unconfirmed_loan() {
|
|||
assert!(get_active_loans(pool).await.unwrap().is_empty());
|
||||
assert!(get_unconfirmed_txids(pool).await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
fn partial_repay_tx(
|
||||
ids: &MoriaV11TokenIds,
|
||||
remaining: Vec<u8>,
|
||||
remaining_sats: u64,
|
||||
) -> Transaction {
|
||||
make_tx(
|
||||
vec![
|
||||
txin(0x01, 0, unlock(MORIA_METHOD_REPAY, &[])),
|
||||
txin(0x02, 0, unlock(0, &[])),
|
||||
txin(0xA0, 2, unlock(LOAN_METHOD_REPAY, &[])),
|
||||
txin(0xB0, 0, ScriptBuf::new()),
|
||||
],
|
||||
vec![
|
||||
out(1_000, Some(minting(ids.moria, vec![0x02], 1_100_000))),
|
||||
out(1_000, Some(nft(tid(0xD1), vec![0x11; 16]))),
|
||||
out(1_000, Some(ft(ids.moria, 42))),
|
||||
bare(1_000),
|
||||
out(remaining_sats, Some(nft(ids.moria, remaining))),
|
||||
bare(1_000),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn partial_redeem_tx(
|
||||
ids: &MoriaV11TokenIds,
|
||||
remaining: Vec<u8>,
|
||||
remaining_sats: u64,
|
||||
) -> Transaction {
|
||||
make_tx(
|
||||
vec![
|
||||
txin(0x01, 0, unlock(MORIA_METHOD_REPAY, &[])),
|
||||
txin(0x02, 0, unlock(0, &[])),
|
||||
txin(0xA0, 2, unlock(LOAN_METHOD_REDEEM, &[])),
|
||||
txin(0xB2, 0, ScriptBuf::new()),
|
||||
],
|
||||
vec![
|
||||
out(1_000, Some(minting(ids.moria, vec![0x02], 1_050_000))),
|
||||
out(1_000, Some(nft(tid(0xD1), vec![0x11; 16]))),
|
||||
out(1_000, Some(ft(ids.moria, 77))),
|
||||
out(1_000, Some(nft(ids.bp_oracle, vec![0x22; 26]))),
|
||||
out(remaining_sats, Some(nft(ids.moria, remaining))),
|
||||
bare(1_000),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn partial_repay_keeps_continuation_loan_utxo() {
|
||||
let db = crate::utiltest::mock_db_pool(setup).await;
|
||||
let pool = &db.moria_w;
|
||||
let ids = token_ids();
|
||||
let bh = blockhash(0xE1);
|
||||
let seed = Txid::from_byte_array([0xA0; 32]);
|
||||
|
||||
insert_loan_utxo(
|
||||
pool,
|
||||
&seed,
|
||||
2,
|
||||
&LoanCommitmentV11 {
|
||||
borrower_nft_hash: borrower(),
|
||||
delegate_nft_hash: delegate(),
|
||||
principal: 20_000,
|
||||
annual_interest_rate_bp: 500,
|
||||
timestamp: 1_700_000_000,
|
||||
},
|
||||
9_000_000,
|
||||
&bh,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let remaining = commitment(borrower(), delegate(), 15_000, 500, 1_700_000_000);
|
||||
let tx = partial_repay_tx(&ids, remaining, 6_000_000);
|
||||
let n = index_moria_v11(pool, &[tx.clone()], &bh, 1_700_100_000, &ids)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(n, 1);
|
||||
|
||||
let history = get_loan_history(pool, &borrower()).await.unwrap();
|
||||
assert_eq!(history.len(), 1);
|
||||
assert_eq!(history[0].action_type, "repay");
|
||||
assert_eq!(history[0].new_principal, Some(15_000));
|
||||
|
||||
let active = get_active_loans(pool).await.unwrap();
|
||||
assert_eq!(
|
||||
active.len(),
|
||||
1,
|
||||
"partial repay must keep the continuation UTXO"
|
||||
);
|
||||
assert_eq!(active[0].principal, 15_000);
|
||||
assert_eq!(active[0].txid, tx.compute_txid().to_string());
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn partial_redeem_keeps_continuation_loan_utxo() {
|
||||
let db = crate::utiltest::mock_db_pool(setup).await;
|
||||
let pool = &db.moria_w;
|
||||
let ids = token_ids();
|
||||
let bh = blockhash(0xE2);
|
||||
let seed = Txid::from_byte_array([0xA0; 32]);
|
||||
|
||||
insert_loan_utxo(
|
||||
pool,
|
||||
&seed,
|
||||
2,
|
||||
&LoanCommitmentV11 {
|
||||
borrower_nft_hash: borrower(),
|
||||
delegate_nft_hash: delegate(),
|
||||
principal: 20_000,
|
||||
annual_interest_rate_bp: 500,
|
||||
timestamp: 1_700_000_000,
|
||||
},
|
||||
9_000_000,
|
||||
&bh,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let remaining = commitment(borrower(), delegate(), 10_000, 500, 1_700_000_000);
|
||||
let tx = partial_redeem_tx(&ids, remaining, 4_000_000);
|
||||
index_moria_v11(pool, &[tx.clone()], &bh, 1_700_100_000, &ids)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let history = get_loan_history(pool, &borrower()).await.unwrap();
|
||||
assert_eq!(history[0].action_type, "redeem");
|
||||
assert_eq!(history[0].new_principal, Some(10_000));
|
||||
|
||||
let active = get_active_loans(pool).await.unwrap();
|
||||
assert_eq!(
|
||||
active.len(),
|
||||
1,
|
||||
"partial redeem must keep the continuation UTXO"
|
||||
);
|
||||
assert_eq!(active[0].principal, 10_000);
|
||||
assert_eq!(active[0].txid, tx.compute_txid().to_string());
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn full_repay_drops_loan_utxo() {
|
||||
let db = crate::utiltest::mock_db_pool(setup).await;
|
||||
let pool = &db.moria_w;
|
||||
let ids = token_ids();
|
||||
let bh = blockhash(0xE3);
|
||||
let seed = Txid::from_byte_array([0xA0; 32]);
|
||||
|
||||
insert_loan_utxo(
|
||||
pool,
|
||||
&seed,
|
||||
2,
|
||||
&LoanCommitmentV11 {
|
||||
borrower_nft_hash: borrower(),
|
||||
delegate_nft_hash: delegate(),
|
||||
principal: 20_000,
|
||||
annual_interest_rate_bp: 500,
|
||||
timestamp: 1_700_000_000,
|
||||
},
|
||||
9_000_000,
|
||||
&bh,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tx = make_tx(
|
||||
vec![
|
||||
txin(0x01, 0, unlock(MORIA_METHOD_REPAY, &[])),
|
||||
txin(0x02, 0, unlock(0, &[])),
|
||||
txin(0xA0, 2, unlock(LOAN_METHOD_REPAY, &[])),
|
||||
txin(0xB0, 0, ScriptBuf::new()),
|
||||
],
|
||||
vec![
|
||||
out(1_000, Some(minting(ids.moria, vec![0x02], 1_100_000))),
|
||||
out(1_000, Some(nft(tid(0xD1), vec![0x11; 16]))),
|
||||
out(1_000, Some(ft(ids.moria, 42))),
|
||||
bare(1_000),
|
||||
bare(5_000_000),
|
||||
bare(1_000),
|
||||
],
|
||||
);
|
||||
index_moria_v11(pool, &[tx], &bh, 1_700_100_000, &ids)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(get_active_loans(pool).await.unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn principal_128000_fits_loan_utxo() {
|
||||
let db = crate::utiltest::mock_db_pool(setup).await;
|
||||
let pool = &db.moria_w;
|
||||
let bh = blockhash(0xE4);
|
||||
insert_loan_utxo(
|
||||
pool,
|
||||
&Txid::from_byte_array([0xC8; 32]),
|
||||
2,
|
||||
&LoanCommitmentV11 {
|
||||
borrower_nft_hash: borrower(),
|
||||
delegate_nft_hash: delegate(),
|
||||
principal: 128_000,
|
||||
annual_interest_rate_bp: 500,
|
||||
timestamp: 1_700_000_000,
|
||||
},
|
||||
50_000_000,
|
||||
&bh,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let active = get_active_loans(pool).await.unwrap();
|
||||
assert_eq!(active.len(), 1);
|
||||
assert_eq!(active[0].principal, 128_000);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue