Symbol ブロックチェーン 履歴 取得 timestamp [検索] \ (´・ω・｀)

# はじめに
+ **timestamp（日付、2024/06/13 etc...）で指定した期間のトランザクション履歴だけ取得したいのにブロック高での絞り込みしかできない...(´・ω・｀)**
+ **ブロック高の計算めんど...(´・ω・｀)**

というお困りごと解消備忘録です。

# 待望のアップデート
2024年4月に公開されたアップデートにより、**ブロック情報取得時にtimestampを利用してフィルタリングできる**ようになりました。ブロック高計算からの卒業。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/70a41316-20f9-eba0-5b62-0ef5fdb20347.png)

https://github.com/symbol/symbol/blob/dev/client/rest/CHANGELOG.md

####
ソースコードを追うと**fromTimestamp**, **toTimestamp** を指定してやることで抽出できるようです。早速やっていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/0f58dba5-8665-f91c-6a4f-0eb9a5724076.png)

参考

https://qiita.com/Toshi_ma/items/2cb5d6eb57b3469d995f

# 一連の流れ
1. 日付をUNIX時間に変換する（timestamp）
2. timestampでブロック高を取得する
3. ブロック高で指定期間内の履歴を抽出する

# やってみる
### 日付をUNIX時間に変換する + timestampでブロック高を取得する
まずは履歴の検索開始ブロック高（fromBlockHeight）と検索終了ブロック高（toBlockHeight）を取得するため、日付をtimestampに変換します。

**ただしこのtimestamp、変換の際に少し工夫する必要がありまして。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/93f052b4-2e85-fae5-d5de-a33006e1231c.png)

<dl>
  <dt>ネメシスブロック（Nemesis Block）</dt>
  <dd>1番目のブロック、他チェーンでジェネシスブロックと呼ばれるもの</dd>
  <dt>epochAdjustment</dt>
  <dd>ネメシスブロックのtimestamp（秒）</dd>
  <dd>　・Symbolメインネット:  1615853185</dd>
  <dd>　・Symbolテストネット:  1667250467</dd>
</dl>

####
つまり、**指定したUNIX時間①からepochAdjustment分②を差し引いてやる**ことでブロック情報の取得に用いるtimestamp③を取得することができます。詳細はこちらから↓

https://qiita.com/villhell/items/bfee8a68b829f3df1ede

<details><summary>実際にtimestampを使って得られた値</summary>


https://sym-main-03.opening-line.jp:3001/blocks?fromTimestamp=98636015000&toTimestamp=98636075000


```json
{
  "meta":{
    "hash":"4E6718A70BF60920BF25D948B1A611247A4896A4DC4E17A4C2691BDD5543641C",
    "generationHash":"FF64D231C2CB153B159CFF140DBCF05063BB17AE2D8586E55BF0253EA5FC8866",
    "totalFee":"31525",
    "totalTransactionsCount":4,
    "stateHashSubCacheMerkleRoots":[
      "95E4666A459371F3669A4FF676B99B929B85CA6540A3E206B75437E30B561820",
      "84E4E37EF1FC37A2CC23B8E56B8A2E46268A2F5A0C97B51FE274547E2BA0260D",
      ...,
    ],
    "transactionsCount":4,
    "statementsCount":1
  },
  "block":{
    "size":1639,
    "signature":"BC7351C34DE14B4216B27C3CE21E4982AF272864C56D16B3A45E469E193A2678C0C33336D5E7CFD59504D2E478EA32581E217208140AFDA97F437B4AEEA74E0C",
    "signerPublicKey":"F862AB32216DE74697E939748755C519C5B461DF7F5AFFB96A5D07585879C1E9",
    "version":1,
    "network":104,
    "type":33091,
    "height":"3282027",
    "timestamp":"98636031099",★★★
    "difficulty":"131198718152991",
    "proofGamma":"6E154716018C8FBBA15DD10CB1CB084388AB141BB13FFC5F7043A4406844019E",
    "proofVerificationHash":"D01B3A7B133C8082C99509375D772674",
    "proofScalar":"8300669A7238E49B872EACDD3CD80593D5DF9204A17D1495A9AFF2C417F09700",
    "previousBlockHash":"E2B8E0873C51853AC2CA73DF5D7A9905AF6BC5F50B8F83DEFFD07AFFE3C25013",
    "transactionsHash":"C8BF28A9E874650F93DC4BD8240506A559745404BEB221BF0E57FC1C4DF329DE",
    "receiptsHash":"651520E50FBE75F8977E1A31BFF9F80E1482E1507DD9490D1D46AAE7973F54F5",
    "stateHash":"FB23B6D6E5EDCCA6E44FD8A522B69FCDED4C85A950B0D80F2EEF636C464F22EB",
    "beneficiaryAddress":"68645FF6B18FEE134E9997D5C81DDA2F31855D710EF08566",
    "feeMultiplier":25
  },
  "id":"663107A4B8D2F084D206AA8F"
}
```
</details>

