From bbc3172b0570bb38ad2ffb7880de2d7d09aa79cd Mon Sep 17 00:00:00 2001 From: INS Date: Wed, 25 Sep 2024 14:42:59 +0800 Subject: [PATCH 01/27] fix(frontend): btc transaction query address field (#197) --- .../[lang]/address/[address]/transactions/btc.tsx | 12 ------------ frontend/src/gql/gql.ts | 4 ++-- frontend/src/gql/graphql.ts | 5 ++--- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx b/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx index d408cea9..782a56a2 100644 --- a/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx +++ b/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx @@ -20,14 +20,6 @@ const query = graphql(` txid rgbppTransaction { ckbTransaction { - isCellbase - blockNumber - hash - fee - feeRate - size - confirmed - confirmations outputs { txHash index @@ -82,10 +74,6 @@ const query = graphql(` index } } - block { - timestamp - hash - } } } blockHeight diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 2c6efecd..80792cec 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -15,7 +15,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/ const documents = { "\n query BtcAddressBase($address: String!) {\n btcAddress(address: $address) {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n": types.BtcAddressBaseDocument, "\n query CkbAddressBase($address: String!) {\n ckbAddress(address: $address) {\n address\n shannon\n balance {\n total\n available\n occupied\n }\n transactionsCount\n }\n }\n": types.CkbAddressBaseDocument, - "\n query BtcTransactionByAddress($address: String!, $afterTxid: String) {\n btcAddress(address: $address) {\n transactions(afterTxid: $afterTxid) {\n txid\n rgbppTransaction {\n ckbTransaction {\n isCellbase\n blockNumber\n hash\n fee\n feeRate\n size\n confirmed\n confirmations\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n block {\n timestamp\n hash\n }\n }\n }\n blockHeight\n blockHash\n txid\n version\n size\n locktime\n weight\n fee\n feeRate\n confirmed\n confirmations\n transactionTime\n vin {\n txid\n vout\n scriptsig\n scriptsigAsm\n isCoinbase\n sequence\n prevout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n vout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n }\n }\n": types.BtcTransactionByAddressDocument, + "\n query BtcTransactionByAddress($address: String!, $afterTxid: String) {\n btcAddress(address: $address) {\n transactions(afterTxid: $afterTxid) {\n txid\n rgbppTransaction {\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n blockHeight\n blockHash\n txid\n version\n size\n locktime\n weight\n fee\n feeRate\n confirmed\n confirmations\n transactionTime\n vin {\n txid\n vout\n scriptsig\n scriptsigAsm\n isCoinbase\n sequence\n prevout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n vout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n }\n }\n": types.BtcTransactionByAddressDocument, "\n query CkbAddress($address: String!, $page: Int!, $pageSize: Int!) {\n ckbAddress(address: $address) {\n transactionsCount\n transactions(page: $page, pageSize: $pageSize) {\n isCellbase\n blockNumber\n hash\n fee\n size\n feeRate\n confirmations\n inputs {\n cellType\n status {\n consumed\n txHash\n index\n }\n txHash\n index\n capacity\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n outputs {\n txHash\n cellType\n index\n capacity\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n block {\n timestamp\n }\n }\n }\n }\n": types.CkbAddressDocument, "\n query RgbppCoin($typeHash: String!) {\n rgbppCoin(typeHash: $typeHash) {\n name\n symbol\n icon\n }\n }\n": types.RgbppCoinDocument, "\n query RgbppCoinTransactionsByTypeHash($typeHash: String!, $page: Int!, $pageSize: Int!) {\n rgbppCoin(typeHash: $typeHash) {\n transactionsCount\n transactions(page: $page, pageSize: $pageSize) {\n ckbTxHash\n btcTxid\n leapDirection\n blockNumber\n timestamp\n ckbTransaction {\n inputs {\n txHash\n index\n capacity\n status {\n consumed\n txHash\n index\n }\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n outputs {\n txHash\n index\n capacity\n status {\n consumed\n txHash\n index\n }\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n }\n }\n }\n }\n": types.RgbppCoinTransactionsByTypeHashDocument, @@ -63,7 +63,7 @@ export function graphql(source: "\n query CkbAddressBase($address: String!) {\n /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query BtcTransactionByAddress($address: String!, $afterTxid: String) {\n btcAddress(address: $address) {\n transactions(afterTxid: $afterTxid) {\n txid\n rgbppTransaction {\n ckbTransaction {\n isCellbase\n blockNumber\n hash\n fee\n feeRate\n size\n confirmed\n confirmations\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n block {\n timestamp\n hash\n }\n }\n }\n blockHeight\n blockHash\n txid\n version\n size\n locktime\n weight\n fee\n feeRate\n confirmed\n confirmations\n transactionTime\n vin {\n txid\n vout\n scriptsig\n scriptsigAsm\n isCoinbase\n sequence\n prevout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n vout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query BtcTransactionByAddress($address: String!, $afterTxid: String) {\n btcAddress(address: $address) {\n transactions(afterTxid: $afterTxid) {\n txid\n rgbppTransaction {\n ckbTransaction {\n isCellbase\n blockNumber\n hash\n fee\n feeRate\n size\n confirmed\n confirmations\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n block {\n timestamp\n hash\n }\n }\n }\n blockHeight\n blockHash\n txid\n version\n size\n locktime\n weight\n fee\n feeRate\n confirmed\n confirmations\n transactionTime\n vin {\n txid\n vout\n scriptsig\n scriptsigAsm\n isCoinbase\n sequence\n prevout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n vout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n }\n }\n"]; +export function graphql(source: "\n query BtcTransactionByAddress($address: String!, $afterTxid: String) {\n btcAddress(address: $address) {\n transactions(afterTxid: $afterTxid) {\n txid\n rgbppTransaction {\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n blockHeight\n blockHash\n txid\n version\n size\n locktime\n weight\n fee\n feeRate\n confirmed\n confirmations\n transactionTime\n vin {\n txid\n vout\n scriptsig\n scriptsigAsm\n isCoinbase\n sequence\n prevout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n vout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query BtcTransactionByAddress($address: String!, $afterTxid: String) {\n btcAddress(address: $address) {\n transactions(afterTxid: $afterTxid) {\n txid\n rgbppTransaction {\n ckbTransaction {\n outputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n status {\n consumed\n txHash\n index\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n }\n inputs {\n txHash\n index\n capacity\n cellType\n type {\n codeHash\n hashType\n args\n }\n lock {\n codeHash\n hashType\n args\n }\n xudtInfo {\n symbol\n amount\n decimal\n typeHash\n }\n status {\n consumed\n txHash\n index\n }\n }\n }\n }\n blockHeight\n blockHash\n txid\n version\n size\n locktime\n weight\n fee\n feeRate\n confirmed\n confirmations\n transactionTime\n vin {\n txid\n vout\n scriptsig\n scriptsigAsm\n isCoinbase\n sequence\n prevout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n vout {\n scriptpubkey\n scriptpubkeyAsm\n scriptpubkeyType\n scriptpubkeyAddress\n value\n status {\n spent\n txid\n vin\n }\n address {\n address\n satoshi\n pendingSatoshi\n transactionsCount\n }\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 4f00ac66..4190ae29 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -401,7 +401,6 @@ export type RgbppAddress = { address: Scalars['String']['output']; assets: Array; balances: Array; - cellsCount: Scalars['Float']['output']; utxosCount: Scalars['Float']['output']; }; @@ -571,7 +570,7 @@ export type BtcTransactionByAddressQueryVariables = Exact<{ }>; -export type BtcTransactionByAddressQuery = { __typename?: 'Query', btcAddress?: { __typename?: 'BitcoinAddress', transactions?: Array<{ __typename?: 'BitcoinTransaction', txid: string, blockHeight?: number | null, blockHash?: string | null, version: number, size: number, locktime: number, weight: number, fee: number, feeRate: number, confirmed: boolean, confirmations: number, transactionTime?: any | null, rgbppTransaction?: { __typename?: 'RgbppTransaction', ckbTransaction?: { __typename?: 'CkbTransaction', isCellbase: boolean, blockNumber: number, hash: string, fee?: number | null, feeRate?: number | null, size: number, confirmed: boolean, confirmations: number, outputs: Array<{ __typename?: 'CkbCell', txHash: string, index: number, capacity: number, cellType?: CellType | null, type?: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string } | null, lock: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string }, status?: { __typename?: 'CkbCellStatus', consumed: boolean, txHash?: string | null, index?: number | null } | null, xudtInfo?: { __typename?: 'CkbXUDTInfo', symbol: string, amount: string, decimal: number, typeHash: string } | null }>, inputs?: Array<{ __typename?: 'CkbCell', txHash: string, index: number, capacity: number, cellType?: CellType | null, type?: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string } | null, lock: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string }, xudtInfo?: { __typename?: 'CkbXUDTInfo', symbol: string, amount: string, decimal: number, typeHash: string } | null, status?: { __typename?: 'CkbCellStatus', consumed: boolean, txHash?: string | null, index?: number | null } | null }> | null, block?: { __typename?: 'CkbBlock', timestamp: any, hash: string } | null } | null } | null, vin?: Array<{ __typename?: 'BitcoinInput', txid: string, vout: number, scriptsig: string, scriptsigAsm: string, isCoinbase: boolean, sequence: number, prevout?: { __typename?: 'BitcoinOutput', scriptpubkey: string, scriptpubkeyAsm: string, scriptpubkeyType: string, scriptpubkeyAddress?: string | null, value: number, status?: { __typename?: 'BitcoinOutputStatus', spent: boolean, txid?: string | null, vin?: number | null } | null, address?: { __typename?: 'BitcoinAddress', address: string, satoshi: number, pendingSatoshi: number, transactionsCount?: number | null } | null } | null }> | null, vout: Array<{ __typename?: 'BitcoinOutput', scriptpubkey: string, scriptpubkeyAsm: string, scriptpubkeyType: string, scriptpubkeyAddress?: string | null, value: number, status?: { __typename?: 'BitcoinOutputStatus', spent: boolean, txid?: string | null, vin?: number | null } | null, address?: { __typename?: 'BitcoinAddress', address: string, satoshi: number, pendingSatoshi: number, transactionsCount?: number | null } | null }> }> | null } | null }; +export type BtcTransactionByAddressQuery = { __typename?: 'Query', btcAddress?: { __typename?: 'BitcoinAddress', transactions?: Array<{ __typename?: 'BitcoinTransaction', txid: string, blockHeight?: number | null, blockHash?: string | null, version: number, size: number, locktime: number, weight: number, fee: number, feeRate: number, confirmed: boolean, confirmations: number, transactionTime?: any | null, rgbppTransaction?: { __typename?: 'RgbppTransaction', ckbTransaction?: { __typename?: 'CkbTransaction', outputs: Array<{ __typename?: 'CkbCell', txHash: string, index: number, capacity: number, cellType?: CellType | null, type?: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string } | null, lock: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string }, status?: { __typename?: 'CkbCellStatus', consumed: boolean, txHash?: string | null, index?: number | null } | null, xudtInfo?: { __typename?: 'CkbXUDTInfo', symbol: string, amount: string, decimal: number, typeHash: string } | null }>, inputs?: Array<{ __typename?: 'CkbCell', txHash: string, index: number, capacity: number, cellType?: CellType | null, type?: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string } | null, lock: { __typename?: 'CkbScript', codeHash: string, hashType: string, args: string }, xudtInfo?: { __typename?: 'CkbXUDTInfo', symbol: string, amount: string, decimal: number, typeHash: string } | null, status?: { __typename?: 'CkbCellStatus', consumed: boolean, txHash?: string | null, index?: number | null } | null }> | null } | null } | null, vin?: Array<{ __typename?: 'BitcoinInput', txid: string, vout: number, scriptsig: string, scriptsigAsm: string, isCoinbase: boolean, sequence: number, prevout?: { __typename?: 'BitcoinOutput', scriptpubkey: string, scriptpubkeyAsm: string, scriptpubkeyType: string, scriptpubkeyAddress?: string | null, value: number, status?: { __typename?: 'BitcoinOutputStatus', spent: boolean, txid?: string | null, vin?: number | null } | null, address?: { __typename?: 'BitcoinAddress', address: string, satoshi: number, pendingSatoshi: number, transactionsCount?: number | null } | null } | null }> | null, vout: Array<{ __typename?: 'BitcoinOutput', scriptpubkey: string, scriptpubkeyAsm: string, scriptpubkeyType: string, scriptpubkeyAddress?: string | null, value: number, status?: { __typename?: 'BitcoinOutputStatus', spent: boolean, txid?: string | null, vin?: number | null } | null, address?: { __typename?: 'BitcoinAddress', address: string, satoshi: number, pendingSatoshi: number, transactionsCount?: number | null } | null }> }> | null } | null }; export type CkbAddressQueryVariables = Exact<{ address: Scalars['String']['input']; @@ -711,7 +710,7 @@ export type BtcAndCkbChainInfoQuery = { __typename?: 'Query', ckbChainInfo: { __ export const BtcAddressBaseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BtcAddressBase"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"address"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"btcAddress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"address"},"value":{"kind":"Variable","name":{"kind":"Name","value":"address"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"satoshi"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSatoshi"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}}]}}]} as unknown as DocumentNode; export const CkbAddressBaseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CkbAddressBase"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"address"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbAddress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"address"},"value":{"kind":"Variable","name":{"kind":"Name","value":"address"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"shannon"}},{"kind":"Field","name":{"kind":"Name","value":"balance"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"available"}},{"kind":"Field","name":{"kind":"Name","value":"occupied"}}]}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}}]}}]} as unknown as DocumentNode; -export const BtcTransactionByAddressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BtcTransactionByAddress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"address"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"afterTxid"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"btcAddress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"address"},"value":{"kind":"Variable","name":{"kind":"Name","value":"address"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"afterTxid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"afterTxid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"rgbppTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isCellbase"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"confirmed"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}},{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"inputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"block"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"blockHeight"}},{"kind":"Field","name":{"kind":"Name","value":"blockHash"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"locktime"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"confirmed"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}},{"kind":"Field","name":{"kind":"Name","value":"transactionTime"}},{"kind":"Field","name":{"kind":"Name","value":"vin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vout"}},{"kind":"Field","name":{"kind":"Name","value":"scriptsig"}},{"kind":"Field","name":{"kind":"Name","value":"scriptsigAsm"}},{"kind":"Field","name":{"kind":"Name","value":"isCoinbase"}},{"kind":"Field","name":{"kind":"Name","value":"sequence"}},{"kind":"Field","name":{"kind":"Name","value":"prevout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scriptpubkey"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAsm"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyType"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAddress"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"spent"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vin"}}]}},{"kind":"Field","name":{"kind":"Name","value":"address"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"satoshi"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSatoshi"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scriptpubkey"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAsm"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyType"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAddress"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"spent"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vin"}}]}},{"kind":"Field","name":{"kind":"Name","value":"address"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"satoshi"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSatoshi"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const BtcTransactionByAddressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BtcTransactionByAddress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"address"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"afterTxid"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"btcAddress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"address"},"value":{"kind":"Variable","name":{"kind":"Name","value":"address"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"afterTxid"},"value":{"kind":"Variable","name":{"kind":"Name","value":"afterTxid"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"rgbppTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"inputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"blockHeight"}},{"kind":"Field","name":{"kind":"Name","value":"blockHash"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"locktime"}},{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"confirmed"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}},{"kind":"Field","name":{"kind":"Name","value":"transactionTime"}},{"kind":"Field","name":{"kind":"Name","value":"vin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vout"}},{"kind":"Field","name":{"kind":"Name","value":"scriptsig"}},{"kind":"Field","name":{"kind":"Name","value":"scriptsigAsm"}},{"kind":"Field","name":{"kind":"Name","value":"isCoinbase"}},{"kind":"Field","name":{"kind":"Name","value":"sequence"}},{"kind":"Field","name":{"kind":"Name","value":"prevout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scriptpubkey"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAsm"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyType"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAddress"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"spent"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vin"}}]}},{"kind":"Field","name":{"kind":"Name","value":"address"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"satoshi"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSatoshi"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"vout"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"scriptpubkey"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAsm"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyType"}},{"kind":"Field","name":{"kind":"Name","value":"scriptpubkeyAddress"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"spent"}},{"kind":"Field","name":{"kind":"Name","value":"txid"}},{"kind":"Field","name":{"kind":"Name","value":"vin"}}]}},{"kind":"Field","name":{"kind":"Name","value":"address"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"address"}},{"kind":"Field","name":{"kind":"Name","value":"satoshi"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSatoshi"}},{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CkbAddressDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CkbAddress"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"address"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbAddress"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"address"},"value":{"kind":"Variable","name":{"kind":"Name","value":"address"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"isCellbase"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"hash"}},{"kind":"Field","name":{"kind":"Name","value":"fee"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"feeRate"}},{"kind":"Field","name":{"kind":"Name","value":"confirmations"}},{"kind":"Field","name":{"kind":"Name","value":"inputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"cellType"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"block"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const RgbppCoinDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RgbppCoin"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"typeHash"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rgbppCoin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"typeHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"typeHash"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}}]}}]}}]} as unknown as DocumentNode; export const RgbppCoinTransactionsByTypeHashDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"RgbppCoinTransactionsByTypeHash"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"typeHash"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"page"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rgbppCoin"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"typeHash"},"value":{"kind":"Variable","name":{"kind":"Name","value":"typeHash"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transactionsCount"}},{"kind":"Field","name":{"kind":"Name","value":"transactions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"page"},"value":{"kind":"Variable","name":{"kind":"Name","value":"page"}}},{"kind":"Argument","name":{"kind":"Name","value":"pageSize"},"value":{"kind":"Variable","name":{"kind":"Name","value":"pageSize"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ckbTxHash"}},{"kind":"Field","name":{"kind":"Name","value":"btcTxid"}},{"kind":"Field","name":{"kind":"Name","value":"leapDirection"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ckbTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"outputs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"capacity"}},{"kind":"Field","name":{"kind":"Name","value":"status"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"consumed"}},{"kind":"Field","name":{"kind":"Name","value":"txHash"}},{"kind":"Field","name":{"kind":"Name","value":"index"}}]}},{"kind":"Field","name":{"kind":"Name","value":"type"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lock"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"codeHash"}},{"kind":"Field","name":{"kind":"Name","value":"hashType"}},{"kind":"Field","name":{"kind":"Name","value":"args"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xudtInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"symbol"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"decimal"}},{"kind":"Field","name":{"kind":"Name","value":"typeHash"}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; From a7674b2e1ca0ee9519e35ca48c68db60874e78f2 Mon Sep 17 00:00:00 2001 From: ahonn Date: Wed, 25 Sep 2024 20:26:44 +1000 Subject: [PATCH 02/27] feat: use cluster and add redis for bullmq queue --- backend/redis-queue.conf | 80 ++++++++++++++++++++++++++++++++ backend/src/app.module.ts | 4 +- backend/src/bootstrap.service.ts | 9 ++++ backend/src/env.ts | 3 +- backend/src/main.ts | 5 +- docker-compose-preview.yaml | 24 ++++++++-- docker-compose.yaml | 21 ++++++++- 7 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 backend/redis-queue.conf diff --git a/backend/redis-queue.conf b/backend/redis-queue.conf new file mode 100644 index 00000000..06175924 --- /dev/null +++ b/backend/redis-queue.conf @@ -0,0 +1,80 @@ +# Redis configuration +# +# Example: https://raw.githubusercontent.com/redis/redis/7.4/redis.conf + +################################## NETWORK ##################################### +bind 0.0.0.0 + +################################ SNAPSHOTTING ################################ + +# Save the DB to disk. +# +# save [ ...] +# +# Redis will save the DB if the given number of seconds elapsed and it +# surpassed the given number of write operations against the DB. +# +# Snapshotting can be completely disabled with a single empty string argument +# as in following example: +# +# save "" +# +# Unless specified otherwise, by default Redis will save the DB: +# * After 3600 seconds (an hour) if at least 1 change was performed +# * After 300 seconds (5 minutes) if at least 100 changes were performed +# * After 60 seconds if at least 10000 changes were performed +# +# You can set these explicitly by uncommenting the following line. +# +save 3600 1 300 100 60 10000 + +############################## APPEND ONLY MODE ############################### + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good enough in many applications, but an issue with the Redis process or +# a power outage may result into a few minutes of writes lost (depending on +# the configured save points). +# +# The Append Only File is an alternative persistence mode that provides +# much better durability. For instance using the default data fsync policy +# (see later in the config file) Redis can lose just one second of writes in a +# dramatic event like a server power outage, or a single write if something +# wrong with the Redis process itself happens, but the operating system is +# still running correctly. +# +# AOF and RDB persistence can be enabled at the same time without problems. +# If the AOF is enabled on startup Redis will load the AOF, that is the file +# with the better durability guarantees. +# +# Please check https://redis.io/topics/persistence for more information. +appendonly yes + +# Redis can create append-only base files in either RDB or AOF formats. Using +# the RDB format is always faster and more efficient, and disabling it is only +# supported for backward compatibility purposes. +aof-use-rdb-preamble yes + +# Set a memory usage limit to the specified amount of bytes. +# When the memory limit is reached Redis will try to remove keys +# according to the eviction policy selected (see maxmemory-policy). +# +# If Redis can't remove keys according to the policy, or if the policy is +# set to 'noeviction', Redis will start to reply with errors to commands +# that would use more memory, like SET, LPUSH, and so on, and will continue +# to reply to read-only commands like GET. +# +# This option is usually useful when using Redis as an LRU or LFU cache, or to +# set a hard memory limit for an instance (using the 'noeviction' policy). +# +# WARNING: If you have replicas attached to an instance with maxmemory on, +# the size of the output buffers needed to feed the replicas are subtracted +# from the used memory count, so that network problems / resyncs will +# not trigger a loop where keys are evicted, and in turn the output +# buffer of replicas is full with DELs of keys evicted triggering the deletion +# of more keys, and so forth until the database is completely emptied. +# +# In short... if you have replicas attached it is suggested that you set a lower +# limit for maxmemory so that there is some free RAM on the system for replica +# output buffers (but this is not needed if the policy is 'noeviction'). +maxmemory 2gb +maxmemory-policy noeviction diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 0c11cbd8..ae754497 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -24,7 +24,7 @@ import { BootstrapService } from './bootstrap.service'; imports: [ConfigModule], useFactory: async (configService: ConfigService) => { const store = (await redisStore({ - url: configService.get('REDIS_URL'), + url: configService.get('REDIS_CACHE_URL'), isCacheable: (value) => value !== undefined, })) as unknown as CacheStore; return { @@ -36,7 +36,7 @@ import { BootstrapService } from './bootstrap.service'; BullModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => { - const url = new URL(configService.get('REDIS_URL')!); + const url = new URL(configService.get('REDIS_QUEUE_URL')!); return { connection: { host: url.hostname, diff --git a/backend/src/bootstrap.service.ts b/backend/src/bootstrap.service.ts index 7a01c186..d40f0212 100644 --- a/backend/src/bootstrap.service.ts +++ b/backend/src/bootstrap.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from './core/database/prisma/prisma.service'; import { IndexerServiceFactory } from './core/indexer/indexer.factory'; +import cluster from 'node:cluster'; @Injectable() export class BootstrapService { @@ -11,6 +12,14 @@ export class BootstrapService { private IndexerServiceFactory: IndexerServiceFactory, ) {} + public async bootstrap() { + if (cluster.isPrimary) { + cluster.fork(); + } else { + await this.bootstrapAssetsIndex(); + } + } + public async bootstrapAssetsIndex() { const chains = await this.prismaService.chain.findMany(); for (const chain of chains) { diff --git a/backend/src/env.ts b/backend/src/env.ts index 1ab52b6b..348dfdb4 100644 --- a/backend/src/env.ts +++ b/backend/src/env.ts @@ -24,7 +24,8 @@ export const envSchema = z }), DATABASE_URL: z.string(), - REDIS_URL: z.string(), + REDIS_CACHE_URL: z.string(), + REDIS_QUEUE_URL: z.string(), BITCOIN_PRIMARY_DATA_PROVIDER: z.enum(['mempool', 'electrs']).default('mempool'), diff --git a/backend/src/main.ts b/backend/src/main.ts index acb704c2..86dde450 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -5,6 +5,7 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify import { envSchema } from './env'; import { BootstrapService } from './bootstrap.service'; import { LogLevel } from '@nestjs/common'; +import cluster from 'node:cluster'; const env = envSchema.parse(process.env); const LOGGER_LEVELS: LogLevel[] = ['verbose', 'debug', 'log', 'warn', 'error']; @@ -39,6 +40,8 @@ async function bootstrap() { }); } - await app.listen(3000, '0.0.0.0'); + if (cluster.isPrimary) { + await app.listen(3000, '0.0.0.0'); + } } bootstrap(); diff --git a/docker-compose-preview.yaml b/docker-compose-preview.yaml index 6bf1b731..6c755de4 100644 --- a/docker-compose-preview.yaml +++ b/docker-compose-preview.yaml @@ -1,7 +1,9 @@ services: preview-explorer-backend: depends_on: - preview-redis: + preview-redis-cache: + condition: service_started + preview-redis-queue: condition: service_started preview-postgres: condition: service_healthy @@ -19,18 +21,31 @@ services: networks: - preview - preview-redis: + preview-redis-cache: # https://github.com/docker-library/redis/blob/b77450d/7.4/alpine/Dockerfile image: redis:7-alpine restart: unless-stopped volumes: # Redis' WORKDIR is /data - - preview-redis-data:/data + - preview-redis-cache-data:/data - ./backend/redis.conf:/usr/local/etc/redis/redis.conf:ro command: /usr/local/etc/redis/redis.conf networks: - preview + + preview-redis-queue: + # https://github.com/docker-library/redis/blob/b77450d/7.4/alpine/Dockerfile + image: redis:7-alpine + restart: unless-stopped + volumes: + # Redis' WORKDIR is /data + - preview-redis-queue-data:/data + - ./backend/redis-queue.conf:/usr/local/etc/redis/redis.conf:ro + command: /usr/local/etc/redis/redis.conf + networks: + - preview + preview-postgres: image: postgres:13 env_file: @@ -47,7 +62,8 @@ services: - preview volumes: - preview-redis-data: + preview-redis-cache-data: + preview-redis-queue-data: preview-pg-volume: networks: diff --git a/docker-compose.yaml b/docker-compose.yaml index cc7b5705..e0318143 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,9 @@ services: ports: - '3000:3000' depends_on: - redis: + redis-cache: + condition: service_started + redis-queue: condition: service_started postgres: condition: service_healthy @@ -21,7 +23,7 @@ services: networks: - internal - redis: + redis-cache: # https://github.com/docker-library/redis/blob/b77450d/7.4/alpine/Dockerfile image: redis:7-alpine restart: unless-stopped @@ -35,6 +37,20 @@ services: networks: - internal + redis-queue: + # https://github.com/docker-library/redis/blob/b77450d/7.4/alpine/Dockerfile + image: redis:7-alpine + restart: unless-stopped + ports: + - '127.0.0.1:6380:6379' + command: /usr/local/etc/redis/redis.conf + volumes: + # Redis' WORKDIR is /data + - redis-queue-data:/data + - ./backend/redis-queue.conf:/usr/local/etc/redis/redis.conf:ro + networks: + - internal + postgres: image: postgres:13 env_file: @@ -54,6 +70,7 @@ services: volumes: redis-data: + redis-queue-data: postgres_volume: networks: From 3fe6e2f418334e7379cd90c5f0fb3a7d78b3bc19 Mon Sep 17 00:00:00 2001 From: ahonn Date: Wed, 25 Sep 2024 21:04:28 +1000 Subject: [PATCH 03/27] fix: remove scheduler registry --- backend/src/bootstrap.service.ts | 9 ++++++++- backend/src/core/indexer/flow/assets.flow.ts | 4 +--- backend/src/core/indexer/flow/transactions.flow.ts | 4 +--- backend/src/core/indexer/indexer.service.ts | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/bootstrap.service.ts b/backend/src/bootstrap.service.ts index d40f0212..98b14cf6 100644 --- a/backend/src/bootstrap.service.ts +++ b/backend/src/bootstrap.service.ts @@ -10,11 +10,18 @@ export class BootstrapService { constructor( private prismaService: PrismaService, private IndexerServiceFactory: IndexerServiceFactory, - ) {} + ) { } public async bootstrap() { if (cluster.isPrimary) { cluster.fork(); + cluster.on('exit', (worker, code, signal) => { + this.logger.error( + `Worker ${worker.process.pid} died with code ${code} and signal ${signal}`, + ); + this.logger.log('Starting a new worker'); + cluster.fork(); + }); } else { await this.bootstrapAssetsIndex(); } diff --git a/backend/src/core/indexer/flow/assets.flow.ts b/backend/src/core/indexer/flow/assets.flow.ts index c020a51a..1327d561 100644 --- a/backend/src/core/indexer/flow/assets.flow.ts +++ b/backend/src/core/indexer/flow/assets.flow.ts @@ -88,9 +88,7 @@ export class IndexerAssetsFlow extends EventEmitter { private setupBlockAssetsIndexedListener() { this.on(IndexerAssetsEvent.BlockAssetsIndexed, () => { - setTimeout(() => { - this.startBlockAssetsIndexing(); - }, 1000 * 10); + setTimeout(this.startBlockAssetsIndexing.bind(this), 1000 * 10); }); } } diff --git a/backend/src/core/indexer/flow/transactions.flow.ts b/backend/src/core/indexer/flow/transactions.flow.ts index a34b6354..4ec9f41d 100644 --- a/backend/src/core/indexer/flow/transactions.flow.ts +++ b/backend/src/core/indexer/flow/transactions.flow.ts @@ -57,9 +57,7 @@ export class IndexerTransactionsFlow extends EventEmitter { private setupBlockAssetsIndexedListener() { this.on(IndexerTransactionsEvent.BlockIndexed, () => { - setTimeout(() => { - this.startBlockAssetsIndexing(); - }, 1000 * 10); + setTimeout(this.startBlockAssetsIndexing.bind(this), 1000 * 10); }); } } diff --git a/backend/src/core/indexer/indexer.service.ts b/backend/src/core/indexer/indexer.service.ts index af8da99d..96e7b0ba 100644 --- a/backend/src/core/indexer/indexer.service.ts +++ b/backend/src/core/indexer/indexer.service.ts @@ -4,6 +4,7 @@ import { BlockchainService } from '../blockchain/blockchain.service'; import { PrismaService } from '../database/prisma/prisma.service'; import { IndexerQueueService } from './indexer.queue'; import { IndexerTransactionsFlow } from './flow/transactions.flow'; +import { SchedulerRegistry } from '@nestjs/schedule'; export class IndexerService { public assetsFlow: IndexerAssetsFlow; From 01830d04d932b1b222c6b3a9a08f68318a42c4c7 Mon Sep 17 00:00:00 2001 From: ahonn Date: Wed, 25 Sep 2024 21:08:05 +1000 Subject: [PATCH 04/27] ci: update backend test --- .github/workflows/backend-test.yml | 14 ++++++++++++-- backend/src/core/indexer/indexer.service.ts | 1 - 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend-test.yml b/.github/workflows/backend-test.yml index f1890091..7d354d48 100644 --- a/.github/workflows/backend-test.yml +++ b/.github/workflows/backend-test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest services: - redis: + redis-cache: image: redis options: >- --health-cmd "redis-cli ping" @@ -23,6 +23,15 @@ jobs: --health-retries 5 ports: - 6379:6379 + redis-queue: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6380:6379 steps: - name: Checkout code @@ -57,7 +66,8 @@ jobs: echo BITCOIN_ELECTRS_API_URL="{{ secrets.BITCOIN_ELECTRS_API_URL }}" >> .env echo CKB_EXPLORER_API_URL="${{ secrets.CKB_EXPLORER_API_URL }}" >> .env echo CKB_RPC_WEBSOCKET_URL="${{ secrets.CKB_RPC_WEBSOCKET_URL }}" >> .env - echo REDIS_URL="redis://localhost:6379" >> .env + echo REDIS_CACHE_URL="redis://localhost:6379" >> .env + echo REDIS_QUEUE_URL="redis://localhost:6380" >> .env echo DATABASE_URL="postgres://postgres:postgres@postgres:5432/explorer?sslmode=disable" >> .env cat .env pnpm run test diff --git a/backend/src/core/indexer/indexer.service.ts b/backend/src/core/indexer/indexer.service.ts index 96e7b0ba..af8da99d 100644 --- a/backend/src/core/indexer/indexer.service.ts +++ b/backend/src/core/indexer/indexer.service.ts @@ -4,7 +4,6 @@ import { BlockchainService } from '../blockchain/blockchain.service'; import { PrismaService } from '../database/prisma/prisma.service'; import { IndexerQueueService } from './indexer.queue'; import { IndexerTransactionsFlow } from './flow/transactions.flow'; -import { SchedulerRegistry } from '@nestjs/schedule'; export class IndexerService { public assetsFlow: IndexerAssetsFlow; From 989c017a9ad6097320a219b391a2acc6009faf26 Mon Sep 17 00:00:00 2001 From: ahonn Date: Wed, 25 Sep 2024 21:36:24 +1000 Subject: [PATCH 05/27] ci: update docker-compose.yaml --- docker-compose.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index e0318143..202626dd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,12 +27,10 @@ services: # https://github.com/docker-library/redis/blob/b77450d/7.4/alpine/Dockerfile image: redis:7-alpine restart: unless-stopped - ports: - - '127.0.0.1:6379:6379' command: /usr/local/etc/redis/redis.conf volumes: # Redis' WORKDIR is /data - - redis-data:/data + - redis-cache-data:/data - ./backend/redis.conf:/usr/local/etc/redis/redis.conf:ro networks: - internal @@ -41,8 +39,6 @@ services: # https://github.com/docker-library/redis/blob/b77450d/7.4/alpine/Dockerfile image: redis:7-alpine restart: unless-stopped - ports: - - '127.0.0.1:6380:6379' command: /usr/local/etc/redis/redis.conf volumes: # Redis' WORKDIR is /data @@ -69,7 +65,7 @@ services: - internal volumes: - redis-data: + redis-cache-data: redis-queue-data: postgres_volume: From 4b23843fb459bfb4da4a9cdaf593d7740eef4bc0 Mon Sep 17 00:00:00 2001 From: ahonn Date: Thu, 26 Sep 2024 14:17:33 +1000 Subject: [PATCH 06/27] fix: improve caching and add dataloader maxBatchSize --- backend/src/decorators/cacheable.decorator.ts | 9 ++++++++- .../src/modules/bitcoin/address/address.dataloader.ts | 11 ++++++++++- .../bitcoin/transaction/transaction.dataloader.ts | 6 ++++++ backend/src/modules/complexity.plugin.ts | 1 + 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/backend/src/decorators/cacheable.decorator.ts b/backend/src/decorators/cacheable.decorator.ts index 690da32d..430add61 100644 --- a/backend/src/decorators/cacheable.decorator.ts +++ b/backend/src/decorators/cacheable.decorator.ts @@ -50,8 +50,14 @@ export function Cacheable(options: CustomCacheableRegisterOptions): MethodDecora const branch = configService.get('GIT_BRANCH') || 'unknown'; const prefix = configService.get('CACHE_KEY_PREFIX'); + const cacheKey = `${prefix}-${branch}/${key}`; + if (await cacheManager.get(cacheKey)) { + logger.debug(`Cache hit for key: ${key}`); + return cacheManager.get(cacheKey); + } + const returnVal = await cacheableHandle( - `${prefix}-${branch}/${key}`, + cacheKey, () => originalMethod.apply(this, args), options.ttl, ); @@ -67,3 +73,4 @@ export function Cacheable(options: CustomCacheableRegisterOptions): MethodDecora }; }; } + diff --git a/backend/src/modules/bitcoin/address/address.dataloader.ts b/backend/src/modules/bitcoin/address/address.dataloader.ts index aee9bec6..69ceb493 100644 --- a/backend/src/modules/bitcoin/address/address.dataloader.ts +++ b/backend/src/modules/bitcoin/address/address.dataloader.ts @@ -13,6 +13,12 @@ export class BitcoinAddressLoader implements NestDataLoader { this.logger.debug(`Loading bitcoin addresses stats: ${addresses.join(', ')}`); @@ -80,6 +86,9 @@ export class BitcoinAddressTransactionsLoader }; } } -export type BitcoinAddressTransactionsLoaderType = DataLoader; +export type BitcoinAddressTransactionsLoaderType = DataLoader< + GetAddressTxsParams, + BitcoinTransaction[] | null +>; export type BitcoinAddressTransactionsLoaderResponse = DataLoaderResponse; diff --git a/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts b/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts index b9af4cfe..2a7c2a3a 100644 --- a/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts +++ b/backend/src/modules/bitcoin/transaction/transaction.dataloader.ts @@ -14,6 +14,12 @@ export class BitcoinTransactionLoader constructor(private bitcoinApiService: BitcoinApiService) {} + public getOptions() { + return { + maxBatchSize: 20, + }; + } + public getBatchFunction() { return async (ids: string[]) => { this.logger.debug(`Loading bitcoin transactions: ${ids.join(', ')}`); diff --git a/backend/src/modules/complexity.plugin.ts b/backend/src/modules/complexity.plugin.ts index 652961fb..46294529 100644 --- a/backend/src/modules/complexity.plugin.ts +++ b/backend/src/modules/complexity.plugin.ts @@ -33,6 +33,7 @@ export class ComplexityPlugin implements ApolloServerPlugin { return; } + Sentry.setMeasurement('graphql.complexity', complexity, 'none'); if (complexity > maxComplexity) { Sentry.setContext('graphql', { query: request.query, From 81f13b0da442e2f10e27c5ecd513d62399cf2e55 Mon Sep 17 00:00:00 2001 From: ahonn Date: Thu, 26 Sep 2024 14:46:51 +1000 Subject: [PATCH 07/27] feat: improve complexity handling and Sentry logging --- .../middlewares/field-performance.middleware.ts | 7 +------ .../modules/bitcoin/address/address.resolver.ts | 5 ++++- .../src/modules/bitcoin/block/block.resolver.ts | 7 +++++-- .../src/modules/ckb/address/address.resolver.ts | 5 ++++- backend/src/modules/ckb/block/block.resolver.ts | 7 +++++-- .../ckb/transaction/transaction.resolver.ts | 11 ++++++++--- backend/src/modules/complexity.plugin.ts | 6 +++++- backend/src/modules/rgbpp/coin/coin.resolver.ts | 10 ++++++++-- .../rgbpp/transaction/transaction.resolver.ts | 15 ++++++++++++--- 9 files changed, 52 insertions(+), 21 deletions(-) diff --git a/backend/src/middlewares/field-performance.middleware.ts b/backend/src/middlewares/field-performance.middleware.ts index ddb3850e..ff319db7 100644 --- a/backend/src/middlewares/field-performance.middleware.ts +++ b/backend/src/middlewares/field-performance.middleware.ts @@ -8,12 +8,7 @@ export const fieldPerformanceMiddleware: FieldMiddleware = async ( const now = performance.now(); const value = await next(); const executionTime = performance.now() - now; - - Sentry.setContext('graphql', { - executionTime, - field: ctx.info.fieldName, - parent: ctx.info.parentType.name, - }); + Sentry.setTag('graphql.field', `${ctx.info.parentType.name}.${ctx.info.fieldName}`); Sentry.setMeasurement('graphql.executionTime', executionTime, 'millisecond'); return value; }; diff --git a/backend/src/modules/bitcoin/address/address.resolver.ts b/backend/src/modules/bitcoin/address/address.resolver.ts index ce7954dc..a17eb8d0 100644 --- a/backend/src/modules/bitcoin/address/address.resolver.ts +++ b/backend/src/modules/bitcoin/address/address.resolver.ts @@ -57,7 +57,10 @@ export class BitcoinAddressResolver { return stats.chain_stats.tx_count; } - @ResolveField(() => [BitcoinTransaction], { nullable: true }) + @ResolveField(() => [BitcoinTransaction], { + nullable: true, + complexity: ({ childComplexity }) => 10 + childComplexity, + }) public async transactions( @Parent() address: BitcoinAddress, @Loader(BitcoinAddressTransactionsLoader) diff --git a/backend/src/modules/bitcoin/block/block.resolver.ts b/backend/src/modules/bitcoin/block/block.resolver.ts index 90d210be..072a7fc2 100644 --- a/backend/src/modules/bitcoin/block/block.resolver.ts +++ b/backend/src/modules/bitcoin/block/block.resolver.ts @@ -12,7 +12,7 @@ import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; @Resolver(() => BitcoinBlock) export class BitcoinBlockResolver { - constructor(private bitcoinApiService: BitcoinApiService) {} + constructor(private bitcoinApiService: BitcoinApiService) { } @Query(() => BitcoinBlock, { name: 'btcBlock', nullable: true }) public async getBlock( @@ -83,7 +83,10 @@ export class BitcoinBlockResolver { }; } - @ResolveField(() => [BitcoinTransaction], { nullable: true }) + @ResolveField(() => [BitcoinTransaction], { + nullable: true, + complexity: ({ childComplexity }) => 10 + childComplexity, + }) public async transactions( @Parent() block: BitcoinBlock, @Loader(BitcoinBlockTransactionsLoader) blockTxsLoader: BitcoinBlockTransactionsLoaderType, diff --git a/backend/src/modules/ckb/address/address.resolver.ts b/backend/src/modules/ckb/address/address.resolver.ts index e2dd9807..49a4e264 100644 --- a/backend/src/modules/ckb/address/address.resolver.ts +++ b/backend/src/modules/ckb/address/address.resolver.ts @@ -50,7 +50,10 @@ export class CkbAddressResolver { return Number(addressInfo[0].transactions_count); } - @ResolveField(() => [CkbTransaction], { nullable: true }) + @ResolveField(() => [CkbTransaction], { + nullable: true, + complexity: ({ args, childComplexity }) => (args.pageSize ?? 10) * childComplexity, + }) public async transactions( @Parent() address: CkbAddress, @Loader(CkbAddressTransactionsLoader) addressTxsLoader: CkbAddressTransactionsLoaderType, diff --git a/backend/src/modules/ckb/block/block.resolver.ts b/backend/src/modules/ckb/block/block.resolver.ts index 6e2b870d..0fd8ebbd 100644 --- a/backend/src/modules/ckb/block/block.resolver.ts +++ b/backend/src/modules/ckb/block/block.resolver.ts @@ -21,7 +21,7 @@ import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.servi @Resolver(() => CkbBlock) export class CkbBlockResolver { - constructor(private ckbRpcService: CkbRpcWebsocketService) {} + constructor(private ckbRpcService: CkbRpcWebsocketService) { } @Query(() => CkbBlock, { name: 'ckbBlock', nullable: true }) public async getBlock( @@ -71,7 +71,10 @@ export class CkbBlockResolver { return toNumber(explorerBlock.miner_reward); } - @ResolveField(() => [CkbTransaction], { nullable: true }) + @ResolveField(() => [CkbTransaction], { + nullable: true, + complexity: ({ childComplexity }) => 10 + childComplexity, + }) public async transactions( @Parent() { hash }: CkbBlock, @Loader(CkbRpcBlockLoader) rpcBlockLoader: CkbRpcBlockLoaderType, diff --git a/backend/src/modules/ckb/transaction/transaction.resolver.ts b/backend/src/modules/ckb/transaction/transaction.resolver.ts index 858552ce..b6d30bdd 100644 --- a/backend/src/modules/ckb/transaction/transaction.resolver.ts +++ b/backend/src/modules/ckb/transaction/transaction.resolver.ts @@ -27,9 +27,12 @@ export class CkbTransactionResolver { constructor( private ckbTransactionService: CkbTransactionService, private ckbScriptService: CkbScriptService, - ) {} + ) { } - @Query(() => [CkbTransaction], { name: 'ckbTransactions' }) + @Query(() => [CkbTransaction], { + name: 'ckbTransactions', + complexity: ({ args, childComplexity }) => (args.limit ?? 10) * childComplexity, + }) public async getTransactions( @Args('types', { type: () => [CellType], nullable: true }) types: CellType[] | null, @Args('scriptKey', { type: () => CkbSearchKeyInput, nullable: true }) @@ -104,7 +107,9 @@ export class CkbTransactionResolver { return CkbTransaction.from(tx); } - @ResolveField(() => [CkbCell], { nullable: true }) + @ResolveField(() => [CkbCell], { + nullable: true, + }) public async inputs( @Parent() tx: CkbTransaction, @Loader(CkbRpcTransactionLoader) rpcTxLoader: CkbRpcTransactionLoaderType, diff --git a/backend/src/modules/complexity.plugin.ts b/backend/src/modules/complexity.plugin.ts index 46294529..17c6dadc 100644 --- a/backend/src/modules/complexity.plugin.ts +++ b/backend/src/modules/complexity.plugin.ts @@ -33,6 +33,8 @@ export class ComplexityPlugin implements ApolloServerPlugin { return; } + console.log('Complexity:', complexity); + console.log(request); Sentry.setMeasurement('graphql.complexity', complexity, 'none'); if (complexity > maxComplexity) { Sentry.setContext('graphql', { @@ -40,9 +42,11 @@ export class ComplexityPlugin implements ApolloServerPlugin { variables: request.variables, complexity, }); - throw new GraphQLError( + const error = new GraphQLError( `Query is too complex: ${complexity}. Maximum allowed complexity: ${maxComplexity}`, ); + Sentry.captureException(error); + throw error; } }, }; diff --git a/backend/src/modules/rgbpp/coin/coin.resolver.ts b/backend/src/modules/rgbpp/coin/coin.resolver.ts index 807977ce..40be8dbd 100644 --- a/backend/src/modules/rgbpp/coin/coin.resolver.ts +++ b/backend/src/modules/rgbpp/coin/coin.resolver.ts @@ -19,7 +19,10 @@ export class RgbppCoinResolver { private rgbppCoinService: RgbppCoinService, ) { } - @Query(() => RgbppCoinList, { name: 'rgbppCoins' }) + @Query(() => RgbppCoinList, { + name: 'rgbppCoins', + complexity: ({ args, childComplexity }) => (args.pageSize ?? 10) * childComplexity, + }) public async coins( @Args('page', { type: () => Int, nullable: true }) page: number = 1, @Args('pageSize', { type: () => Int, nullable: true }) pageSize: number = 10, @@ -50,7 +53,10 @@ export class RgbppCoinResolver { return RgbppCoin.from(response.data.attributes); } - @ResolveField(() => [RgbppTransaction], { nullable: true }) + @ResolveField(() => [RgbppTransaction], { + nullable: true, + complexity: ({ args, childComplexity }) => (args.pageSize ?? 10) * childComplexity, + }) public async transactions( @Parent() coin: RgbppCoin, @Args('page', { type: () => Int, nullable: true }) page: number = 1, diff --git a/backend/src/modules/rgbpp/transaction/transaction.resolver.ts b/backend/src/modules/rgbpp/transaction/transaction.resolver.ts index 8f60cdf6..e725893b 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.resolver.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.resolver.ts @@ -24,7 +24,10 @@ export class RgbppTransactionResolver { private bitcoinApiService: BitcoinApiService, ) { } - @Query(() => RgbppLatestTransactionList, { name: 'rgbppLatestTransactions' }) + @Query(() => RgbppLatestTransactionList, { + name: 'rgbppLatestTransactions', + complexity: ({ args, childComplexity }) => (args.limit ?? 10) * childComplexity, + }) public async getRecentTransactions( @Args('limit', { type: () => Int, nullable: true }) limit: number = 10, ): Promise { @@ -37,7 +40,10 @@ export class RgbppTransactionResolver { }; } - @Query(() => RgbppLatestTransactionList, { name: 'rgbppLatestL1Transactions' }) + @Query(() => RgbppLatestTransactionList, { + name: 'rgbppLatestL1Transactions', + complexity: ({ args, childComplexity }) => (args.limit ?? 10) * childComplexity, + }) public async getLatestL1Transactions( @Args('limit', { type: () => Int, nullable: true }) limit: number = 10, ): Promise { @@ -49,7 +55,10 @@ export class RgbppTransactionResolver { }; } - @Query(() => RgbppLatestTransactionList, { name: 'rgbppLatestL2Transactions' }) + @Query(() => RgbppLatestTransactionList, { + name: 'rgbppLatestL2Transactions', + complexity: ({ args, childComplexity }) => (args.limit ?? 10) * childComplexity, + }) public async getLatestL2Transactions( @Args('limit', { type: () => Int, nullable: true }) limit: number = 10, ): Promise { From 0ea526856ca30e3c9418857d1dce6c9bef6cdab1 Mon Sep 17 00:00:00 2001 From: ahonn Date: Thu, 26 Sep 2024 15:31:08 +1000 Subject: [PATCH 08/27] feat: add resolver field complexity --- .../core/bitcoin-api/bitcoin-api.service.ts | 1 - .../bitcoin/address/address.resolver.ts | 9 +++--- .../src/modules/bitcoin/bitcoin.resolver.ts | 9 +++--- .../modules/bitcoin/block/block.resolver.ts | 28 ++++++++++++++----- .../modules/bitcoin/output/output.resolver.ts | 6 +++- .../transaction/transaction.resolver.ts | 19 +++++++++---- .../modules/ckb/address/address.resolver.ts | 7 +++-- .../src/modules/ckb/block/block.resolver.ts | 19 ++++++++----- backend/src/modules/ckb/cell/cell.resolver.ts | 7 +++-- .../ckb/transaction/transaction.resolver.ts | 16 +++++++---- backend/src/modules/complexity.plugin.ts | 7 +++-- .../src/modules/rgbpp/asset/asset.resolver.ts | 5 ++-- .../src/modules/rgbpp/coin/coin.resolver.ts | 3 +- .../rgbpp/transaction/transaction.resolver.ts | 18 ++++++++---- 14 files changed, 103 insertions(+), 51 deletions(-) diff --git a/backend/src/core/bitcoin-api/bitcoin-api.service.ts b/backend/src/core/bitcoin-api/bitcoin-api.service.ts index 2845302b..2de9098b 100644 --- a/backend/src/core/bitcoin-api/bitcoin-api.service.ts +++ b/backend/src/core/bitcoin-api/bitcoin-api.service.ts @@ -9,7 +9,6 @@ import { MempoolService } from './provider/mempool.service'; import { ChainInfo, Transaction } from './bitcoin-api.schema'; import { ONE_HOUR_MS, ONE_MONTH_MS, TEN_MINUTES_MS } from 'src/common/date'; import { Cacheable } from 'src/decorators/cacheable.decorator'; -import { PLimit } from 'src/decorators/plimit.decorator'; import * as Sentry from '@sentry/nestjs'; type MethodParameters = T[K] extends (...args: infer P) => any ? P : never; diff --git a/backend/src/modules/bitcoin/address/address.resolver.ts b/backend/src/modules/bitcoin/address/address.resolver.ts index a17eb8d0..fa28dfdc 100644 --- a/backend/src/modules/bitcoin/address/address.resolver.ts +++ b/backend/src/modules/bitcoin/address/address.resolver.ts @@ -10,6 +10,7 @@ import { BitcoinAddressTransactionsLoaderType, } from './address.dataloader'; import { ValidateBtcAddressPipe } from 'src/pipes/validate-address.pipe'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => BitcoinAddress) export class BitcoinAddressResolver { @@ -20,7 +21,7 @@ export class BitcoinAddressResolver { return BitcoinAddress.from(address); } - @ResolveField(() => Float) + @ResolveField(() => Float, { complexity: ComplexityType.RequestField }) public async satoshi( @Parent() address: BitcoinAddress, @Loader(BitcoinAddressLoader) addressLoader: BitcoinAddressLoaderType, @@ -32,7 +33,7 @@ export class BitcoinAddressResolver { return addressStats.chain_stats.funded_txo_sum - addressStats.chain_stats.spent_txo_sum; } - @ResolveField(() => Float) + @ResolveField(() => Float, { complexity: ComplexityType.RequestField }) public async pendingSatoshi( @Parent() address: BitcoinAddress, @Loader(BitcoinAddressLoader) addressLoader: BitcoinAddressLoaderType, @@ -44,7 +45,7 @@ export class BitcoinAddressResolver { return addressStats.mempool_stats.funded_txo_sum - addressStats.mempool_stats.spent_txo_sum; } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async transactionsCount( @Parent() address: BitcoinAddress, @Loader(BitcoinAddressLoader) addressLoader: BitcoinAddressLoaderType, @@ -59,7 +60,7 @@ export class BitcoinAddressResolver { @ResolveField(() => [BitcoinTransaction], { nullable: true, - complexity: ({ childComplexity }) => 10 + childComplexity, + complexity: ({ childComplexity }) => ComplexityType.ListField + childComplexity, }) public async transactions( @Parent() address: BitcoinAddress, diff --git a/backend/src/modules/bitcoin/bitcoin.resolver.ts b/backend/src/modules/bitcoin/bitcoin.resolver.ts index 7ab489fc..7a23d8b0 100644 --- a/backend/src/modules/bitcoin/bitcoin.resolver.ts +++ b/backend/src/modules/bitcoin/bitcoin.resolver.ts @@ -6,21 +6,22 @@ import { BitcoinBlockTxidsLoader, BitcoinBlockTxidsLoaderType, } from './block/dataloader/block-txids.dataloader'; +import { ComplexityType } from '../complexity.plugin'; // 60 * 24 = 1440 minutes const BLOCK_NUMBER_OF_24_HOURS = 144; @Resolver(() => BitcoinChainInfo) export class BitcoinResolver { - constructor(private bitcoinApiService: BitcoinApiService) {} + constructor(private bitcoinApiService: BitcoinApiService) { } - @Query(() => BitcoinChainInfo, { name: 'btcChainInfo' }) + @Query(() => BitcoinChainInfo, { name: 'btcChainInfo', complexity: ComplexityType.RequestField }) public async chainInfo(): Promise { const info = await this.bitcoinApiService.getBlockchainInfo(); return BitcoinChainInfo.from(info); } - @ResolveField(() => Float) + @ResolveField(() => Float, { complexity: ComplexityType.RequestField }) public async transactionsCountIn24Hours( @Parent() chainInfo: BitcoinBaseChainInfo, @Loader(BitcoinBlockTxidsLoader) blockTxidsLoader: BitcoinBlockTxidsLoaderType, @@ -39,7 +40,7 @@ export class BitcoinResolver { return count; } - @ResolveField(() => BitcoinFees) + @ResolveField(() => BitcoinFees, { complexity: ComplexityType.RequestField }) public async fees(): Promise { const fees = await this.bitcoinApiService.getFeesRecommended(); return BitcoinFees.from(fees); diff --git a/backend/src/modules/bitcoin/block/block.resolver.ts b/backend/src/modules/bitcoin/block/block.resolver.ts index 072a7fc2..71460497 100644 --- a/backend/src/modules/bitcoin/block/block.resolver.ts +++ b/backend/src/modules/bitcoin/block/block.resolver.ts @@ -9,12 +9,17 @@ import { BitcoinBlockTransactionsLoaderType, } from './dataloader/block-transactions.dataloader'; import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => BitcoinBlock) export class BitcoinBlockResolver { constructor(private bitcoinApiService: BitcoinApiService) { } - @Query(() => BitcoinBlock, { name: 'btcBlock', nullable: true }) + @Query(() => BitcoinBlock, { + name: 'btcBlock', + nullable: true, + complexity: ComplexityType.RequestField, + }) public async getBlock( @Args('hashOrHeight', { type: () => String }) hashOrHeight: string, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, @@ -26,7 +31,10 @@ export class BitcoinBlockResolver { return BitcoinBlock.from(block); } - @ResolveField(() => BitcoinAddress, { nullable: true }) + @ResolveField(() => BitcoinAddress, { + nullable: true, + complexity: ComplexityType.RequestField, + }) public async miner( @Parent() block: BitcoinBlock, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, @@ -41,7 +49,10 @@ export class BitcoinBlockResolver { }; } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { + nullable: true, + complexity: ComplexityType.RequestField, + }) public async reward( @Parent() block: BitcoinBlock, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, @@ -54,7 +65,7 @@ export class BitcoinBlockResolver { return detail.extras.reward; } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async totalFee( @Parent() block: BitcoinBlock, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, @@ -67,7 +78,10 @@ export class BitcoinBlockResolver { return detail.extras.totalFees; } - @ResolveField(() => FeeRateRange, { nullable: true }) + @ResolveField(() => FeeRateRange, { + nullable: true, + complexity: ComplexityType.RequestField, + }) public async feeRateRange( @Parent() block: BitcoinBlock, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, @@ -85,7 +99,7 @@ export class BitcoinBlockResolver { @ResolveField(() => [BitcoinTransaction], { nullable: true, - complexity: ({ childComplexity }) => 10 + childComplexity, + complexity: ({ childComplexity }) => ComplexityType.ListField + childComplexity, }) public async transactions( @Parent() block: BitcoinBlock, @@ -106,7 +120,7 @@ export class BitcoinBlockResolver { return txs.map((tx) => BitcoinTransaction.from(tx)); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async confirmations(@Parent() block: BitcoinBlock): Promise { const info = await this.bitcoinApiService.getBlockchainInfo(); return info.blocks - block.height; diff --git a/backend/src/modules/bitcoin/output/output.resolver.ts b/backend/src/modules/bitcoin/output/output.resolver.ts index 898780f7..6a27bb75 100644 --- a/backend/src/modules/bitcoin/output/output.resolver.ts +++ b/backend/src/modules/bitcoin/output/output.resolver.ts @@ -6,6 +6,7 @@ import { BitcoinTransactionOutSpendsLoader, BitcoinTransactionOutSpendsLoaderType, } from '../transaction/transaction.dataloader'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => BitcoinOutput) export class BitcoinOutputResolver { @@ -20,7 +21,10 @@ export class BitcoinOutputResolver { }; } - @ResolveField(() => BitcoinOutputStatus, { nullable: true }) + @ResolveField(() => BitcoinOutputStatus, { + nullable: true, + complexity: ComplexityType.RequestField, + }) public async status( @Parent() output: BitcoinOutput, @Loader(BitcoinTransactionOutSpendsLoader) diff --git a/backend/src/modules/bitcoin/transaction/transaction.resolver.ts b/backend/src/modules/bitcoin/transaction/transaction.resolver.ts index b9a99eac..7d2f79a8 100644 --- a/backend/src/modules/bitcoin/transaction/transaction.resolver.ts +++ b/backend/src/modules/bitcoin/transaction/transaction.resolver.ts @@ -10,12 +10,17 @@ import { } from 'src/modules/rgbpp/transaction/transaction.dataloader'; import { BitcoinBlock } from '../block/block.model'; import { BitcoinBlockLoader, BitcoinBlockLoaderType } from '../block/dataloader/block.dataloader'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => BitcoinTransaction) export class BitcoinTransactionResolver { - constructor(private bitcoinApiService: BitcoinApiService) {} + constructor(private bitcoinApiService: BitcoinApiService) { } - @Query(() => BitcoinTransaction, { name: 'btcTransaction', nullable: true }) + @Query(() => BitcoinTransaction, { + name: 'btcTransaction', + nullable: true, + complexity: ComplexityType.RequestField, + }) public async getTransaction( @Args('txid') txid: string, @Loader(BitcoinTransactionLoader) txLoader: BitcoinTransactionLoaderType, @@ -27,7 +32,9 @@ export class BitcoinTransactionResolver { return BitcoinTransaction.from(transaction); } - @ResolveField(() => Float) + @ResolveField(() => Float, { + complexity: ComplexityType.RequestField, + }) public async confirmations(@Parent() tx: BitcoinTransaction): Promise { if (!tx.confirmed) { return 0; @@ -36,7 +43,7 @@ export class BitcoinTransactionResolver { return info.blocks - tx.blockHeight! + 1; } - @ResolveField(() => Date, { nullable: true }) + @ResolveField(() => Date, { nullable: true, complexity: ComplexityType.RequestField }) public async transactionTime(@Parent() tx: BitcoinTransaction): Promise { const [txTime] = await this.bitcoinApiService.getTransactionTimes({ txids: [tx.txid] }); if (!txTime) { @@ -45,7 +52,7 @@ export class BitcoinTransactionResolver { return new Date(txTime * 1000); } - @ResolveField(() => BitcoinBlock, { nullable: true }) + @ResolveField(() => BitcoinBlock, { nullable: true, complexity: ComplexityType.RequestField }) public async block( @Parent() tx: BitcoinTransaction, @Loader(BitcoinBlockLoader) blockLoader: BitcoinBlockLoaderType, @@ -60,7 +67,7 @@ export class BitcoinTransactionResolver { return BitcoinBlock.from(block); } - @ResolveField(() => RgbppTransaction, { nullable: true }) + @ResolveField(() => RgbppTransaction, { nullable: true, complexity: ComplexityType.RequestField }) public async rgbppTransaction( @Parent() tx: BitcoinTransaction, @Loader(RgbppTransactionLoader) txLoader: RgbppTransactionLoaderType, diff --git a/backend/src/modules/ckb/address/address.resolver.ts b/backend/src/modules/ckb/address/address.resolver.ts index 49a4e264..40e02799 100644 --- a/backend/src/modules/ckb/address/address.resolver.ts +++ b/backend/src/modules/ckb/address/address.resolver.ts @@ -14,6 +14,7 @@ import { } from '../transaction/transaction.dataloader'; import { ValidateCkbAddressPipe } from 'src/pipes/validate-address.pipe'; import { BI } from '@ckb-lumos/bi'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => CkbAddress) export class CkbAddressResolver { @@ -26,7 +27,7 @@ export class CkbAddressResolver { }; } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async shannon( @Parent() address: CkbAddress, @Loader(CkbAddressLoader) addressLoader: CkbAddressLoaderType, @@ -38,7 +39,7 @@ export class CkbAddressResolver { return Number(addressInfo[0].balance); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async transactionsCount( @Parent() address: CkbAddress, @Loader(CkbAddressLoader) addressLoader: CkbAddressLoaderType, @@ -80,7 +81,7 @@ export class CkbAddressResolver { ); } - @ResolveField(() => CkbAddressBalance, { nullable: true }) + @ResolveField(() => CkbAddressBalance, { nullable: true, complexity: ComplexityType.RequestField }) public async balance( @Parent() address: CkbAddress, @Loader(CkbAddressLoader) addressLoader: CkbAddressLoaderType, diff --git a/backend/src/modules/ckb/block/block.resolver.ts b/backend/src/modules/ckb/block/block.resolver.ts index 0fd8ebbd..a2415d21 100644 --- a/backend/src/modules/ckb/block/block.resolver.ts +++ b/backend/src/modules/ckb/block/block.resolver.ts @@ -18,12 +18,17 @@ import { CkbRpcTransactionLoaderType, } from '../transaction/transaction.dataloader'; import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.service'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => CkbBlock) export class CkbBlockResolver { constructor(private ckbRpcService: CkbRpcWebsocketService) { } - @Query(() => CkbBlock, { name: 'ckbBlock', nullable: true }) + @Query(() => CkbBlock, { + name: 'ckbBlock', + nullable: true, + complexity: ComplexityType.RequestField, + }) public async getBlock( @Args('heightOrHash', { type: () => String }) heightOrHash: string, @Loader(CkbRpcBlockLoader) rpcBlockLoader: CkbRpcBlockLoaderType, @@ -35,7 +40,7 @@ export class CkbBlockResolver { return CkbBlock.from(block); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async totalFee( @Parent() block: CkbBlock, @Loader(CkbBlockEconomicStateLoader) blockEconomicLoader: CkbBlockEconomicStateLoaderType, @@ -47,7 +52,7 @@ export class CkbBlockResolver { return BI.from(blockEconomicState.txs_fee).toNumber(); } - @ResolveField(() => CkbAddress, { nullable: true }) + @ResolveField(() => CkbAddress, { nullable: true, complexity: ComplexityType.RequestField }) public async miner( @Parent() block: CkbBlock, @Loader(CkbExplorerBlockLoader) explorerBlockLoader: CkbExplorerBlockLoaderType, @@ -59,7 +64,7 @@ export class CkbBlockResolver { return CkbAddress.from(explorerBlock.miner_hash); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async reward( @Parent() block: CkbBlock, @Loader(CkbExplorerBlockLoader) explorerBlockLoader: CkbExplorerBlockLoaderType, @@ -73,7 +78,7 @@ export class CkbBlockResolver { @ResolveField(() => [CkbTransaction], { nullable: true, - complexity: ({ childComplexity }) => 10 + childComplexity, + complexity: ({ childComplexity }) => ComplexityType.ListField + childComplexity, }) public async transactions( @Parent() { hash }: CkbBlock, @@ -95,7 +100,7 @@ export class CkbBlockResolver { ); } - @ResolveField(() => Float) + @ResolveField(() => Float, { complexity: ComplexityType.RequestField }) public async size( @Parent() block: CkbBlock, @Loader(CkbExplorerBlockLoader) explorerBlockLoader: CkbExplorerBlockLoaderType, @@ -107,7 +112,7 @@ export class CkbBlockResolver { return explorerBlock.size; } - @ResolveField(() => Float) + @ResolveField(() => Float, { complexity: ComplexityType.RequestField }) public async confirmations( @Parent() block: CkbBlock, @Loader(CkbExplorerBlockLoader) explorerBlockLoader: CkbExplorerBlockLoaderType, diff --git a/backend/src/modules/ckb/cell/cell.resolver.ts b/backend/src/modules/ckb/cell/cell.resolver.ts index b1e33aea..b637d91f 100644 --- a/backend/src/modules/ckb/cell/cell.resolver.ts +++ b/backend/src/modules/ckb/cell/cell.resolver.ts @@ -11,15 +11,16 @@ import { CkbCell, CkbXUDTInfo, CkbCellStatus } from './cell.model'; import { CkbCellService } from './cell.service'; import { CellType } from '../script/script.model'; import { CkbScriptService } from '../script/script.service'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => CkbCell) export class CkbCellResolver { constructor( private ckbCellService: CkbCellService, private ckbScriptService: CkbScriptService, - ) {} + ) { } - @ResolveField(() => CkbXUDTInfo, { nullable: true }) + @ResolveField(() => CkbXUDTInfo, { nullable: true, complexity: ComplexityType.RequestField }) public async xudtInfo( @Parent() cell: CkbCell, @Loader(CkbExplorerTransactionLoader) explorerTxLoader: CkbExplorerTransactionLoaderType, @@ -32,7 +33,7 @@ export class CkbCellResolver { return this.ckbCellService.getXUDTInfoFromOutput(cell, output); } - @ResolveField(() => CkbCellStatus, { nullable: true }) + @ResolveField(() => CkbCellStatus, { nullable: true, complexity: ComplexityType.RequestField }) public async status( @Parent() cell: CkbCell, @Loader(CkbRpcTransactionLoader) rpcTxLoader: CkbRpcTransactionLoaderType, diff --git a/backend/src/modules/ckb/transaction/transaction.resolver.ts b/backend/src/modules/ckb/transaction/transaction.resolver.ts index b6d30bdd..1f874a4d 100644 --- a/backend/src/modules/ckb/transaction/transaction.resolver.ts +++ b/backend/src/modules/ckb/transaction/transaction.resolver.ts @@ -19,6 +19,7 @@ import { CkbScriptService } from '../script/script.service'; import { OrderType } from 'src/modules/api.model'; import { BaseScriptService } from '../script/base/base-script.service'; import * as Sentry from '@sentry/nestjs'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => CkbTransaction) export class CkbTransactionResolver { @@ -95,7 +96,11 @@ export class CkbTransactionResolver { throw new BadRequestException('One of types and scriptKey must be provided'); } - @Query(() => CkbTransaction, { name: 'ckbTransaction', nullable: true }) + @Query(() => CkbTransaction, { + name: 'ckbTransaction', + nullable: true, + complexity: ComplexityType.RequestField, + }) public async getTransaction( @Args('txHash') txHash: string, @Loader(CkbRpcTransactionLoader) rpcTxLoader: CkbRpcTransactionLoaderType, @@ -109,6 +114,7 @@ export class CkbTransactionResolver { @ResolveField(() => [CkbCell], { nullable: true, + complexity: ComplexityType.RequestField, }) public async inputs( @Parent() tx: CkbTransaction, @@ -134,7 +140,7 @@ export class CkbTransactionResolver { ); } - @ResolveField(() => CkbBlock, { nullable: true }) + @ResolveField(() => CkbBlock, { nullable: true, complexity: ComplexityType.RequestField }) public async block( @Parent() tx: CkbTransaction, @Loader(CkbRpcBlockLoader) rpcBlockLoader: CkbRpcBlockLoaderType, @@ -146,7 +152,7 @@ export class CkbTransactionResolver { return CkbBlock.from(block); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async fee( @Parent() tx: CkbTransaction, @Loader(CkbExplorerTransactionLoader) explorerTxLoader: CkbExplorerTransactionLoaderType, @@ -158,7 +164,7 @@ export class CkbTransactionResolver { return toNumber(explorerTx.transaction_fee); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async feeRate( @Parent() tx: CkbTransaction, @Loader(CkbExplorerTransactionLoader) explorerTxLoader: CkbExplorerTransactionLoaderType, @@ -173,7 +179,7 @@ export class CkbTransactionResolver { return fee.mul(ratio).div(size).toNumber(); } - @ResolveField(() => Float) + @ResolveField(() => Float, { complexity: ComplexityType.RequestField }) public async confirmations(@Parent() tx: CkbTransaction): Promise { if (!tx.confirmed) { return 0; diff --git a/backend/src/modules/complexity.plugin.ts b/backend/src/modules/complexity.plugin.ts index 17c6dadc..a4b4f44e 100644 --- a/backend/src/modules/complexity.plugin.ts +++ b/backend/src/modules/complexity.plugin.ts @@ -7,6 +7,11 @@ import * as Sentry from '@sentry/nestjs'; import { ConfigService } from '@nestjs/config'; import { Env } from 'src/env'; +export enum ComplexityType { + RequestField = 3, + ListField = 10, +} + @Plugin() export class ComplexityPlugin implements ApolloServerPlugin { constructor( @@ -33,8 +38,6 @@ export class ComplexityPlugin implements ApolloServerPlugin { return; } - console.log('Complexity:', complexity); - console.log(request); Sentry.setMeasurement('graphql.complexity', complexity, 'none'); if (complexity > maxComplexity) { Sentry.setContext('graphql', { diff --git a/backend/src/modules/rgbpp/asset/asset.resolver.ts b/backend/src/modules/rgbpp/asset/asset.resolver.ts index 36aff6f6..e615477e 100644 --- a/backend/src/modules/rgbpp/asset/asset.resolver.ts +++ b/backend/src/modules/rgbpp/asset/asset.resolver.ts @@ -7,12 +7,13 @@ import { } from 'src/modules/bitcoin/transaction/transaction.dataloader'; import { RgbppService } from '../rgbpp.service'; import { Loader } from 'src/common/dataloader'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => RgbppAsset) export class RgbppAssetResolver { - constructor(private rgbppService: RgbppService) {} + constructor(private rgbppService: RgbppService) { } - @ResolveField(() => BitcoinOutput, { nullable: true }) + @ResolveField(() => BitcoinOutput, { nullable: true, complexity: ComplexityType.RequestField }) public async utxo( @Parent() asset: RgbppAsset, @Loader(BitcoinTransactionLoader) txLoader: BitcoinTransactionLoaderType, diff --git a/backend/src/modules/rgbpp/coin/coin.resolver.ts b/backend/src/modules/rgbpp/coin/coin.resolver.ts index 40be8dbd..c5064eb7 100644 --- a/backend/src/modules/rgbpp/coin/coin.resolver.ts +++ b/backend/src/modules/rgbpp/coin/coin.resolver.ts @@ -11,6 +11,7 @@ import { import { Layer, RgbppHolder } from '../statistic/statistic.model'; import { OrderType } from 'src/modules/api.model'; import { RgbppCoinService } from './coin.service'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => RgbppCoin) export class RgbppCoinResolver { @@ -77,7 +78,7 @@ export class RgbppCoinResolver { return transactions.data.map((tx) => RgbppTransaction.fromCkbTransaction(tx.attributes)); } - @ResolveField(() => Float, { nullable: true }) + @ResolveField(() => Float, { nullable: true, complexity: ComplexityType.RequestField }) public async transactionsCount( @Parent() coin: RgbppCoin, @Loader(CkbExplorerXUDTTransactionsLoader) txsLoader: CkbExplorerXUDTTransactionsLoaderType, diff --git a/backend/src/modules/rgbpp/transaction/transaction.resolver.ts b/backend/src/modules/rgbpp/transaction/transaction.resolver.ts index e725893b..7b1a679c 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.resolver.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.resolver.ts @@ -16,6 +16,7 @@ import { RgbppTransactionLoader, RgbppTransactionLoaderType } from './transactio import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; import { BI } from '@ckb-lumos/bi'; import { LeapDirection } from '@prisma/client'; +import { ComplexityType } from 'src/modules/complexity.plugin'; @Resolver(() => RgbppTransaction) export class RgbppTransactionResolver { @@ -70,7 +71,11 @@ export class RgbppTransactionResolver { }; } - @Query(() => RgbppTransaction, { name: 'rgbppTransaction', nullable: true }) + @Query(() => RgbppTransaction, { + name: 'rgbppTransaction', + nullable: true, + complexity: ComplexityType.RequestField, + }) public async getTransaction( @Args('txidOrTxHash') txidOrTxHash: string, @Loader(RgbppTransactionLoader) txLoader: RgbppTransactionLoaderType, @@ -79,7 +84,7 @@ export class RgbppTransactionResolver { return tx || null; } - @ResolveField(() => Date) + @ResolveField(() => Date, { complexity: ComplexityType.RequestField }) public async timestamp( @Parent() tx: RgbppTransaction, @Loader(BitcoinTransactionLoader) btcTxLoader: BitcoinTransactionLoaderType, @@ -110,7 +115,7 @@ export class RgbppTransactionResolver { return tx.blockTime; } - @ResolveField(() => LeapDirection, { nullable: true }) + @ResolveField(() => LeapDirection, { nullable: true, complexity: ComplexityType.RequestField }) public async leapDirection( @Parent() tx: RgbppTransaction, @Loader(CkbRpcTransactionLoader) ckbRpcTxLoader: CkbRpcTransactionLoaderType, @@ -122,7 +127,7 @@ export class RgbppTransactionResolver { return this.rgbppTransactionService.getLeapDirectionByCkbTx(ckbTx.transaction); } - @ResolveField(() => CkbTransaction, { nullable: true }) + @ResolveField(() => CkbTransaction, { nullable: true, complexity: ComplexityType.RequestField }) public async ckbTransaction( @Parent() tx: RgbppTransaction, @Loader(CkbRpcTransactionLoader) ckbRpcTxLoader: CkbRpcTransactionLoaderType, @@ -134,7 +139,10 @@ export class RgbppTransactionResolver { return CkbTransaction.from(ckbTx); } - @ResolveField(() => BitcoinTransaction, { nullable: true }) + @ResolveField(() => BitcoinTransaction, { + nullable: true, + complexity: ComplexityType.RequestField, + }) public async btcTransaction( @Parent() tx: RgbppTransaction, @Loader(BitcoinTransactionLoader) txLoader: BitcoinTransactionLoaderType, From 1972bad8056f5591d81cd6d8e2748c32ccc8119d Mon Sep 17 00:00:00 2001 From: ahonn Date: Thu, 26 Sep 2024 15:58:16 +1000 Subject: [PATCH 09/27] feat: improve caching for get_transaction/get_transactions --- backend/src/core/core.service.ts | 9 +++++ .../ckb/script/base/base-script.service.ts | 9 ++++- .../ckb/transaction/transaction.service.ts | 7 ++++ .../modules/rgbpp/address/address.service.ts | 8 ++++- .../rgbpp/transaction/transaction.service.ts | 36 +++++++++++++++++-- 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/backend/src/core/core.service.ts b/backend/src/core/core.service.ts index 5fb1fea4..3f7d05c6 100644 --- a/backend/src/core/core.service.ts +++ b/backend/src/core/core.service.ts @@ -14,6 +14,8 @@ import { Env } from 'src/env'; import { Transaction } from './blockchain/blockchain.interface'; import { BlockchainServiceFactory } from './blockchain/blockchain.factory'; import { LeapDirection } from '@prisma/client'; +import { Cacheable } from 'nestjs-cacheable'; +import { ONE_MONTH_MS } from 'src/common/date'; export const CELLBASE_TX_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000'; @@ -72,6 +74,13 @@ export class CoreService { ); } + @Cacheable({ + namespace: 'CoreService', + key: (chainId: number, ckbTx: Transaction) => { + return `getLeapDirectionByCkbTx:${chainId}:${ckbTx.hash}`; + }, + ttl: ONE_MONTH_MS, + }) public async getLeapDirectionByCkbTx(chainId: number, ckbTx: Transaction) { const blockchainService = this.blockchainServiceFactory.getService(chainId); const inputCells = await Promise.all( diff --git a/backend/src/modules/ckb/script/base/base-script.service.ts b/backend/src/modules/ckb/script/base/base-script.service.ts index fd842cab..17bd7085 100644 --- a/backend/src/modules/ckb/script/base/base-script.service.ts +++ b/backend/src/modules/ckb/script/base/base-script.service.ts @@ -9,6 +9,7 @@ import { Env } from 'src/env'; import { CellType } from '../script.model'; import { OrderType } from 'src/modules/api.model'; import * as Sentry from '@sentry/nestjs'; +import { Cacheable } from 'src/decorators/cacheable.decorator'; export abstract class BaseScriptService { protected logger = new Logger(BaseScriptService.name); @@ -17,7 +18,7 @@ export abstract class BaseScriptService { constructor( protected configService: ConfigService, protected ckbRpcService: CkbRpcWebsocketService, - ) {} + ) { } public static sortTransactionCmp(a: CkbRpc.IndexerCell, b: CkbRpc.IndexerCell, order: OrderType) { const blockNumberCmp = BI.from(b.block_number).sub(BI.from(a.block_number)).toNumber(); @@ -35,6 +36,12 @@ export abstract class BaseScriptService { return scripts.some((s) => isScriptEqual(s, { ...script, args: '0x' })); } + @Cacheable({ + namespace: 'BaseScriptService', + key: (limit: number, order: OrderType, after?: string) => + `getTransactions:${limit}:${order}:${after}`, + ttl: 10_000, + }) public async getTransactions( limit: number = 10, order: OrderType = OrderType.Desc, diff --git a/backend/src/modules/ckb/transaction/transaction.service.ts b/backend/src/modules/ckb/transaction/transaction.service.ts index e01be132..04873ad2 100644 --- a/backend/src/modules/ckb/transaction/transaction.service.ts +++ b/backend/src/modules/ckb/transaction/transaction.service.ts @@ -6,6 +6,7 @@ import { CkbExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service'; import { CkbSearchKeyInput } from './transaction.model'; import { BI } from '@ckb-lumos/bi'; import { OrderType } from 'src/modules/api.model'; +import { Cacheable } from 'src/decorators/cacheable.decorator'; @Injectable() export class CkbTransactionService { @@ -29,6 +30,12 @@ export class CkbTransactionService { return this.ckbRpcService.getTipBlockNumber(); } + @Cacheable({ + namespace: 'CkbTransactionService', + key: (searchKey: CkbSearchKeyInput, order: OrderType, limit: number, after?: string) => + `getTransactions:${JSON.stringify(searchKey)}:${order}:${limit}:${after}`, + ttl: 10_000, + }) public async getTransactions( searchKey: CkbSearchKeyInput, order: OrderType = OrderType.Desc, diff --git a/backend/src/modules/rgbpp/address/address.service.ts b/backend/src/modules/rgbpp/address/address.service.ts index 25198916..c0575f75 100644 --- a/backend/src/modules/rgbpp/address/address.service.ts +++ b/backend/src/modules/rgbpp/address/address.service.ts @@ -2,10 +2,11 @@ import { BI } from '@ckb-lumos/bi'; import { Injectable } from '@nestjs/common'; import { CkbExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service'; import { PrismaService } from 'src/core/database/prisma/prisma.service'; -import { CkbCell, CkbXUDTInfo } from 'src/modules/ckb/cell/cell.model'; +import { CkbXUDTInfo } from 'src/modules/ckb/cell/cell.model'; import { RgbppAsset } from '../asset/asset.model'; import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.service'; import { CKB_CHAIN_ID } from 'src/constants'; +import { Cacheable } from 'src/decorators/cacheable.decorator'; @Injectable() export class RgbppAddressService { @@ -15,6 +16,11 @@ export class RgbppAddressService { private ckbRpcService: CkbRpcWebsocketService, ) { } + @Cacheable({ + namespace: 'RgbppAddressService', + key: (address: string) => `getAddressAssets:${address}`, + ttl: 10_000, + }) public async getAddressAssets(address: string) { const lockScript = await this.prismaService.lockScript.findMany({ where: { diff --git a/backend/src/modules/rgbpp/transaction/transaction.service.ts b/backend/src/modules/rgbpp/transaction/transaction.service.ts index 01ae041c..2537dd0f 100644 --- a/backend/src/modules/rgbpp/transaction/transaction.service.ts +++ b/backend/src/modules/rgbpp/transaction/transaction.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service'; import { CkbExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service'; -import { RgbppTransaction, RgbppLatestTransactionList } from './transaction.model'; +import { RgbppTransaction } from './transaction.model'; import { ConfigService } from '@nestjs/config'; import { Env } from 'src/env'; import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.service'; @@ -12,10 +12,9 @@ import { RgbppService } from '../rgbpp.service'; import { BI, HashType } from '@ckb-lumos/lumos'; import { Cacheable } from 'src/decorators/cacheable.decorator'; import { ONE_MONTH_MS } from 'src/common/date'; -import { CkbScriptService } from 'src/modules/ckb/script/script.service'; import { LeapDirection } from '@prisma/client'; import { PrismaService } from 'src/core/database/prisma/prisma.service'; -import { CKB_CHAIN_ID } from 'src/constants'; +import { CKB_CHAIN_ID, CKB_MIN_SAFE_CONFIRMATIONS } from 'src/constants'; @Injectable() export class RgbppTransactionService { @@ -30,6 +29,15 @@ export class RgbppTransactionService { private configService: ConfigService, ) { } + private async isSafeConfirmations(blockNumber: string): Promise { + try { + const tipBlockNumber = await this.ckbRpcService.getTipBlockNumber(); + return BI.from(blockNumber).lt(BI.from(tipBlockNumber).sub(CKB_MIN_SAFE_CONFIRMATIONS)); + } catch { + return false; + } + } + public async getLatestTransactions(limit: number) { const transactions = await this.prismaService.transaction.findMany({ where: { @@ -155,6 +163,17 @@ export class RgbppTransactionService { return null; } + @Cacheable({ + namespace: 'RgbppTransactionService', + key: (btcTx: BitcoinApiInterface.Transaction) => `queryRgbppLockTx:${btcTx.txid}`, + ttl: ONE_MONTH_MS, + shouldCache: (tx: RgbppTransaction, that: RgbppTransactionService) => { + if (!tx) { + return false; + } + return that.isSafeConfirmations(BI.from(tx.blockNumber).toHexString()); + }, + }) public async queryRgbppLockTx(btcTx: BitcoinApiInterface.Transaction) { const ckbTxs = await Promise.all( btcTx.vout.map(async (_, index) => { @@ -192,6 +211,17 @@ export class RgbppTransactionService { return null; } + @Cacheable({ + namespace: 'RgbppTransactionService', + key: (btcTx: BitcoinApiInterface.Transaction) => `queryRgbppBtcTimeLockTx:${btcTx.txid}`, + ttl: ONE_MONTH_MS, + shouldCache: (tx: RgbppTransaction, that: RgbppTransactionService) => { + if (!tx) { + return false; + } + return that.isSafeConfirmations(BI.from(tx.blockNumber).toHexString()); + }, + }) public async queryRgbppBtcTimeLockTx(btcTx: BitcoinApiInterface.Transaction) { const ckbTxs = ( await Promise.all( From 3ece2ef3b183b7b5a01e8e1923473496fc04c252 Mon Sep 17 00:00:00 2001 From: INS Date: Thu, 26 Sep 2024 14:11:29 +0800 Subject: [PATCH 10/27] perf(frontend): cache and query (#175) * perf(frontend): full route cache * perf(frontend): full route cache without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * perf(frontend): i18n without headers * fix(frontend): 404 page i18n * fix(frontend): 404 page i18n * fix(frontend): block page i18n * fix(frontend): isr search params * fix(frontend): build errors * perf(frontend): optimize address page query * feat(frontend): new paginator * feat(frontend): add route progress bar * feat(frontend): min PaginationSearchParams * feat(frontend): format page total * chore(frontend): spelling check --- frontend/cspell.json | 3 +- frontend/package.json | 5 +- frontend/panda.config.ts | 2 + .../app/[lang]/address/[address]/layout.tsx | 14 +- .../address/[address]/transactions/btc.tsx | 197 --- .../address/[address]/transactions/ckb.tsx | 140 --- .../[address]/transactions/not-found.ts | 6 + .../address/[address]/transactions/page.tsx | 324 ++++- .../assets/coins/[typeHash]/holders/page.tsx | 6 +- .../[lang]/assets/coins/[typeHash]/layout.tsx | 8 +- .../coins/[typeHash]/transactions/page.tsx | 16 +- frontend/src/app/[lang]/assets/coins/page.tsx | 20 +- frontend/src/app/[lang]/assets/layout.tsx | 13 +- .../block/btc/[hashOrHeight]/layout.tsx | 26 +- .../btc/[hashOrHeight]/transactions/page.tsx | 13 +- .../block/ckb/[hashOrHeight]/layout.tsx | 28 +- .../ckb/[hashOrHeight]/transactions/page.tsx | 13 +- frontend/src/app/[lang]/explorer/btc/info.tsx | 5 +- frontend/src/app/[lang]/explorer/btc/page.tsx | 11 +- frontend/src/app/[lang]/explorer/ckb/info.tsx | 5 +- frontend/src/app/[lang]/explorer/ckb/page.tsx | 11 +- frontend/src/app/[lang]/page.tsx | 14 +- .../src/app/[lang]/transaction/[tx]/btc.tsx | 9 +- .../src/app/[lang]/transaction/[tx]/ckb.tsx | 9 +- .../src/app/[lang]/transaction/[tx]/page.tsx | 11 +- frontend/src/app/layout.tsx | 4 +- frontend/src/app/not-found.tsx | 17 +- frontend/src/components/block-header.tsx | 6 +- .../components/btc/btc-address-overview.tsx | 12 +- .../src/components/btc/btc-block-overview.tsx | 6 +- .../btc/btc-transaction-card-in-address.tsx | 2 +- .../btc/btc-transaction-overview.tsx | 5 +- frontend/src/components/btc/btc-utxos.tsx | 6 +- .../components/ckb/ckb-address-overview.tsx | 12 +- .../src/components/ckb/ckb-block-overview.tsx | 5 +- frontend/src/components/ckb/ckb-cells.tsx | 5 +- frontend/src/components/ckb/ckb-diff-tags.tsx | 43 +- .../ckb/ckb-transaction-overview.tsx | 5 +- frontend/src/components/footer.tsx | 6 +- frontend/src/components/home-quick-info.tsx | 5 +- frontend/src/components/navbar/index.tsx | 18 +- .../components/pagination-searchparams.tsx | 52 +- .../src/components/transaction-header.tsx | 5 +- frontend/src/components/ui/link.tsx | 8 +- frontend/src/components/ui/number-input.tsx | 57 + .../components/ui/primitives/number-input.tsx | 54 + .../src/configs/ui-preset/number-input.ts | 121 ++ frontend/src/lib/get-i18n-from-headers.ts | 6 - frontend/src/lib/graphql.ts | 6 +- frontend/src/lib/resolve-page.ts | 9 + frontend/src/lib/resolve-searchparams-page.ts | 14 - frontend/src/locales/en/messages.po | 205 +-- frontend/src/types/route.ts | 7 + pnpm-lock.yaml | 1112 +++++++++-------- 54 files changed, 1557 insertions(+), 1165 deletions(-) delete mode 100644 frontend/src/app/[lang]/address/[address]/transactions/btc.tsx delete mode 100644 frontend/src/app/[lang]/address/[address]/transactions/ckb.tsx create mode 100644 frontend/src/app/[lang]/address/[address]/transactions/not-found.ts create mode 100644 frontend/src/components/ui/number-input.tsx create mode 100644 frontend/src/components/ui/primitives/number-input.tsx create mode 100644 frontend/src/configs/ui-preset/number-input.ts delete mode 100644 frontend/src/lib/get-i18n-from-headers.ts create mode 100644 frontend/src/lib/resolve-page.ts delete mode 100644 frontend/src/lib/resolve-searchparams-page.ts create mode 100644 frontend/src/types/route.ts diff --git a/frontend/cspell.json b/frontend/cspell.json index 0ce55886..f13b74ca 100644 --- a/frontend/cspell.json +++ b/frontend/cspell.json @@ -31,7 +31,8 @@ "bech", "lumos", "hexify", - "bytify" + "bytify", + "nprogress" ], "ignorePaths": [ "pnpm-lock.yaml", diff --git a/frontend/package.json b/frontend/package.json index ad5027fe..a4044881 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "private": true, "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "scripts": { - "prepare": "panda codegen", + "prepare": "panda codegen && graphql-codegen", "dev": "next dev", "build": "npm run lingui && next build", "start": "next start", @@ -16,7 +16,7 @@ }, "dependencies": { "@apollo/client": "^3.11.2", - "@ark-ui/react": "^3.5.0", + "@ark-ui/react": "^3.12.1", "@bitcoinerlab/secp256k1": "^1.1.1", "@ckb-lumos/lumos": "^0.23.0", "@graphql-typed-document-node/core": "^3.2.0", @@ -35,6 +35,7 @@ "lodash-es": "^4.17.21", "negotiator": "^0.6.3", "next": "14.2.4", + "next-nprogress-bar": "^2.3.13", "react": "^18", "react-dom": "^18", "typed.js": "^2.1.0", diff --git a/frontend/panda.config.ts b/frontend/panda.config.ts index e891285c..ca5dcdb5 100644 --- a/frontend/panda.config.ts +++ b/frontend/panda.config.ts @@ -4,6 +4,7 @@ import { createPreset } from '@park-ui/panda-preset' import { button } from '@/configs/ui-preset/button' import { hoverCard } from '@/configs/ui-preset/hover-card' import { iconButton } from '@/configs/ui-preset/icon-button' +import { numberInput } from '@/configs/ui-preset/number-input' import { pagination } from '@/configs/ui-preset/pagination' import { popover } from '@/configs/ui-preset/popover' import { table } from '@/configs/ui-preset/table' @@ -61,6 +62,7 @@ export default defineConfig({ tabs, hoverCard, popover, + numberInput, }, tokens: { sizes: { diff --git a/frontend/src/app/[lang]/address/[address]/layout.tsx b/frontend/src/app/[lang]/address/[address]/layout.tsx index 0bb2daeb..efb2c552 100644 --- a/frontend/src/app/[lang]/address/[address]/layout.tsx +++ b/frontend/src/app/[lang]/address/[address]/layout.tsx @@ -3,6 +3,7 @@ import { notFound } from 'next/navigation' import type { PropsWithChildren, ReactNode } from 'react' import { Flex, HStack, VStack } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import { BtcAddressOverview } from '@/components/btc/btc-address-overview' import { BtcAddressType } from '@/components/btc/btc-address-type' import { CkbAddressOverview } from '@/components/ckb/ckb-address-overview' @@ -13,7 +14,6 @@ import { Heading, Text } from '@/components/ui' import { graphql } from '@/gql' import { isValidBTCAddress } from '@/lib/btc/is-valid-btc-address' import { isValidCkbAddress } from '@/lib/ckb/is-valid-ckb-address' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' import { graphQLClient } from '@/lib/graphql' const btcAddressQuery = graphql(` @@ -44,9 +44,11 @@ const ckbAddressQuery = graphql(` export default async function Layout({ children, - params: { address }, -}: PropsWithChildren & { params: { address: string } }) { - const i18n = getI18nFromHeaders() + params: { address, lang }, +}: PropsWithChildren<{ + params: { address: string; lang: string } +}>) { + const i18n = getI18nInstance(lang) const isBtcAddress = isValidBTCAddress(address) const isCkbAddress = isValidCkbAddress(address) @@ -56,12 +58,12 @@ export default async function Layout({ if (isBtcAddress) { const data = await graphQLClient.request(btcAddressQuery, { address }) if (data?.btcAddress) { - overflow = + overflow = } } else if (isCkbAddress) { const data = await graphQLClient.request(ckbAddressQuery, { address }) if (data?.ckbAddress) { - overflow = + overflow = } } diff --git a/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx b/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx deleted file mode 100644 index 782a56a2..00000000 --- a/frontend/src/app/[lang]/address/[address]/transactions/btc.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import { t } from '@lingui/macro' -import { last } from 'lodash-es' -import { Center, HStack } from 'styled-system/jsx' - -import { BtcTransactionCardInAddress } from '@/components/btc/btc-transaction-card-in-address' -import { FailedFallback } from '@/components/failed-fallback' -import { NoData } from '@/components/no-data' -import { Button } from '@/components/ui' -import Link from '@/components/ui/link' -import { graphql } from '@/gql' -import { BitcoinTransaction, CkbTransaction } from '@/gql/graphql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' -import { getUrl } from '@/lib/get-url' -import { graphQLClient } from '@/lib/graphql' - -const query = graphql(` - query BtcTransactionByAddress($address: String!, $afterTxid: String) { - btcAddress(address: $address) { - transactions(afterTxid: $afterTxid) { - txid - rgbppTransaction { - ckbTransaction { - outputs { - txHash - index - capacity - cellType - type { - codeHash - hashType - args - } - lock { - codeHash - hashType - args - } - status { - consumed - txHash - index - } - xudtInfo { - symbol - amount - decimal - typeHash - } - } - inputs { - txHash - index - capacity - cellType - type { - codeHash - hashType - args - } - lock { - codeHash - hashType - args - } - xudtInfo { - symbol - amount - decimal - typeHash - } - status { - consumed - txHash - index - } - } - } - } - blockHeight - blockHash - txid - version - size - locktime - weight - fee - feeRate - confirmed - confirmations - transactionTime - vin { - txid - vout - scriptsig - scriptsigAsm - isCoinbase - sequence - prevout { - scriptpubkey - scriptpubkeyAsm - scriptpubkeyType - scriptpubkeyAddress - value - status { - spent - txid - vin - } - address { - address - satoshi - pendingSatoshi - transactionsCount - } - } - } - vout { - scriptpubkey - scriptpubkeyAsm - scriptpubkeyType - scriptpubkeyAddress - value - status { - spent - txid - vin - } - address { - address - satoshi - pendingSatoshi - transactionsCount - } - } - } - } - } -`) - -export async function BtcTransactionsByAddress({ address }: { address: string }) { - const i18n = getI18nFromHeaders() - const url = getUrl() - const afterTxid = url.searchParams.get('afterTxid') - - const { btcAddress } = await graphQLClient.request(query, { - address, - afterTxid, - }) - - const nextCursor = last(btcAddress?.transactions)?.txid - - if (!btcAddress) { - return - } - - return ( - <> - {!btcAddress.transactions?.length ? ( -
- {t(i18n)`No Transaction`} -
- ) : ( - btcAddress.transactions?.map(({ rgbppTransaction, ...tx }) => { - return ( - - ) - }) - )} -
- - {afterTxid ? ( - - - - ) : null} - {nextCursor ? ( - - - - ) : null} - -
- - ) -} diff --git a/frontend/src/app/[lang]/address/[address]/transactions/ckb.tsx b/frontend/src/app/[lang]/address/[address]/transactions/ckb.tsx deleted file mode 100644 index 398cabf6..00000000 --- a/frontend/src/app/[lang]/address/[address]/transactions/ckb.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { t } from '@lingui/macro' -import { Center, HStack, VStack } from 'styled-system/jsx' - -import { CkbCellTables } from '@/components/ckb/ckb-cell-tables' -import { FailedFallback } from '@/components/failed-fallback' -import { IfBreakpoint } from '@/components/if-breakpoint' -import { NoData } from '@/components/no-data' -import { PaginationSearchParams } from '@/components/pagination-searchparams' -import { TransactionHeaderInAddress } from '@/components/transaction-header-in-address' -import { Text } from '@/components/ui' -import { UtxoOrCellFooter } from '@/components/utxo-or-cell-footer' -import { graphql } from '@/gql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' -import { graphQLClient } from '@/lib/graphql' -import { resolveSearchParamsPage } from '@/lib/resolve-searchparams-page' -import { formatNumber } from '@/lib/string/format-number' - -const query = graphql(` - query CkbAddress($address: String!, $page: Int!, $pageSize: Int!) { - ckbAddress(address: $address) { - transactionsCount - transactions(page: $page, pageSize: $pageSize) { - isCellbase - blockNumber - hash - fee - size - feeRate - confirmations - inputs { - cellType - status { - consumed - txHash - index - } - txHash - index - capacity - type { - codeHash - hashType - args - } - lock { - codeHash - hashType - args - } - xudtInfo { - symbol - amount - decimal - typeHash - } - } - outputs { - txHash - cellType - index - capacity - type { - codeHash - hashType - args - } - lock { - codeHash - hashType - args - } - xudtInfo { - symbol - amount - decimal - typeHash - } - status { - consumed - txHash - index - } - } - block { - timestamp - } - } - } - } -`) - -export async function CkbTransactionsByAddress({ address }: { address: string }) { - const i18n = getI18nFromHeaders() - const page = resolveSearchParamsPage() - const pageSize = 10 - const { ckbAddress } = await graphQLClient.request(query, { - address, - page, - pageSize, - }) - - if (!ckbAddress) { - return - } - - const total = ckbAddress?.transactionsCount ?? 0 - - return ( - <> - {!ckbAddress.transactions?.length ? ( -
- {t(i18n)`No Transaction`} -
- ) : ( - ckbAddress.transactions?.map((tx) => { - return ( - - - - - - ) - }) - )} - - - {t(i18n)`Total ${formatNumber(total)} Items`} - - - - - ) -} diff --git a/frontend/src/app/[lang]/address/[address]/transactions/not-found.ts b/frontend/src/app/[lang]/address/[address]/transactions/not-found.ts new file mode 100644 index 00000000..38f7f3cb --- /dev/null +++ b/frontend/src/app/[lang]/address/[address]/transactions/not-found.ts @@ -0,0 +1,6 @@ +'use client' + +import { FailedFallback } from '@/components/failed-fallback' + +const Error = FailedFallback +export default Error diff --git a/frontend/src/app/[lang]/address/[address]/transactions/page.tsx b/frontend/src/app/[lang]/address/[address]/transactions/page.tsx index 38eb3bb4..129187fb 100644 --- a/frontend/src/app/[lang]/address/[address]/transactions/page.tsx +++ b/frontend/src/app/[lang]/address/[address]/transactions/page.tsx @@ -1,19 +1,333 @@ +import { t } from '@lingui/macro' +import { last } from 'lodash-es' import { notFound } from 'next/navigation' +import { Center, HStack, VStack } from 'styled-system/jsx' -import { BtcTransactionsByAddress } from '@/app/[lang]/address/[address]/transactions/btc' -import { CkbTransactionsByAddress } from '@/app/[lang]/address/[address]/transactions/ckb' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' +import { BtcTransactionCardInAddress } from '@/components/btc/btc-transaction-card-in-address' +import { CkbCellTables } from '@/components/ckb/ckb-cell-tables' +import { IfBreakpoint } from '@/components/if-breakpoint' +import { NoData } from '@/components/no-data' +import { PaginationSearchParams } from '@/components/pagination-searchparams' +import { TransactionHeaderInAddress } from '@/components/transaction-header-in-address' +import { Button, Text } from '@/components/ui' +import Link from '@/components/ui/link' +import { UtxoOrCellFooter } from '@/components/utxo-or-cell-footer' +import { graphql } from '@/gql' +import { BitcoinTransaction, CkbTransaction } from '@/gql/graphql' import { isValidBTCAddress } from '@/lib/btc/is-valid-btc-address' import { isValidCkbAddress } from '@/lib/ckb/is-valid-ckb-address' +import { graphQLClient } from '@/lib/graphql' +import { resolvePage } from '@/lib/resolve-page' +import { formatNumber } from '@/lib/string/format-number' export const maxDuration = 30 -export default function Page({ params: { address } }: { params: { address: string } }) { +const btcAddressTxsQuery = graphql(` + query BtcTransactionByAddress($address: String!, $afterTxid: String) { + btcAddress(address: $address) { + transactions(afterTxid: $afterTxid) { + txid + rgbppTransaction { + ckbTransaction { + outputs { + txHash + index + capacity + cellType + type { + codeHash + hashType + args + } + lock { + codeHash + hashType + args + } + status { + consumed + txHash + index + } + xudtInfo { + symbol + amount + decimal + typeHash + } + } + inputs { + txHash + index + capacity + cellType + type { + codeHash + hashType + args + } + lock { + codeHash + hashType + args + } + xudtInfo { + symbol + amount + decimal + typeHash + } + status { + consumed + txHash + index + } + } + } + } + blockHeight + blockHash + txid + version + size + locktime + weight + fee + feeRate + confirmed + confirmations + transactionTime + vin { + txid + vout + scriptsig + scriptsigAsm + isCoinbase + sequence + prevout { + scriptpubkey + scriptpubkeyAsm + scriptpubkeyType + scriptpubkeyAddress + value + status { + spent + txid + vin + } + address { + address + satoshi + pendingSatoshi + transactionsCount + } + } + } + vout { + scriptpubkey + scriptpubkeyAsm + scriptpubkeyType + scriptpubkeyAddress + value + status { + spent + txid + vin + } + address { + address + satoshi + pendingSatoshi + transactionsCount + } + } + } + } + } +`) + +const ckbAddressTxsQuery = graphql(` + query CkbAddress($address: String!, $page: Int!, $pageSize: Int!) { + ckbAddress(address: $address) { + transactionsCount + transactions(page: $page, pageSize: $pageSize) { + isCellbase + blockNumber + hash + fee + size + feeRate + confirmations + inputs { + cellType + status { + consumed + txHash + index + } + txHash + index + capacity + type { + codeHash + hashType + args + } + lock { + codeHash + hashType + args + } + xudtInfo { + symbol + amount + decimal + typeHash + } + } + outputs { + txHash + cellType + index + capacity + type { + codeHash + hashType + args + } + lock { + codeHash + hashType + args + } + xudtInfo { + symbol + amount + decimal + typeHash + } + status { + consumed + txHash + index + } + } + block { + timestamp + } + } + } + } +`) + +export default async function Page({ + params: { address, lang }, + searchParams, +}: { + params: { address: string; lang: string } + searchParams: { page?: string; afterTxid?: string } +}) { + const { afterTxid } = searchParams + const i18n = getI18nInstance(lang) if (isValidBTCAddress(address)) { - return + const { btcAddress } = await graphQLClient.request(btcAddressTxsQuery, { + address, + afterTxid, + }) + + const nextCursor = last(btcAddress?.transactions)?.txid + + if (!btcAddress) notFound() + + return ( + <> + {!btcAddress.transactions?.length ? ( +
+ {t(i18n)`No Transaction`} +
+ ) : ( + btcAddress.transactions?.map(({ rgbppTransaction, ...tx }) => { + return ( + } + key={tx.txid} + /> + ) + }) + )} +
+ + {afterTxid ? ( + + + + ) : null} + {nextCursor ? ( + + + + ) : null} + +
+ + ) } if (isValidCkbAddress(address)) { - return + const page = resolvePage(searchParams.page) + const pageSize = 10 + const { ckbAddress } = await graphQLClient.request(ckbAddressTxsQuery, { + address, + page, + pageSize, + }) + + if (!ckbAddress) notFound() + + const total = ckbAddress?.transactionsCount ?? 0 + + return ( + <> + {!ckbAddress.transactions?.length ? ( +
+ {t(i18n)`No Transaction`} +
+ ) : ( + ckbAddress.transactions?.map((tx) => { + return ( + + + + + + ) + }) + )} + + + {t(i18n)`Total ${formatNumber(total)} Items`} + + + + + ) } return notFound() } diff --git a/frontend/src/app/[lang]/assets/coins/[typeHash]/holders/page.tsx b/frontend/src/app/[lang]/assets/coins/[typeHash]/holders/page.tsx index f418a628..06453aa9 100644 --- a/frontend/src/app/[lang]/assets/coins/[typeHash]/holders/page.tsx +++ b/frontend/src/app/[lang]/assets/coins/[typeHash]/holders/page.tsx @@ -1,16 +1,16 @@ +'use client' + import { t } from '@lingui/macro' import { VStack } from 'styled-system/jsx' import ComingSoonSVG from '@/assets/coming-soon.svg' import { Text } from '@/components/ui' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' export default function Page() { - const i18n = getI18nFromHeaders() return ( - {t(i18n)`Coming soon, please stay tuned`} + {t`Coming soon, please stay tuned`} ) } diff --git a/frontend/src/app/[lang]/assets/coins/[typeHash]/layout.tsx b/frontend/src/app/[lang]/assets/coins/[typeHash]/layout.tsx index 336b373a..00bbf6b6 100644 --- a/frontend/src/app/[lang]/assets/coins/[typeHash]/layout.tsx +++ b/frontend/src/app/[lang]/assets/coins/[typeHash]/layout.tsx @@ -3,12 +3,12 @@ import { notFound } from 'next/navigation' import { PropsWithChildren } from 'react' import { Box, Flex, Grid, styled } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import BtcIcon from '@/assets/chains/btc.svg' import { Copier } from '@/components/copier' import { LinkTabs } from '@/components/link-tabs' import { Text } from '@/components/ui' import { graphql } from '@/gql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' import { graphQLClient } from '@/lib/graphql' const query = graphql(` @@ -22,10 +22,10 @@ const query = graphql(` `) export default async function AssetDetail({ - params: { typeHash }, children, -}: { params: { typeHash: string } } & PropsWithChildren) { - const i18n = getI18nFromHeaders() + params: { typeHash, lang }, +}: PropsWithChildren<{ params: { typeHash: string; lang: string } }>) { + const i18n = getI18nInstance(lang) const response = await graphQLClient.request(query, { typeHash }) if (!response.rgbppCoin) notFound() return ( diff --git a/frontend/src/app/[lang]/assets/coins/[typeHash]/transactions/page.tsx b/frontend/src/app/[lang]/assets/coins/[typeHash]/transactions/page.tsx index d4c143d5..12dad02f 100644 --- a/frontend/src/app/[lang]/assets/coins/[typeHash]/transactions/page.tsx +++ b/frontend/src/app/[lang]/assets/coins/[typeHash]/transactions/page.tsx @@ -2,15 +2,15 @@ import { t } from '@lingui/macro' import { notFound } from 'next/navigation' import { Box, HStack, VStack } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import { IfBreakpoint } from '@/components/if-breakpoint' import { LatestTxnListUI } from '@/components/latest-tx-list/ui' import { PaginationSearchParams } from '@/components/pagination-searchparams' import { Text } from '@/components/ui' import { graphql } from '@/gql' import { RgbppTransaction } from '@/gql/graphql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' import { graphQLClient } from '@/lib/graphql' -import { resolveSearchParamsPage } from '@/lib/resolve-searchparams-page' +import { resolvePage } from '@/lib/resolve-page' import { formatNumber } from '@/lib/string/format-number' const query = graphql(` @@ -82,9 +82,15 @@ const query = graphql(` } `) -export default async function Page({ params: { typeHash } }: { params: { typeHash: string } }) { - const i18n = getI18nFromHeaders() - const page = resolveSearchParamsPage() +export default async function Page({ + params: { typeHash, lang }, + searchParams, +}: { + params: { typeHash: string; lang: string } + searchParams: { page?: string } +}) { + const i18n = getI18nInstance(lang) + const page = resolvePage(searchParams.page) const pageSize = 10 const response = await graphQLClient.request(query, { typeHash, page, pageSize }) if (!response.rgbppCoin) notFound() diff --git a/frontend/src/app/[lang]/assets/coins/page.tsx b/frontend/src/app/[lang]/assets/coins/page.tsx index baa547b5..b0dd9297 100644 --- a/frontend/src/app/[lang]/assets/coins/page.tsx +++ b/frontend/src/app/[lang]/assets/coins/page.tsx @@ -1,14 +1,15 @@ import { t } from '@lingui/macro' import { Box, HStack, VStack } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import { CoinList } from '@/components/coin-list' import { IfBreakpoint } from '@/components/if-breakpoint' import { PaginationSearchParams } from '@/components/pagination-searchparams' import { Text } from '@/components/ui' import { graphql } from '@/gql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' import { graphQLClient } from '@/lib/graphql' -import { resolveSearchParamsPage } from '@/lib/resolve-searchparams-page' +import { resolvePage } from '@/lib/resolve-page' +import { formatNumber } from '@/lib/string/format-number' const query = graphql(` query RgbppCoins($page: Int!, $pageSize: Int!) { @@ -31,10 +32,15 @@ const query = graphql(` } `) -export default async function Page() { - const i18n = getI18nFromHeaders() - const page = resolveSearchParamsPage() - +export default async function Page({ + params, + searchParams, +}: { + params: { lang: string } + searchParams: { page?: string } +}) { + const i18n = getI18nInstance(params.lang) + const page = resolvePage(searchParams.page) const pageSize = 10 const response = await graphQLClient.request(query, { page, pageSize }) @@ -45,7 +51,7 @@ export default async function Page() { - {t(i18n)`Total ${response.rgbppCoins.total} Items`} + {t(i18n)`Total ${formatNumber(response.rgbppCoins.total)} Items`} diff --git a/frontend/src/app/[lang]/assets/layout.tsx b/frontend/src/app/[lang]/assets/layout.tsx index c53346e5..e976c635 100644 --- a/frontend/src/app/[lang]/assets/layout.tsx +++ b/frontend/src/app/[lang]/assets/layout.tsx @@ -1,12 +1,17 @@ import { t } from '@lingui/macro' -import type { PropsWithChildren } from 'react' +import { PropsWithChildren } from 'react' import { VStack } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import { LinkTabs } from '@/components/link-tabs' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' -export default function Layout({ children }: PropsWithChildren) { - const i18n = getI18nFromHeaders() +export default function Layout({ + params, + children, +}: PropsWithChildren<{ + params: { lang: string } +}>) { + const i18n = getI18nInstance(params.lang) return ( ) { const data = await graphQLClient.request(query, { hashOrHeight }) - if (!data?.btcBlock) notFound() + const i18n = getI18nInstance(lang) return ( - - + + ) { const data = await graphQLClient.request(query, { hashOrHeight }) - if (!data?.ckbBlock) notFound() + const i18n = getI18nInstance(lang) return ( - - + + - + {t(i18n)`Latest L1 RGB++ transaction`} diff --git a/frontend/src/app/[lang]/explorer/ckb/info.tsx b/frontend/src/app/[lang]/explorer/ckb/info.tsx index 881bae99..a2da9145 100644 --- a/frontend/src/app/[lang]/explorer/ckb/info.tsx +++ b/frontend/src/app/[lang]/explorer/ckb/info.tsx @@ -1,3 +1,4 @@ +import type { I18n } from '@lingui/core' import { t } from '@lingui/macro' import { Grid, HStack, VStack } from 'styled-system/jsx' @@ -8,11 +9,9 @@ import SpeedMediumIcon from '@/assets/speed/medium.svg' import { OverviewInfo, OverviewInfoItem, OverviewInfoTagLabel } from '@/components/overview-info' import { Heading } from '@/components/ui' import { graphql } from '@/gql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' import { graphQLClient } from '@/lib/graphql' -export async function Info() { - const i18n = getI18nFromHeaders() +export async function Info({ i18n }: { i18n: I18n }) { const { ckbChainInfo, rgbppStatistic } = await graphQLClient.request( graphql(` query CkbChainInfo { diff --git a/frontend/src/app/[lang]/explorer/ckb/page.tsx b/frontend/src/app/[lang]/explorer/ckb/page.tsx index 8c6f94ee..3d845410 100644 --- a/frontend/src/app/[lang]/explorer/ckb/page.tsx +++ b/frontend/src/app/[lang]/explorer/ckb/page.tsx @@ -1,15 +1,16 @@ import { t } from '@lingui/macro' import { Box, Grid } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import { Info } from '@/app/[lang]/explorer/ckb/info' import { ExplorerTxList } from '@/components/explorer-tx-list' import { Heading } from '@/components/ui' import { graphql } from '@/gql' import { RgbppTransaction } from '@/gql/graphql' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' import { graphQLClient } from '@/lib/graphql' -export const revalidate = 5 +export const revalidate = 10 +export const dynamic = 'force-static' const query = graphql(` query RgbppLatestL2Transactions($limit: Int!) { @@ -48,13 +49,13 @@ const query = graphql(` } `) -export default async function Page() { - const i18n = getI18nFromHeaders() +export default async function Page({ params: { lang } }: { params: { lang: string } }) { + const i18n = getI18nInstance(lang) const { rgbppLatestL2Transactions } = await graphQLClient.request(query, { limit: 10 }) return ( - + {t(i18n)`Latest L2 RGB++ transaction`} diff --git a/frontend/src/app/[lang]/page.tsx b/frontend/src/app/[lang]/page.tsx index b1df0a80..228a1155 100644 --- a/frontend/src/app/[lang]/page.tsx +++ b/frontend/src/app/[lang]/page.tsx @@ -1,17 +1,25 @@ import { t } from '@lingui/macro' +import linguiConfig from 'lingui.config.mjs' import { LastRgbppTxnsTable } from 'src/components/latest-tx-list' import { Box, Center, Flex } from 'styled-system/jsx' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import HomeBgSVG from '@/assets/home-bg.svg' // import { HomeQuickInfo } from '@/components/home-quick-info' import { HomeTitle } from '@/components/home-title' import { NetworkCards } from '@/components/network-cards' import { SearchBar } from '@/components/search-bar' import { Heading } from '@/components/ui' -import { getI18nFromHeaders } from '@/lib/get-i18n-from-headers' -export default function Home() { - const i18n = getI18nFromHeaders() +export const dynamic = 'force-static' +export const revalidate = 3600 + +export async function generateStaticParams() { + return linguiConfig.locales.map((locale) => ({ lang: locale })) +} + +export default function Home({ params: { lang } }: { params: { lang: string } }) { + const i18n = getI18nInstance(lang) return ( <>
diff --git a/frontend/src/app/[lang]/transaction/[tx]/btc.tsx b/frontend/src/app/[lang]/transaction/[tx]/btc.tsx index b855d998..e15341e8 100644 --- a/frontend/src/app/[lang]/transaction/[tx]/btc.tsx +++ b/frontend/src/app/[lang]/transaction/[tx]/btc.tsx @@ -1,3 +1,4 @@ +import type { I18n } from '@lingui/core' import { VStack } from 'styled-system/jsx' import { BtcTransactionOverview } from '@/components/btc/btc-transaction-overview' @@ -11,10 +12,12 @@ export function BTCTransactionPage({ btcTransaction, ckbTransaction, leapDirection, + i18n, }: { btcTransaction: BitcoinTransaction ckbTransaction?: CkbTransaction | null leapDirection?: LeapDirection | null + i18n: I18n }) { return ( @@ -22,15 +25,17 @@ export function BTCTransactionPage({ type={resolveLayerTypeFromRGBppTransaction({ ckbTransaction, leapDirection, btcTransaction })} txid={btcTransaction.txid} confirmations={btcTransaction.confirmations} + i18n={i18n} /> - + - {ckbTransaction ? : null} + {ckbTransaction ? : null} ) } diff --git a/frontend/src/app/[lang]/transaction/[tx]/ckb.tsx b/frontend/src/app/[lang]/transaction/[tx]/ckb.tsx index 68fbf12a..c9e6a985 100644 --- a/frontend/src/app/[lang]/transaction/[tx]/ckb.tsx +++ b/frontend/src/app/[lang]/transaction/[tx]/ckb.tsx @@ -1,3 +1,4 @@ +import type { I18n } from '@lingui/core' import { VStack } from 'styled-system/jsx' import { BtcUtxos } from '@/components/btc/btc-utxos' @@ -11,10 +12,12 @@ export function CKBTransactionPage({ ckbTransaction, btcTransaction, leapDirection, + i18n, }: { ckbTransaction: CkbTransaction btcTransaction?: BitcoinTransaction | null leapDirection?: LeapDirection | null + i18n: I18n }) { return ( @@ -22,9 +25,10 @@ export function CKBTransactionPage({ type={resolveLayerTypeFromRGBppTransaction({ ckbTransaction, leapDirection, btcTransaction })} txid={ckbTransaction.hash} confirmations={ckbTransaction.confirmations} + i18n={i18n} /> - - + + {btcTransaction ? ( ) : null} diff --git a/frontend/src/app/[lang]/transaction/[tx]/page.tsx b/frontend/src/app/[lang]/transaction/[tx]/page.tsx index 37681cff..056f4055 100644 --- a/frontend/src/app/[lang]/transaction/[tx]/page.tsx +++ b/frontend/src/app/[lang]/transaction/[tx]/page.tsx @@ -1,5 +1,6 @@ import { notFound } from 'next/navigation' +import { getI18nInstance } from '@/app/[lang]/appRouterI18n' import { BTCTransactionPage } from '@/app/[lang]/transaction/[tx]/btc' import { CKBTransactionPage } from '@/app/[lang]/transaction/[tx]/ckb' import { graphql } from '@/gql' @@ -7,6 +8,7 @@ import { BitcoinTransaction, CkbTransaction } from '@/gql/graphql' import { graphQLClient } from '@/lib/graphql' export const revalidate = 60 +export const dynamic = 'force-static' const rgbppTxQuery = graphql(` query RgbppTransaction($txidOrTxHash: String!) { @@ -263,7 +265,8 @@ const ckbTxQuery = graphql(` } `) -export default async function Page({ params: { tx } }: { params: { tx: string } }) { +export default async function Page({ params: { tx, lang } }: { params: { tx: string; lang: string } }) { + const i18n = getI18nInstance(lang) const { rgbppTransaction } = await graphQLClient.request(rgbppTxQuery, { txidOrTxHash: tx }) if (rgbppTransaction) { @@ -274,6 +277,7 @@ export default async function Page({ params: { tx } }: { params: { tx: string } btcTransaction={btcTransaction as BitcoinTransaction} ckbTransaction={ckbTransaction as CkbTransaction} leapDirection={rgbppTransaction?.leapDirection} + i18n={i18n} /> ) } @@ -284,6 +288,7 @@ export default async function Page({ params: { tx } }: { params: { tx: string } ckbTransaction={ckbTransaction as CkbTransaction} btcTransaction={btcTransaction as BitcoinTransaction} leapDirection={rgbppTransaction?.leapDirection} + i18n={i18n} /> ) } @@ -292,12 +297,12 @@ export default async function Page({ params: { tx } }: { params: { tx: string } const btcTxRes = await graphQLClient.request(btcTxQuery, { txid: tx }) if (btcTxRes.btcTransaction) { - return + return } const ckbTxRes = await graphQLClient.request(ckbTxQuery, { hash: tx }) if (ckbTxRes.ckbTransaction) { - return + return } notFound() diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 106a820b..3bae3270 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -26,11 +26,11 @@ export default function RootLayout({ children }: PropsWithChildren) { const locale = getLocaleFromHeaders() return ( - + {children} -