eth_twc_sdk/
verify.rs

1//! トランザクションレシート上の `TransferWithCommitmentSent` イベントの取得・検証。
2//!
3//! 信頼できる RPC から得たレシートを前提に、指定フィールドと一致するログの有無を確認します(チェーン最終性や悪意ある RPC の改ざんまでは防ぎません)。
4
5use alloy::primitives::{Address, B256, TxHash, U256};
6use alloy::providers::Provider;
7use alloy::sol_types::SolEvent;
8
9use crate::config::{self, assert_transfer_contract_configured};
10use crate::error::SdkError;
11use crate::events::TransferWithCommitmentSent;
12use crate::utils::assert_transfer_contract_deployed;
13
14/// [`verify`] に渡す、イベント内容との照合に使うフィールド集合。
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct VerifyArgs {
17    /// 送金元(ログの `from`)。
18    pub from: Address,
19    /// ERC-20 トークン(ログの `token`)。
20    pub token: Address,
21    /// 送金先(ログの `to`)。
22    pub to: Address,
23    /// 送金額(ログの `value`)。
24    pub value: U256,
25    /// コミットメント(ログの `commitment`)。
26    pub commitment: B256,
27}
28
29/// 指定トランザクションのレシートから、`TransferWithCommitmentSent` をすべてデコードして返す。
30///
31/// 該当ログがなければ **空ベクタ**を返し、エラーにはしません。
32///
33/// # 引数
34///
35/// * `public_provider` — レシート取得に使用するプロバイダ。
36/// * `hash` — 対象トランザクションのハッシュ。
37///
38/// # 戻り値
39///
40/// デコードに成功したイベント本体(`TransferWithCommitmentSent::decode_log` の戻り値の `.data`)の列。
41///
42/// # エラー
43///
44/// - [`crate::SdkError::ContractNotConfigured`]
45/// - [`crate::SdkError::ContractNotDeployed`]
46/// - [`crate::SdkError::ReceiptNotFound`]
47/// - RPC エラー([`crate::SdkError::Alloy`] など)
48pub async fn get_transfer_with_commitment_sent_event_logs<P: Provider>(
49    public_provider: &P,
50    hash: TxHash,
51) -> Result<Vec<TransferWithCommitmentSent>, SdkError> {
52    assert_transfer_contract_configured()?;
53    assert_transfer_contract_deployed(public_provider).await?;
54    let receipt = public_provider
55        .get_transaction_receipt(hash)
56        .await?
57        .ok_or(SdkError::ReceiptNotFound)?;
58    let mut out = Vec::new();
59    for log in receipt.logs() {
60        if log.address() != config::transfer_with_commitment_address() {
61            continue;
62        }
63        if let Ok(d) = TransferWithCommitmentSent::decode_log(&log.inner) {
64            out.push(d.data);
65        }
66    }
67    Ok(out)
68}
69
70/// レシート上に、[`VerifyArgs`] と一致する `TransferWithCommitmentSent` が **少なくとも 1 件**あることを検証する。
71///
72/// # 引数
73///
74/// * `public_provider` — レシート取得用プロバイダ。
75/// * `hash` — 検証対象トランザクションのハッシュ。
76/// * `args` — 期待する `from` / `token` / `to` / `value` / `commitment`。
77///
78/// # 戻り値
79///
80/// - `Ok(())` — 一致するログが 1 件以上ある。
81///
82/// # エラー
83///
84/// - [`crate::SdkError::ContractNotConfigured`]
85/// - [`crate::SdkError::ContractNotDeployed`]
86/// - [`crate::SdkError::ReceiptNotFound`]
87/// - [`crate::SdkError::EventNotFound`] — 一致するログが 0 件
88/// - RPC エラー
89pub async fn verify<P: Provider>(
90    public_provider: &P,
91    hash: TxHash,
92    args: &VerifyArgs,
93) -> Result<(), SdkError> {
94    assert_transfer_contract_configured()?;
95    assert_transfer_contract_deployed(public_provider).await?;
96    let receipt = public_provider
97        .get_transaction_receipt(hash)
98        .await?
99        .ok_or(SdkError::ReceiptNotFound)?;
100    for log in receipt.logs() {
101        if log.address() != config::transfer_with_commitment_address() {
102            continue;
103        }
104        let Ok(wrapped) = TransferWithCommitmentSent::decode_log(&log.inner) else {
105            continue;
106        };
107        let ev = wrapped.data;
108        if ev.from == args.from
109            && ev.token == args.token
110            && ev.to == args.to
111            && ev.value == args.value
112            && ev.commitment == args.commitment
113        {
114            return Ok(());
115        }
116    }
117    Err(SdkError::EventNotFound)
118}
119
120#[cfg(all(test, not(feature = "test-config")))]
121mod tests {
122    use alloy::primitives::{address, B256, TxHash, U256};
123    use alloy::providers::ProviderBuilder;
124    use serde_json::json;
125    use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};
126
127    use super::{get_transfer_with_commitment_sent_event_logs, verify, VerifyArgs};
128
129    async fn mock_rpc_chain_and_code(chain_id_hex: &str, code: &str) -> MockServer {
130        let server = MockServer::start().await;
131        let cid = chain_id_hex.to_string();
132        let code = code.to_string();
133        Mock::given(method("POST"))
134            .respond_with(move |req: &wiremock::Request| {
135                let body: serde_json::Value =
136                    serde_json::from_slice(&req.body).unwrap_or(json!({}));
137                let id = body.get("id").cloned().unwrap_or(json!(1));
138                let m = body.get("method").and_then(|x| x.as_str()).unwrap_or("");
139                let result = match m {
140                    "eth_chainId" => json!(cid.clone()),
141                    "eth_getCode" => json!(code.clone()),
142                    _ => json!(null),
143                };
144                ResponseTemplate::new(200).set_body_json(json!({
145                    "jsonrpc": "2.0",
146                    "id": id,
147                    "result": result
148                }))
149            })
150            .mount(&server)
151            .await;
152        server
153    }
154
155    fn sample_verify_args() -> VerifyArgs {
156        let a = address!("0x1111111111111111111111111111111111111111");
157        VerifyArgs {
158            from: a,
159            token: a,
160            to: a,
161            value: U256::from(1u64),
162            commitment: B256::repeat_byte(0xaa),
163        }
164    }
165
166    #[tokio::test]
167    async fn get_logs_fails_when_twc_not_deployed() {
168        let server = mock_rpc_chain_and_code("0x1", "0x").await;
169        let provider = ProviderBuilder::new().connect_http(server.uri().parse().unwrap());
170        let hash = TxHash::repeat_byte(0xab);
171        let r = get_transfer_with_commitment_sent_event_logs(&provider, hash).await;
172        assert!(matches!(
173            r,
174            Err(crate::SdkError::ContractNotDeployed { .. })
175        ));
176    }
177
178    #[tokio::test]
179    async fn verify_fails_when_twc_not_deployed() {
180        let server = mock_rpc_chain_and_code("0x1", "0x").await;
181        let provider = ProviderBuilder::new().connect_http(server.uri().parse().unwrap());
182        let hash = TxHash::repeat_byte(0xab);
183        let args = sample_verify_args();
184        let r = verify(&provider, hash, &args).await;
185        assert!(matches!(
186            r,
187            Err(crate::SdkError::ContractNotDeployed { .. })
188        ));
189    }
190}