- - -
Symbolブロックチェーンでは**約30秒ごとに1ブロック生成される**ため、検索開始日の最初のブロックと検索終了日の最後のブロックを以下のように取得します。

ブロック情報を取得するAPIはこちら↓

https://symbol.github.io/symbol-openapi/v1.0.4/#tag/Block-routes/operation/searchBlocks


```TypeScript
import dayjs from 'dayjs';

// ブロック情報を取得する関数
async function fetchBlockHeight(
  baseUrl: string,
  startTimestamp: number,
  endTimestamp: number,
) {
  const url = `${baseUrl}/blocks?fromTimestamp=${startTimestamp}&toTimestamp=${endTimestamp}`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error('Network response error.');
  }

  const data = await response.json();
  return data.data[0]?.block?.height || 0;
}

// -----------------------
async function main() {
  const baseUrl = 'https://sym-main-03.opening-line.jp:3001';
  const fromDate = '2024/05/01';
  const toDate = '2024/05/31';
  const nemesisBlockTimestamp = 1615853185000; // epochAdjustment * 1000（ミリ秒に変換）

  // 検索開始日の最初のブロックを取得するためのtimestamp
  const fromStartTimestamp = dayjs(fromTimestamp, 'YYYY/MM/DD').unix() * 1000 - nemesisBlockTimestamp;
  const fromEndTimestamp = dayjs(fromTimestamp, 'YYYY/MM/DD').add(1, 'minute').unix() * 1000 - nemesisBlockTimestamp;
  // 検索終了日の最後のブロックを取得するためのtimestamp
  const toStartTimestamp = dayjs(toTimestamp, 'YYYY/MM/DD').add(1, 'day').unix() * 1000 - nemesisBlockTimestamp;
  const toEndTimestamp = dayjs(toTimestamp, 'YYYY/MM/DD').add(1, 'day').add(1, 'minute').unix() * 1000 - nemesisBlockTimestamp;

  try {
    // 各ブロック高を取得する
    const [rawFromBlockHeight, rawToBlockHeight] = await Promise.all([
      fetchBlockHeight(baseUrl, fromStartTimestamp, fromEndTimestamp),
      fetchBlockHeight(baseUrl, toStartTimestamp, toEndTimestamp),
    ]);

    if (rawFromBlockHeight === 0 || rawToBlockHeight === 0) {
      throw new Error('Block not found');
    }

    const fromBlockHeight = rawFromBlockHeight; // 検索開始ブロックは調整不要
    const toBlockHeight = rawToBlockHeight - 1; // 欲しい検索終了ブロック高は1つ前のブロックなので調整
  } catch (e) {
    console.error('Fetch error:', e);
  }
};

main();
```

やや冗長な気もしますが整理すると以下のようになります。
今回の場合は2024/05/31の最終ブロック高 3371218 を利用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2239577/2057fcb1-42d2-3b5f-e37d-c9581037477b.png)


### ブロック高で指定期間内の履歴を抽出する
あとは得られたブロック高を利用して指定期間内のトランザクション履歴を取得するだけです。

https://symbol.github.io/symbol-openapi/v1.0.4/#tag/Transaction-routes/operation/searchConfirmedTransactions

