o2-tools 0.4.7-rc5

Reusable tooling for trade account and order book contract interactions on Fuel
//! Session-bound authorization for `PropMarginPool::force_close_session_signed`.
//!
//! The configured expiry cosigner signs this message; any relayer may submit it.
//! `pool` is the deployed pool/proxy ID, not the implementation or blob ID.
//! Cleanup inputs are unsigned. There is no deadline or separate nonce: the
//! authorization remains usable while this session is active and this signer is
//! configured. A reverted close may be retried with the same authorization.

use fuels::{
    crypto::Message,
    types::ContractId,
};
use sha2::{
    Digest,
    Sha256,
};

// Distinct from account approval (0x02), expiry (0x03), and account linking (0x04).
const FORCE_CLOSE_COSIGN_TAG: u8 = 0x05;

fn force_close_preimage(
    chain_id: u64,
    pool: ContractId,
    account: ContractId,
    session_id: u64,
) -> [u8; 81] {
    let mut bytes = [0; 81];
    bytes[0] = FORCE_CLOSE_COSIGN_TAG;
    bytes[1..9].copy_from_slice(&chain_id.to_be_bytes());
    bytes[9..41].copy_from_slice(pool.as_slice());
    bytes[41..73].copy_from_slice(account.as_slice());
    bytes[73..81].copy_from_slice(&session_id.to_be_bytes());
    bytes
}

/// SHA-256 of the 81-byte preimage:
/// `0x05 ++ chain_id_be_u64 ++ pool_raw32 ++ account_raw32 ++ session_id_be_u64`.
/// No length prefix, expiry, evaluation data, cleanup inputs, or relayer identity.
pub fn generate_pool_force_close_digest(
    chain_id: u64,
    pool: ContractId,
    account: ContractId,
    session_id: u64,
) -> [u8; 32] {
    Sha256::digest(force_close_preimage(chain_id, pool, account, session_id)).into()
}

/// The digest as a signable message, without hashing it a second time.
/// Matches Sway's `Message::from(digest)` and the pool expiry signing convention.
pub fn generate_pool_force_close_signing_payload(
    chain_id: u64,
    pool: ContractId,
    account: ContractId,
    session_id: u64,
) -> Message {
    Message::from_bytes(generate_pool_force_close_digest(
        chain_id, pool, account, session_id,
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn encoding_and_signable_message_match_the_sway_vector() {
        let chain = 0x0102_0304_0506_0708;
        let pool = ContractId::new([0x11; 32]);
        let account = ContractId::new([0x22; 32]);
        let session = 0x1122_3344_5566_7788;
        let bytes = force_close_preimage(chain, pool, account, session);
        assert_eq!(bytes.len(), 81);
        assert_eq!(&bytes[..9], &[5, 1, 2, 3, 4, 5, 6, 7, 8]);
        assert_eq!(&bytes[9..41], &[0x11; 32]);
        assert_eq!(&bytes[41..73], &[0x22; 32]);
        assert_eq!(
            &bytes[73..],
            &[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]
        );
        // Independently computed SHA-256; also asserted by the Sway unit test.
        let expected = [
            0x69, 0xca, 0x8d, 0x56, 0x99, 0x2a, 0x1b, 0xbc, 0xdd, 0xce, 0xc7, 0x3a, 0xb2,
            0x9c, 0x71, 0xcd, 0x9b, 0x99, 0xaf, 0xea, 0xe1, 0x92, 0x01, 0xbf, 0x97, 0x5a,
            0xcb, 0x5d, 0xee, 0x56, 0x7c, 0x36,
        ];
        assert_eq!(
            generate_pool_force_close_digest(chain, pool, account, session),
            expected
        );
        assert_eq!(
            generate_pool_force_close_signing_payload(chain, pool, account, session),
            Message::from_bytes(expected)
        );
    }
}