```typescript
// トランザクション履歴を取得する関数
async function fetchTransactions(
  baseUrl: string,
  recipientAddress: string,
  fromBlockHeight: number,
  toBlockHeight: number,
) {
  let allTransactions = [];

  async function fetchPage(pageNumber: number) {
    const url = `${baseUrl}/transactions/confirmed?recipientAddress=${recipientAddress}&fromHeight=${fromBlockHeight}&toHeight=${toBlockHeight}&embedded=true&order=asc&pageNumber=${pageNumber}&pageSize=100`;
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      throw new Error('Network response error.');
    }

    const data = await response.json();
    allTransactions = allTransactions.concat(data.data);

    if (data.data.length === 100) {
      await fetchPage(pageNumber + 1);
    }
  }
  
  await fetchPage(1);
  return allTransactions;
}

// -----------------------
async function main() {
  const baseUrl = 'https://sym-main-03.opening-line.jp:3001';
  const recipientAddress = 'Your wallet address';
  const fromBlockHeight = 3282027;
  const toBlockHeight = 3371219;
  const transactions = await fetchTransactions(baseUrl, recipientAddress, fromBlockHeight, toBlockHeight);
}

main();
```

# ソースコード総括
```typescript
import dayjs from 'dayjs';

// ブロック情報を取得する関数
async function fetchBlockHeight(
  baseUrl: string,
  startTimestamp: number,
  endTimestamp: number,
) {
  const url = `${baseUrl}/blocks?fromTimestamp=${startTimestamp}&toTimestamp=${endTimestamp}`;
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
    },
  });

  if (!response.ok) {
    throw new Error('Network response error.');
  }

  const data = await response.json();
  return data.data[0]?.block?.height || 0;
}

// トランザクション履歴を取得する関数
async function fetchTransactions(
  baseUrl: string,
  recipientAddress: string,
  fromBlockHeight: number,
  toBlockHeight: number,
) {
  let allTransactions = [];

  async function fetchPage(pageNumber: number) {
    const url = `${baseUrl}/transactions/confirmed?recipientAddress=${recipientAddress}&fromHeight=${fromBlockHeight}&toHeight=${toBlockHeight}&embedded=true&order=asc&pageNumber=${pageNumber}&pageSize=100`;
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    });

    if (!response.ok) {
      throw new Error('Network response error.');
    }

    const data = await response.json();
    allTransactions = allTransactions.concat(data.data);

    if (data.data.length === 100) {
      await fetchPage(pageNumber + 1);
    }
  }
  
  await fetchPage(1);
  return allTransactions;
}

// -----------------------
async function main() {
  const baseUrl = 'https://sym-main-03.opening-line.jp:3001';
  const recipientAddress = 'Your wallet address';
  const fromDate = '2024/05/01';
  const toDate = '2024/05/31';
  const nemesisBlockTimestamp = 1615853185000; // epochAdjustment * 1000（ミリ秒に変換）

  // 検索開始日の最初のブロックを取得するためのtimestamp
  const fromStartTimestamp = dayjs(fromTimestamp, 'YYYY/MM/DD').unix() * 1000 - nemesisBlockTimestamp;
  const fromEndTimestamp = dayjs(fromTimestamp, 'YYYY/MM/DD').add(1, 'minute').unix() * 1000 - nemesisBlockTimestamp;
  // 検索終了日の最後のブロックを取得するためのtimestamp
  const toStartTimestamp = dayjs(toTimestamp, 'YYYY/MM/DD').add(1, 'day').unix() * 1000 - nemesisBlockTimestamp;
  const toEndTimestamp = dayjs(toTimestamp, 'YYYY/MM/DD').add(1, 'day').add(1, 'minute').unix() * 1000 - nemesisBlockTimestamp;

  try {
    // 各ブロック高を取得する
    const [rawFromBlockHeight, rawToBlockHeight] = await Promise.all([
      fetchBlockHeight(baseUrl, fromStartTimestamp, fromEndTimestamp),
      fetchBlockHeight(baseUrl, toStartTimestamp, toEndTimestamp),
    ]);

    if (rawFromBlockHeight === 0 || rawToBlockHeight === 0) {
      throw new Error('Block not found');
    }

    const fromBlockHeight = rawFromBlockHeight; // 検索開始ブロックは調整不要
    const toBlockHeight = rawToBlockHeight - 1; // 欲しい検索終了ブロック高は1つ前のブロックなので調整

    // トランザクション履歴を取得する
    const transactions = await fetchTransactions(baseUrl, recipientAddress, fromBlockHeight, toBlockHeight);
  
  } catch (e) {
    console.error('Unexpected error:', e);
  }
};

main();
```

# 参考

https://qiita.com/nem_takanobu/items/4f50e5740318d92d7dcb

https://symbol.github.io/symbol-openapi/v1.0.4/