diff --git a/.changeset/big-pots-attack.md b/.changeset/big-pots-attack.md new file mode 100644 index 0000000000..a260922ae2 --- /dev/null +++ b/.changeset/big-pots-attack.md @@ -0,0 +1,6 @@ +--- +'@kadena/graph': patch +--- + +Add policies to non fungible token; restructure the guards and create JsonString +object diff --git a/.changeset/three-rules-mate.md b/.changeset/three-rules-mate.md new file mode 100644 index 0000000000..225e35aba4 --- /dev/null +++ b/.changeset/three-rules-mate.md @@ -0,0 +1,5 @@ +--- +'@kadena/graph-client': patch +--- + +Adjusted client to changes made in graph schema for Guards and Policies diff --git a/packages/apps/graph-client/src/components/chain-fungible-account-table/chain-fungible-account-table.tsx b/packages/apps/graph-client/src/components/chain-fungible-account-table/chain-fungible-account-table.tsx index 33dc8e10f9..e2cee4d5cd 100644 --- a/packages/apps/graph-client/src/components/chain-fungible-account-table/chain-fungible-account-table.tsx +++ b/packages/apps/graph-client/src/components/chain-fungible-account-table/chain-fungible-account-table.tsx @@ -51,8 +51,20 @@ export const FungibleChainAccountTable = ( {chainAccount.balance} - {chainAccount.guard.predicate} - {chainAccount.guard.keys.join(', ')} + + {chainAccount.guard.__typename === 'Keyset' ? ( + chainAccount.guard.predicate + ) : ( + N/A + )} + + + {chainAccount.guard.__typename === 'Keyset' ? ( + chainAccount.guard.keys.join(', ') + ) : ( + N/A + )} + ))} diff --git a/packages/apps/graph-client/src/components/token-table/token-table.tsx b/packages/apps/graph-client/src/components/token-table/token-table.tsx index f6b6c55864..9cebef84e0 100644 --- a/packages/apps/graph-client/src/components/token-table/token-table.tsx +++ b/packages/apps/graph-client/src/components/token-table/token-table.tsx @@ -44,10 +44,21 @@ export const TokenTable = (props: ITokenTableProps): JSX.Element => { {token.tokenId} {token.chainId} {token.balance} + - Predicate: {token.guard.predicate} -
- Keys: {token.guard.keys.join(', ')} + {token.guard.__typename === 'Keyset' ? ( + <> + Predicate: {token.guard.predicate} +
+ Keys: {token.guard.keys.join(', ')} + + ) : token.guard.__typename === 'JsonString' ? ( + <> + Guard: {token.guard.value} + + ) : ( + N/A + )}
); diff --git a/packages/apps/graph-client/src/graphql/fields/fungible-chain-account.graph.ts b/packages/apps/graph-client/src/graphql/fields/fungible-chain-account.graph.ts index cb554ae8bc..6ffe20b8d7 100644 --- a/packages/apps/graph-client/src/graphql/fields/fungible-chain-account.graph.ts +++ b/packages/apps/graph-client/src/graphql/fields/fungible-chain-account.graph.ts @@ -15,8 +15,10 @@ export const ALL_FUNGIBLE_CHAIN_ACCOUNT_FIELDS: DocumentNode = gql` ...CoreFungibleChainAccountFields accountName guard { - keys - predicate + ... on Keyset { + keys + predicate + } } fungibleName diff --git a/packages/apps/graph-client/src/graphql/queries.graph.ts b/packages/apps/graph-client/src/graphql/queries.graph.ts index ed60446cfc..a39d58b482 100644 --- a/packages/apps/graph-client/src/graphql/queries.graph.ts +++ b/packages/apps/graph-client/src/graphql/queries.graph.ts @@ -44,8 +44,10 @@ export const getBlockFromHash: DocumentNode = gql` } minerAccount { guard { - predicate - keys + ... on Keyset { + predicate + keys + } } } parent { @@ -88,8 +90,10 @@ export const getFungibleAccount: DocumentNode = gql` chainAccounts { ...CoreFungibleChainAccountFields guard { - keys - predicate + ... on Keyset { + keys + predicate + } } } transactions { @@ -337,8 +341,10 @@ export const getNonFungibleAccount: DocumentNode = gql` tokenId chainId guard { - predicate - keys + ... on Keyset { + predicate + keys + } } } transactions { @@ -364,8 +370,10 @@ export const getNonFungibleChainAccount: DocumentNode = gql` tokenId chainId guard { - predicate - keys + ... on Keyset { + predicate + keys + } } } transactions { diff --git a/packages/apps/graph-client/src/pages/account/overview/[fungible]/[account]/[chain].tsx b/packages/apps/graph-client/src/pages/account/overview/[fungible]/[account]/[chain].tsx index 8d6b74142c..9bfd613ec0 100644 --- a/packages/apps/graph-client/src/pages/account/overview/[fungible]/[account]/[chain].tsx +++ b/packages/apps/graph-client/src/pages/account/overview/[fungible]/[account]/[chain].tsx @@ -217,10 +217,13 @@ const ChainAccount: React.FC = () => { Guard Predicate - { + {fungibleChainAccountData.fungibleChainAccount.guard + .__typename === 'Keyset' ? ( fungibleChainAccountData.fungibleChainAccount.guard .predicate - } + ) : ( + N/A + )} @@ -228,7 +231,15 @@ const ChainAccount: React.FC = () => { Guard Keys - {fungibleChainAccountData.fungibleChainAccount.guard.keys} + + {fungibleChainAccountData.fungibleChainAccount.guard + .__typename === 'Keyset' ? ( + fungibleChainAccountData.fungibleChainAccount.guard + .keys + ) : ( + N/A + )} + diff --git a/packages/apps/graph-client/src/pages/block/overview/[hash].tsx b/packages/apps/graph-client/src/pages/block/overview/[hash].tsx index fbb4f2cb4d..f39c39ffef 100644 --- a/packages/apps/graph-client/src/pages/block/overview/[hash].tsx +++ b/packages/apps/graph-client/src/pages/block/overview/[hash].tsx @@ -202,12 +202,17 @@ const Block: React.FC = () => { Value - {data.block.minerAccount.guard.keys?.map( - (minerKey, index) => ( - - {minerKey} - - ), + {data.block.minerAccount.guard.__typename === + 'Keyset' ? ( + data.block.minerAccount.guard.keys?.map( + (minerKey, index) => ( + + {minerKey} + + ), + ) + ) : ( + N/A )} @@ -217,7 +222,14 @@ const Block: React.FC = () => { Predicate - {data.block.minerAccount.guard.predicate} + + {data.block.minerAccount.guard.__typename === + 'Keyset' ? ( + data.block.minerAccount.guard.predicate + ) : ( + N/A + )} + diff --git a/packages/apps/graph/generated-schema.graphql b/packages/apps/graph/generated-schema.graphql index febb9132ff..cf17a9cb26 100644 --- a/packages/apps/graph/generated-schema.graphql +++ b/packages/apps/graph/generated-schema.graphql @@ -212,8 +212,17 @@ type GraphConfiguration { minimumBlockHeight: BigInt } +"""A guard""" +union Guard = JsonString | Keyset + +"""A JSON string.""" +type JsonString { + type: String! + value: String! +} + """Guard for an account.""" -type Guard { +type Keyset { keys: [String!]! predicate: String! } @@ -226,6 +235,12 @@ type MinerKey implements Node { key: String! } +"""A reference to a module.""" +type ModuleReference { + name: String! + namespace: String! +} + """Information about the network.""" type NetworkInfo { """The version of the API.""" @@ -302,13 +317,6 @@ type NonFungibleChainAccountTransactionsConnectionEdge { node: Transaction! } -"""Information related to a token.""" -type NonFungibleToken { - precision: Int! - supply: Int! - uri: String! -} - """The token identifier and its balance.""" type NonFungibleTokenBalance implements Node { accountName: String! @@ -316,11 +324,19 @@ type NonFungibleTokenBalance implements Node { chainId: String! guard: Guard! id: ID! - info: NonFungibleToken + info: NonFungibleTokenInfo tokenId: String! version: String! } +"""Information related to a token.""" +type NonFungibleTokenInfo { + policies: [Policy!]! + precision: Int! + supply: Int! + uri: String! +} + input PactQuery { chainId: String! code: String! @@ -348,6 +364,12 @@ type PageInfo { startCursor: String } +"""A policy that defines the rules for a non-fungible token.""" +type Policy { + refName: ModuleReference! + refSpec: [ModuleReference!]! +} + """Floats that will have a value greater than 0.""" scalar PositiveFloat diff --git a/packages/apps/graph/src/devnet/deployment/marmalade/deploy.ts b/packages/apps/graph/src/devnet/deployment/marmalade/deploy.ts index 2b74c04640..eed9632c4d 100644 --- a/packages/apps/graph/src/devnet/deployment/marmalade/deploy.ts +++ b/packages/apps/graph/src/devnet/deployment/marmalade/deploy.ts @@ -37,11 +37,15 @@ export async function deployMarmaladeContracts( nsDestinationPath: string = marmaladeLocalConfig.namespacePath, ): Promise { logger.info('Validating repository data...'); - validateConfig( - marmaladeRepository, - marmaladeLocalConfig, - marmaladeRemoteConfig, - ); + if ( + !validateConfig( + marmaladeRepository, + marmaladeLocalConfig, + marmaladeRemoteConfig, + ) + ) { + throw new Error('Invalid repository data'); + } logger.info('Preparing directories...'); await handleDirectorySetup( @@ -362,8 +366,10 @@ export function validateConfig( repositoryConfig: IMarmaladeRepository, localConfig: IMarmaladeLocalConfig, remoteConfig: IMarmaladeRemoteConfig, -): void { - validateObjectProperties(repositoryConfig, 'Repository'); - validateObjectProperties(localConfig, 'Local'); - validateObjectProperties(remoteConfig, 'Remote'); +): boolean { + return ( + validateObjectProperties(repositoryConfig, 'Repository') && + validateObjectProperties(localConfig, 'Local') && + validateObjectProperties(remoteConfig, 'Remote') + ); } diff --git a/packages/apps/graph/src/devnet/simulation/marmalade/get-token-info.ts b/packages/apps/graph/src/devnet/simulation/marmalade/get-token-info.ts index 8986a91d54..93884d6ad4 100644 --- a/packages/apps/graph/src/devnet/simulation/marmalade/get-token-info.ts +++ b/packages/apps/graph/src/devnet/simulation/marmalade/get-token-info.ts @@ -9,6 +9,7 @@ interface ITokenInfo { precision: number; uri: string; id: string; + policies: { moduleName: string }[]; } export const getTokenInfo = async ( @@ -97,5 +98,15 @@ export const getTokenInfo = async ( } } + if ('policies' in tokenInfo) { + if (!Array.isArray(tokenInfo.policies)) { + tokenInfo.policies = tokenInfo.policies.map((policy: string) => ({ + moduleName: policy, + })); + } + } else if ('policy' in tokenInfo) { + tokenInfo.policies = { moduleName: tokenInfo.policy.toString() }; + } + return tokenInfo as ITokenInfo; }; diff --git a/packages/apps/graph/src/graph/builder.ts b/packages/apps/graph/src/graph/builder.ts index 07c4482da9..d410d93a1c 100644 --- a/packages/apps/graph/src/graph/builder.ts +++ b/packages/apps/graph/src/graph/builder.ts @@ -25,13 +25,16 @@ import type { IFungibleChainAccount, IGasLimitEstimation, IGraphConfiguration, - IGuard, + IJsonString, + IKeyset, + IModuleReference, INetworkInfo, INonFungibleAccount, INonFungibleChainAccount, - INonFungibleToken, INonFungibleTokenBalance, + INonFungibleTokenInfo, IPactQueryResponse, + IPolicy, ITransactionCapability, ITransactionCommand, ITransactionMempoolInfo, @@ -80,11 +83,11 @@ export const builder = new SchemaBuilder< FungibleChainAccount: IFungibleChainAccount; GasLimitEstimation: IGasLimitEstimation; GraphConfiguration: IGraphConfiguration; - Guard: IGuard; + Keyset: IKeyset; NonFungibleAccount: INonFungibleAccount; NonFungibleChainAccount: INonFungibleChainAccount; NonFungibleTokenBalance: INonFungibleTokenBalance; - NonFungibleToken: INonFungibleToken; + NonFungibleTokenInfo: INonFungibleTokenInfo; TransactionMeta: ITransactionMeta; ExecutionPayload: IExecutionPayload; ContinuationPayload: IContinuationPayload; @@ -94,6 +97,9 @@ export const builder = new SchemaBuilder< TransactionCapability: ITransactionCapability; TransactionSignature: ITransactionSignature; PactQueryResponse: IPactQueryResponse; + Policy: IPolicy; + ModuleReference: IModuleReference; + JsonString: IJsonString; NetworkInfo: INetworkInfo; }; } diff --git a/packages/apps/graph/src/graph/data-loaders/fungible-account-details.ts b/packages/apps/graph/src/graph/data-loaders/fungible-account-details.ts index 63954eb5c2..926ae5464c 100644 --- a/packages/apps/graph/src/graph/data-loaders/fungible-account-details.ts +++ b/packages/apps/graph/src/graph/data-loaders/fungible-account-details.ts @@ -1,5 +1,5 @@ import type { IFungibleChainAccountDetails } from '@services/chainweb-node/fungible-account-details'; -import { getFungibleAccountDetails } from '@services/chainweb-node/fungible-account-details'; +import { getFungibleAccountDetailsWithRetry } from '@services/chainweb-node/fungible-account-details'; import DataLoader from 'dataloader'; interface IFungibleAccountDetailsKey { @@ -14,7 +14,7 @@ export const fungibleAccountDetailsLoader = new DataLoader< >(async (keys: readonly IFungibleAccountDetailsKey[]) => { const results = await Promise.all( keys.map(({ fungibleName, accountName, chainId }) => - getFungibleAccountDetails(fungibleName, accountName, chainId), + getFungibleAccountDetailsWithRetry(fungibleName, accountName, chainId), ), ); diff --git a/packages/apps/graph/src/graph/data-loaders/non-fungible-account-details.ts b/packages/apps/graph/src/graph/data-loaders/non-fungible-account-details.ts index e4a34fadb4..e47a288c6e 100644 --- a/packages/apps/graph/src/graph/data-loaders/non-fungible-account-details.ts +++ b/packages/apps/graph/src/graph/data-loaders/non-fungible-account-details.ts @@ -1,11 +1,12 @@ import type { INonFungibleChainAccountDetails } from '@services/chainweb-node/non-fungible-account-details'; -import { getNonFungibleAccountDetails } from '@services/chainweb-node/non-fungible-account-details'; +import { getNonFungibleAccountDetailsWithRetry } from '@services/chainweb-node/non-fungible-account-details'; import DataLoader from 'dataloader'; interface INonFungibleAccountDetailsKey { tokenId: string; accountName: string; chainId: string; + version: string; } export const nonFungibleAccountDetailsLoader = new DataLoader< @@ -13,8 +14,13 @@ export const nonFungibleAccountDetailsLoader = new DataLoader< INonFungibleChainAccountDetails | null >(async (keys: readonly INonFungibleAccountDetailsKey[]) => { const results = await Promise.all( - keys.map(({ tokenId, accountName, chainId }) => - getNonFungibleAccountDetails(tokenId, accountName, chainId), + keys.map(({ tokenId, accountName, chainId, version }) => + getNonFungibleAccountDetailsWithRetry( + tokenId, + accountName, + chainId, + version, + ), ), ); diff --git a/packages/apps/graph/src/graph/data-loaders/non-fungible-token-balances.ts b/packages/apps/graph/src/graph/data-loaders/non-fungible-token-balances.ts new file mode 100644 index 0000000000..dfa8654fa1 --- /dev/null +++ b/packages/apps/graph/src/graph/data-loaders/non-fungible-token-balances.ts @@ -0,0 +1,21 @@ +import { getNonFungibleTokenBalances } from '@services/token-service'; +import DataLoader from 'dataloader'; +import type { INonFungibleTokenBalance } from '../types/graphql-types'; + +interface INonFungibleTokenBalancesKey { + accountName: string; + chainId?: string; +} + +export const nonFungibleTokenBalancesLoader = new DataLoader< + INonFungibleTokenBalancesKey, + INonFungibleTokenBalance[] +>(async (keys: readonly INonFungibleTokenBalancesKey[]) => { + const results = await Promise.all( + keys.map(({ accountName, chainId }) => + getNonFungibleTokenBalances(accountName, chainId), + ), + ); + + return results; +}); diff --git a/packages/apps/graph/src/graph/data-loaders/non-fungible-token-info.ts b/packages/apps/graph/src/graph/data-loaders/non-fungible-token-info.ts new file mode 100644 index 0000000000..51f208dd5f --- /dev/null +++ b/packages/apps/graph/src/graph/data-loaders/non-fungible-token-info.ts @@ -0,0 +1,25 @@ +import { getNonFungibleTokenInfoWithRetry } from '@services/token-service'; +import DataLoader from 'dataloader'; +import type { INonFungibleTokenInfo } from '../types/graphql-types'; + +interface INonFungibleTokenInfoKey { + tokenId: string; + chainId: string; + version: string; +} + +/** + * Get the info for a non-fungible token + */ +export const nonFungibleTokenInfoLoader = new DataLoader< + INonFungibleTokenInfoKey, + INonFungibleTokenInfo | null +>(async (keys: readonly INonFungibleTokenInfoKey[]) => { + const results = await Promise.all( + keys.map(({ tokenId, chainId, version }) => + getNonFungibleTokenInfoWithRetry(tokenId, chainId, version), + ), + ); + + return results; +}); diff --git a/packages/apps/graph/src/graph/data-loaders/token-details.ts b/packages/apps/graph/src/graph/data-loaders/token-details.ts deleted file mode 100644 index e5b336ef71..0000000000 --- a/packages/apps/graph/src/graph/data-loaders/token-details.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getTokenDetails } from '@services/token-service'; -import DataLoader from 'dataloader'; -import type { INonFungibleTokenBalance } from '../types/graphql-types'; - -interface ITokenDetailsKey { - accountName: string; - chainId?: string; -} - -export const tokenDetailsLoader = new DataLoader< - ITokenDetailsKey, - INonFungibleTokenBalance[] ->(async (keys: readonly ITokenDetailsKey[]) => { - const results = await Promise.all( - keys.map(({ accountName, chainId }) => - getTokenDetails(accountName, chainId), - ), - ); - - return results; -}); diff --git a/packages/apps/graph/src/graph/index.ts b/packages/apps/graph/src/graph/index.ts index 9aad9c181f..1f69c7d884 100644 --- a/packages/apps/graph/src/graph/index.ts +++ b/packages/apps/graph/src/graph/index.ts @@ -5,13 +5,13 @@ import './objects/fungible-account'; import './objects/fungible-chain-account'; import './objects/gas-limit-estimation'; import './objects/graph-configuration'; -import './objects/guard'; +import './objects/keyset'; import './objects/miner-key'; import './objects/network-info'; import './objects/non-fungible-account'; import './objects/non-fungible-chain-account'; -import './objects/non-fungible-token'; import './objects/non-fungible-token-balance'; +import './objects/non-fungible-token-info'; import './objects/pact-query-response'; import './objects/signer'; import './objects/transaction'; diff --git a/packages/apps/graph/src/graph/objects/block.ts b/packages/apps/graph/src/graph/objects/block.ts index d790dec41f..4348755c16 100644 --- a/packages/apps/graph/src/graph/objects/block.ts +++ b/packages/apps/graph/src/graph/objects/block.ts @@ -7,7 +7,7 @@ import { } from '@services/complexity'; import { normalizeError } from '@utils/errors'; import { builder } from '../builder'; -import type { IGuard } from '../types/graphql-types'; +import type { IKeyset } from '../types/graphql-types'; import { FungibleChainAccountName } from '../types/graphql-types'; import FungibleChainAccount from './fungible-chain-account'; @@ -56,7 +56,7 @@ export default builder.prismaNode(Prisma.ModelName.Block, { }, }) )?.map((x) => x.key), - predicate: parent.predicate as IGuard['predicate'], + predicate: parent.predicate as IKeyset['predicate'], }, balance: 0, transactions: [], diff --git a/packages/apps/graph/src/graph/objects/fungible-chain-account.ts b/packages/apps/graph/src/graph/objects/fungible-chain-account.ts index fb8268ab27..ed5ce08784 100644 --- a/packages/apps/graph/src/graph/objects/fungible-chain-account.ts +++ b/packages/apps/graph/src/graph/objects/fungible-chain-account.ts @@ -59,6 +59,7 @@ export default builder.node( chainId: parent.chainId, }); + //If the account does not exist, the resolver will return null so we can safely use the non-null assertion operator return { keys: accountDetails!.guard.keys, predicate: accountDetails!.guard.pred, diff --git a/packages/apps/graph/src/graph/objects/guard.ts b/packages/apps/graph/src/graph/objects/guard.ts index 98ed495c50..179459d786 100644 --- a/packages/apps/graph/src/graph/objects/guard.ts +++ b/packages/apps/graph/src/graph/objects/guard.ts @@ -1,9 +1,15 @@ import { builder } from '../builder'; +import JsonString from './json-string'; +import Keyset from './keyset'; -export default builder.objectType('Guard', { - description: 'Guard for an account.', - fields: (t) => ({ - keys: t.exposeStringList('keys'), - predicate: t.exposeString('predicate'), - }), +export default builder.unionType('Guard', { + description: 'A guard', + types: [Keyset, JsonString], + resolveType(guard) { + if ('keys' in guard) { + return Keyset.name; + } else { + return JsonString.name; + } + }, }); diff --git a/packages/apps/graph/src/graph/objects/json-string.ts b/packages/apps/graph/src/graph/objects/json-string.ts new file mode 100644 index 0000000000..1d14b7c27e --- /dev/null +++ b/packages/apps/graph/src/graph/objects/json-string.ts @@ -0,0 +1,9 @@ +import { builder } from '../builder'; + +export default builder.objectType('JsonString', { + description: 'A JSON string.', + fields: (t) => ({ + type: t.exposeString('type'), + value: t.exposeString('value'), + }), +}); diff --git a/packages/apps/graph/src/graph/objects/keyset.ts b/packages/apps/graph/src/graph/objects/keyset.ts new file mode 100644 index 0000000000..e64fd8de31 --- /dev/null +++ b/packages/apps/graph/src/graph/objects/keyset.ts @@ -0,0 +1,9 @@ +import { builder } from '../builder'; + +export default builder.objectType('Keyset', { + description: 'Guard for an account.', + fields: (t) => ({ + keys: t.exposeStringList('keys'), + predicate: t.exposeString('predicate'), + }), +}); diff --git a/packages/apps/graph/src/graph/objects/module-reference.ts b/packages/apps/graph/src/graph/objects/module-reference.ts new file mode 100644 index 0000000000..24c5a10db8 --- /dev/null +++ b/packages/apps/graph/src/graph/objects/module-reference.ts @@ -0,0 +1,9 @@ +import { builder } from '../builder'; + +export default builder.objectType('ModuleReference', { + description: 'A reference to a module.', + fields: (t) => ({ + name: t.exposeString('name'), + namespace: t.exposeString('namespace'), + }), +}); diff --git a/packages/apps/graph/src/graph/objects/non-fungible-account.ts b/packages/apps/graph/src/graph/objects/non-fungible-account.ts index 9bd3ca0706..c7dfd2c976 100644 --- a/packages/apps/graph/src/graph/objects/non-fungible-account.ts +++ b/packages/apps/graph/src/graph/objects/non-fungible-account.ts @@ -9,7 +9,7 @@ import { dotenv } from '@utils/dotenv'; import { normalizeError } from '@utils/errors'; import { builder } from '../builder'; import { nonFungibleChainCheck } from '../data-loaders/non-fungible-chain-check'; -import { tokenDetailsLoader } from '../data-loaders/token-details'; +import { nonFungibleTokenBalancesLoader } from '../data-loaders/non-fungible-token-balances'; import type { INonFungibleAccount, INonFungibleChainAccount, @@ -18,7 +18,7 @@ import { NonFungibleAccountName, NonFungibleChainAccountName, } from '../types/graphql-types'; -import Token from './non-fungible-token-balance'; +import NonFungibleTokenBalance from './non-fungible-token-balance'; export default builder.node( builder.objectRef(NonFungibleAccountName), @@ -77,11 +77,11 @@ export default builder.node( }, }), nonFungibleTokenBalances: t.field({ - type: [Token], + type: [NonFungibleTokenBalance], complexity: COMPLEXITY.FIELD.PRISMA_WITHOUT_RELATIONS, async resolve(parent) { try { - const tokenDetails = await tokenDetailsLoader.load({ + const tokenDetails = await nonFungibleTokenBalancesLoader.load({ accountName: parent.accountName, }); diff --git a/packages/apps/graph/src/graph/objects/non-fungible-chain-account.ts b/packages/apps/graph/src/graph/objects/non-fungible-chain-account.ts index 9b0f7d1878..b5e0d25200 100644 --- a/packages/apps/graph/src/graph/objects/non-fungible-chain-account.ts +++ b/packages/apps/graph/src/graph/objects/non-fungible-chain-account.ts @@ -7,7 +7,7 @@ import { } from '@services/complexity'; import { normalizeError } from '@utils/errors'; import { builder } from '../builder'; -import { tokenDetailsLoader } from '../data-loaders/token-details'; +import { nonFungibleTokenBalancesLoader } from '../data-loaders/non-fungible-token-balances'; import type { INonFungibleChainAccount } from '../types/graphql-types'; import { NonFungibleChainAccountName } from '../types/graphql-types'; import NonFungibleTokenBalance from './non-fungible-token-balance'; @@ -45,7 +45,7 @@ export default builder.node( complexity: COMPLEXITY.FIELD.PRISMA_WITHOUT_RELATIONS, async resolve(parent) { try { - const tokenDetails = await tokenDetailsLoader.load({ + const tokenDetails = await nonFungibleTokenBalancesLoader.load({ accountName: parent.accountName, chainId: parent.chainId, }); diff --git a/packages/apps/graph/src/graph/objects/non-fungible-token-balance.ts b/packages/apps/graph/src/graph/objects/non-fungible-token-balance.ts index 4feac23618..ba8abc8169 100644 --- a/packages/apps/graph/src/graph/objects/non-fungible-token-balance.ts +++ b/packages/apps/graph/src/graph/objects/non-fungible-token-balance.ts @@ -1,11 +1,12 @@ -import { getTokenInfo } from '@devnet/simulation/marmalade/get-token-info'; -import type { ChainId } from '@kadena/types'; +import { COMPLEXITY } from '@services/complexity'; import { normalizeError } from '@utils/errors'; import { builder } from '../builder'; -import { tokenDetailsLoader } from '../data-loaders/token-details'; +import { nonFungibleTokenBalancesLoader } from '../data-loaders/non-fungible-token-balances'; +import { nonFungibleTokenInfoLoader } from '../data-loaders/non-fungible-token-info'; import type { INonFungibleTokenBalance } from '../types/graphql-types'; import { NonFungibleTokenBalanceName } from '../types/graphql-types'; -import NonFungibleToken from './non-fungible-token'; +import Guard from './guard'; +import NonFungibleTokenInfo from './non-fungible-token-info'; export default builder.node( builder.objectRef(NonFungibleTokenBalanceName), @@ -27,7 +28,7 @@ export default builder.node( async loadOne({ tokenId, accountName, chainId }) { try { return ( - await tokenDetailsLoader.load({ + await nonFungibleTokenBalancesLoader.load({ accountName, chainId, }) @@ -43,21 +44,24 @@ export default builder.node( chainId: t.exposeString('chainId'), version: t.exposeString('version'), guard: t.field({ - type: 'Guard', + type: Guard, resolve(parent) { return parent.guard; }, }), info: t.field({ - type: NonFungibleToken, + type: NonFungibleTokenInfo, + complexity: COMPLEXITY.FIELD.CHAINWEB_NODE, nullable: true, async resolve(parent) { try { - return await getTokenInfo( - parent.tokenId, - parent.chainId.toString() as ChainId, - parent.version, - ); + const tokenInfo = await nonFungibleTokenInfoLoader.load({ + tokenId: parent.tokenId, + chainId: parent.chainId, + version: parent.version, + }); + + return tokenInfo; } catch (error) { throw normalizeError(error); } diff --git a/packages/apps/graph/src/graph/objects/non-fungible-token.ts b/packages/apps/graph/src/graph/objects/non-fungible-token-info.ts similarity index 54% rename from packages/apps/graph/src/graph/objects/non-fungible-token.ts rename to packages/apps/graph/src/graph/objects/non-fungible-token-info.ts index 45ebb7b242..79e4db6a83 100644 --- a/packages/apps/graph/src/graph/objects/non-fungible-token.ts +++ b/packages/apps/graph/src/graph/objects/non-fungible-token-info.ts @@ -1,10 +1,15 @@ import { builder } from '../builder'; +import Policy from './policy'; -export default builder.objectType('NonFungibleToken', { +export default builder.objectType('NonFungibleTokenInfo', { description: 'Information related to a token.', fields: (t) => ({ supply: t.exposeInt('supply'), precision: t.exposeInt('precision'), uri: t.exposeString('uri'), + policies: t.field({ + type: [Policy], + resolve: (parent) => parent.policies, + }), }), }); diff --git a/packages/apps/graph/src/graph/objects/policy.ts b/packages/apps/graph/src/graph/objects/policy.ts new file mode 100644 index 0000000000..0dfb0daa7c --- /dev/null +++ b/packages/apps/graph/src/graph/objects/policy.ts @@ -0,0 +1,16 @@ +import { builder } from '../builder'; +import ModuleReference from './module-reference'; + +export default builder.objectType('Policy', { + description: 'A policy that defines the rules for a non-fungible token.', + fields: (t) => ({ + refSpec: t.field({ + type: [ModuleReference], + resolve: (parent) => parent.refSpec, + }), + refName: t.field({ + type: ModuleReference, + resolve: (parent) => parent.refName, + }), + }), +}); diff --git a/packages/apps/graph/src/graph/types/graphql-types.ts b/packages/apps/graph/src/graph/types/graphql-types.ts index 7f42f095c5..fdf2422b03 100644 --- a/packages/apps/graph/src/graph/types/graphql-types.ts +++ b/packages/apps/graph/src/graph/types/graphql-types.ts @@ -1,6 +1,6 @@ import type { Signer, Transaction, Transfer } from '@prisma/client'; -export interface IGuard { +export interface IKeyset { keys: string[]; predicate: 'keys-all' | 'keys-any' | 'keys-two'; } @@ -22,17 +22,26 @@ export interface INonFungibleTokenBalance { balance: number; accountName: string; chainId: string; - guard: IGuard; - info?: INonFungibleToken; + guard: IKeyset | IJsonString; + info?: INonFungibleTokenInfo; version: string; } -export interface INonFungibleToken { +export interface IModuleReference { + name: string; + namespace: string; +} + +export interface IPolicy { + refSpec: IModuleReference[]; + refName: IModuleReference; +} + +export interface INonFungibleTokenInfo { supply: number; precision: number; uri: string; - // TODO: figure out what to do with weird pact-arrays - // policies: string[]; + policies: [IPolicy]; } export const FungibleChainAccountName: 'FungibleChainAccount' = @@ -43,7 +52,7 @@ export interface IFungibleChainAccount { chainId: string; fungibleName: string; accountName: string; - guard: IGuard; + guard: IKeyset; balance: number; transactions: Transaction[]; transfers: Transfer[]; @@ -161,6 +170,10 @@ export interface IPactQueryResponse { code: string; } +export interface IJsonString { + type: string; + value: string; +} export interface INetworkInfo { networkHost: string; networkId: string; diff --git a/packages/apps/graph/src/services/account-service.ts b/packages/apps/graph/src/services/account-service.ts index fe02aeb3cc..890541edcc 100644 --- a/packages/apps/graph/src/services/account-service.ts +++ b/packages/apps/graph/src/services/account-service.ts @@ -1,5 +1,5 @@ import { fungibleAccountDetailsLoader } from '../graph/data-loaders/fungible-account-details'; -import { tokenDetailsLoader } from '../graph/data-loaders/token-details'; +import { nonFungibleTokenBalancesLoader } from '../graph/data-loaders/non-fungible-token-balances'; import type { IFungibleChainAccount, INonFungibleChainAccount, @@ -24,21 +24,23 @@ export async function getFungibleChainAccount({ chainId, }); - return accountDetails !== null - ? { - __typename: FungibleChainAccountName, - chainId, - accountName, - fungibleName, - guard: { - keys: accountDetails.guard.keys, - predicate: accountDetails.guard.pred, - }, - balance: accountDetails.balance, - transactions: [], - transfers: [], - } - : null; + if (!accountDetails || accountDetails === null) return null; + + const guard = { + predicate: accountDetails.guard.pred, + keys: accountDetails.guard.keys, + }; + + return { + __typename: FungibleChainAccountName, + chainId, + accountName, + fungibleName, + guard, + balance: accountDetails.balance, + transactions: [], + transfers: [], + }; } export async function getNonFungibleChainAccount({ @@ -48,7 +50,10 @@ export async function getNonFungibleChainAccount({ chainId: string; accountName: string; }): Promise { - const tokenDetails = await tokenDetailsLoader.load({ accountName, chainId }); + const tokenDetails = await nonFungibleTokenBalancesLoader.load({ + accountName, + chainId, + }); return tokenDetails !== null && tokenDetails.length !== 0 ? { diff --git a/packages/apps/graph/src/services/chainweb-node/fungible-account-details.ts b/packages/apps/graph/src/services/chainweb-node/fungible-account-details.ts index 056e9ceb2c..ca4ba6f572 100644 --- a/packages/apps/graph/src/services/chainweb-node/fungible-account-details.ts +++ b/packages/apps/graph/src/services/chainweb-node/fungible-account-details.ts @@ -2,7 +2,8 @@ import { details } from '@kadena/client-utils/coin'; import type { ChainId } from '@kadena/types'; import { dotenv } from '@utils/dotenv'; import { networkData } from '@utils/network'; -import type { IGuard } from '../../graph/types/graphql-types'; +import { withRetry } from '@utils/withRetry'; +import type { IKeyset } from '../../graph/types/graphql-types'; import { PactCommandError } from './utils'; export interface IFungibleChainAccountDetails { @@ -10,7 +11,7 @@ export interface IFungibleChainAccountDetails { balance: number; guard: { keys: string[]; - pred: IGuard['predicate']; + pred: IKeyset['predicate']; }; } @@ -18,8 +19,6 @@ export async function getFungibleAccountDetails( fungibleName: string, accountName: string, chainId: string, - retries = dotenv.CHAINWEB_NODE_RETRY_ATTEMPTS, - delay = dotenv.CHAINWEB_NODE_RETRY_DELAY, ): Promise { let result; @@ -45,17 +44,13 @@ export async function getFungibleAccountDetails( ) { return null; } else { - if (retries > 0) { - await new Promise((resolve) => setTimeout(resolve, delay)); - return getFungibleAccountDetails( - fungibleName, - accountName, - chainId, - retries - 1, - ); - } else { - throw new PactCommandError('Pact Command failed with error', result); - } + throw new PactCommandError('Pact Command failed with error', error); } } } + +export const getFungibleAccountDetailsWithRetry = withRetry( + getFungibleAccountDetails, + dotenv.CHAINWEB_NODE_RETRY_ATTEMPTS, + dotenv.CHAINWEB_NODE_RETRY_DELAY, +); diff --git a/packages/apps/graph/src/services/chainweb-node/non-fungible-account-details.ts b/packages/apps/graph/src/services/chainweb-node/non-fungible-account-details.ts index 78db0be052..adaa01d8ce 100644 --- a/packages/apps/graph/src/services/chainweb-node/non-fungible-account-details.ts +++ b/packages/apps/graph/src/services/chainweb-node/non-fungible-account-details.ts @@ -1,71 +1,57 @@ -import type { IClient } from '@kadena/client'; -import { Pact, createClient } from '@kadena/client'; -import type { ChainId } from '@kadena/types'; +import { getNonFungibleTokenDetails } from '@services/token-service'; import { dotenv } from '@utils/dotenv'; -import { networkData } from '@utils/network'; -import type { IGuard } from '../../graph/types/graphql-types'; +import { withRetry } from '@utils/withRetry'; +import type { IJsonString, IKeyset } from '../../graph/types/graphql-types'; import { PactCommandError } from './utils'; export interface INonFungibleChainAccountDetails { id: string; account: string; balance: number; - guard: { - keys: string[]; - pred: IGuard['predicate']; - }; -} - -function getClient(chainId: string): IClient { - return createClient( - `${dotenv.NETWORK_HOST}/chainweb/${networkData.apiVersion}/${networkData.networkId}/chain/${chainId}/pact`, - ); + guard: IKeyset | IJsonString; } export async function getNonFungibleAccountDetails( tokenId: string, accountName: string, chainId: string, + version?: string, ): Promise { - let result; - try { - let commandResult; - - commandResult = await getClient(chainId).dirtyRead( - Pact.builder - .execution( - Pact.modules['marmalade.ledger'].details(tokenId, accountName), - ) - .setMeta({ - chainId: chainId as ChainId, - }) - .setNetworkId(networkData.networkId) - .createTransaction(), + const commandResult = await getNonFungibleTokenDetails( + tokenId, + accountName, + chainId, + version, ); - if (commandResult.result.status === 'failure') { - commandResult = await getClient(chainId).dirtyRead( - Pact.builder - .execution( - Pact.modules['marmalade-v2.ledger'].details(tokenId, accountName), - ) - .setMeta({ - chainId: chainId as ChainId, - }) - .setNetworkId(networkData.networkId) - .createTransaction(), - ); - } - const result = // eslint-disable-next-line @typescript-eslint/no-explicit-any - (commandResult.result as unknown as any).data as unknown as any; + commandResult as unknown as any as unknown as any; if (typeof result.balance === 'object') { result.balance = parseFloat(result.balance.decimal); } + if ('guard' in result) { + if ( + 'keys' in result.guard && + typeof result.guard.keys === 'object' && + 'pred' in result.guard && + typeof result.guard.pred === 'string' + ) { + result.guard = { + keys: result.guard.keys, + predicate: result.guard.pred, + }; + } else { + result.guard = { + type: 'Guard', + value: JSON.stringify(result.guard), + }; + } + } + return result as INonFungibleChainAccountDetails; } catch (error) { if ( @@ -73,7 +59,13 @@ export async function getNonFungibleAccountDetails( ) { return null; } else { - throw new PactCommandError('Pact Command failed with error', result); + throw new PactCommandError('Pact Command failed with error', error); } } } + +export const getNonFungibleAccountDetailsWithRetry = withRetry( + getNonFungibleAccountDetails, + dotenv.CHAINWEB_NODE_RETRY_ATTEMPTS, + dotenv.CHAINWEB_NODE_RETRY_DELAY, +); diff --git a/packages/apps/graph/src/services/token-service.ts b/packages/apps/graph/src/services/token-service.ts index ec899a72f0..56eface059 100644 --- a/packages/apps/graph/src/services/token-service.ts +++ b/packages/apps/graph/src/services/token-service.ts @@ -1,11 +1,19 @@ import { prismaClient } from '@db/prisma-client'; +import type { ChainId } from '@kadena/client'; +import { Pact } from '@kadena/client'; +import { dirtyReadClient } from '@kadena/client-utils/core'; +import { composePactCommand, execution, setMeta } from '@kadena/client/fp'; import type { Prisma } from '@prisma/client'; import { dotenv } from '@utils/dotenv'; +import { withRetry } from '@utils/withRetry'; import { nonFungibleAccountDetailsLoader } from '../graph/data-loaders/non-fungible-account-details'; -import type { INonFungibleTokenBalance } from '../graph/types/graphql-types'; +import type { + INonFungibleTokenBalance, + INonFungibleTokenInfo, +} from '../graph/types/graphql-types'; import { NonFungibleTokenBalanceName } from '../graph/types/graphql-types'; -export async function getTokenDetails( +export async function getNonFungibleTokenBalances( accountName: string, chainId?: string, ): Promise { @@ -63,18 +71,22 @@ export async function getTokenDetails( tokenId: event.token, accountName: accountName, chainId: finalChainId, + version: event.version!, }); + if (!accountDetails) { + throw new Error( + `Account details not found for token ${event.token} and account ${accountName}`, + ); + } + result.push({ __typename: NonFungibleTokenBalanceName, balance: Number(balance), accountName, tokenId: event.token, chainId: finalChainId, - guard: { - keys: accountDetails!.guard.keys, - predicate: accountDetails!.guard.pred, - }, + guard: accountDetails.guard, version: event.version!, }); processedTokens.add(tokenChainIdKey); @@ -110,3 +122,166 @@ export async function checkAccountChains( return Array.from(chainIds); } + +export async function getNonFungibleTokenInfo( + tokenId: string, + chainId: string, + version: string, +): Promise { + if (version !== 'v1' && version !== 'v2') { + throw new Error( + `Invalid version found for token ${tokenId}. Got ${version} but expected v1 or v2.`, + ); + } + + let executionCmd; + let tokenInfo; + + if (version === 'v1') { + executionCmd = execution( + Pact.modules['marmalade.ledger']['get-policy-info'](tokenId), + ); + // Note: Alternative approach left for reference + // executionCmd = execution(`(bind + // (marmalade.ledger.get-policy-info "${tokenId}") + // {"token" := token } + // (bind + // token + // { "id" := id, "precision":= precision, "supply":= supply, "manifest":= manifest } + // { "id": id, "precision": precision, "supply": supply, "uri": + // (format + // "data:{},{}" + // [ + // (at 'scheme (at 'uri manifest)) + // (at 'data (at 'uri manifest)) + // ] + // ) + // } + // ) + // )`); + } else { + executionCmd = execution( + Pact.modules['marmalade-v2.ledger']['get-token-info'](tokenId), + ); + // Note: Alternative approach left for reference + // executionCmd = execution(`(bind + // (marmalade-v2.ledger.get-token-info "${tokenId}") + // { "id" := id, "precision":= precision, "supply" := supply, "uri" := uri } + // { "id" : id, "precision": precision, "supply" : supply, "uri" : uri } + // )`); + } + + const command = composePactCommand( + executionCmd, + + setMeta({ + chainId: chainId as ChainId, + }), + ); + + const config = { + host: dotenv.NETWORK_HOST, + defaults: { + networkId: dotenv.NETWORK_ID, + }, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tokenInfoResult = await dirtyReadClient(config)(command).execute(); + + if (!tokenInfoResult) { + return null; + } + + // Note: Alternative approach left for reference + if (version === 'v1') { + if ('token' in tokenInfoResult) { + tokenInfo = tokenInfoResult.token; + } + + // if ('policy' in tokenInfoResult) { + // policies = Array(tokenInfoResult.policy); + // } + + if ('manifest' in tokenInfo) { + tokenInfo.uri = `data:${tokenInfo.manifest.uri.scheme},${tokenInfo.manifest.uri.data}`; + } + } else { + tokenInfo = tokenInfoResult; + // if ('policies' in tokenInfoResult) { + // policies = tokenInfoResult.policies; + // } + } + + if ('precision' in tokenInfo) { + if ( + typeof tokenInfo.precision === 'object' && + tokenInfo.precision !== null + ) { + tokenInfo.precision = (tokenInfo.precision as { int: number }).int; + } + } + + if ('policy' in tokenInfoResult || 'policies' in tokenInfoResult) { + if (version === 'v1') { + tokenInfo.policies = Array(tokenInfoResult.policy); + } else { + tokenInfo.policies = tokenInfoResult.policies; + } + } + + return tokenInfo as INonFungibleTokenInfo; +} + +export const getNonFungibleTokenInfoWithRetry = withRetry( + getNonFungibleTokenInfo, + dotenv.CHAINWEB_NODE_RETRY_ATTEMPTS, + dotenv.CHAINWEB_NODE_RETRY_DELAY, +); + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export async function getNonFungibleTokenDetails( + tokenId: string, + accountName: string, + chainId: string, + version?: string, +) { + const config = { + host: dotenv.NETWORK_HOST, + defaults: { + networkId: dotenv.NETWORK_ID, + }, + }; + + const commandV2 = composePactCommand( + execution( + Pact.modules['marmalade-v2.ledger'].details(tokenId, accountName), + ), + setMeta({ + chainId: chainId as ChainId, + }), + ); + + const commandV1 = composePactCommand( + execution(Pact.modules['marmalade.ledger'].details(tokenId, accountName)), + setMeta({ + chainId: chainId as ChainId, + }), + ); + + if (version === 'v2') { + return dirtyReadClient(config)(commandV2).execute(); + } else if (version === 'v1') { + return dirtyReadClient(config)(commandV1).execute(); + } else { + try { + return await dirtyReadClient(config)(commandV1).execute(); + } catch (error) { + // As safety measure, we're doing a timeout before retrying the command + await new Promise((resolve) => + setTimeout(resolve, dotenv.CHAINWEB_NODE_RETRY_DELAY), + ); + return await dirtyReadClient(config)(commandV2).execute(); + } + } +} diff --git a/packages/apps/graph/src/utils/errors.ts b/packages/apps/graph/src/utils/errors.ts index 3373c088ac..791d1b5435 100644 --- a/packages/apps/graph/src/utils/errors.ts +++ b/packages/apps/graph/src/utils/errors.ts @@ -191,6 +191,23 @@ export function normalizeError(error: unknown): GraphQLError { }); } + if ( + error instanceof Error && + 'code' in error && + 'type' in error && + error.code === 'ECONNRESET' + ) { + return new GraphQLError('Chainweb Node Connection Reset', { + extensions: { + type: error.type, + message: error.message, + description: + 'Chainweb Node connection reset. Check if you have reached any rate limits, or if the Chainweb Node is overloaded.', + data: error.stack, + }, + }); + } + return new GraphQLError('Unknown error occured.', { extensions: { type: 'UnknownError', diff --git a/packages/apps/graph/src/utils/validate-object.ts b/packages/apps/graph/src/utils/validate-object.ts index 0159ce2552..c6e2516465 100644 --- a/packages/apps/graph/src/utils/validate-object.ts +++ b/packages/apps/graph/src/utils/validate-object.ts @@ -4,11 +4,13 @@ import { nullishOrEmpty } from './nullish-or-empty'; export function validateObjectProperties>( obj: T, typeName: string, -): void { +): boolean { Object.entries(obj).forEach(([key, value]) => { console.log(key, value); if (nullishOrEmpty(value)) { throw new Error(`${typeName} ${key} not specified`); } }); + + return true; } diff --git a/packages/apps/graph/src/utils/withRetry.ts b/packages/apps/graph/src/utils/withRetry.ts new file mode 100644 index 0000000000..f63bb16ccb --- /dev/null +++ b/packages/apps/graph/src/utils/withRetry.ts @@ -0,0 +1,24 @@ +import { dotenv } from './dotenv'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function withRetry( + func: (...args: T) => Promise, + retries: number = dotenv.CHAINWEB_NODE_RETRY_ATTEMPTS, + delay: number = dotenv.CHAINWEB_NODE_RETRY_DELAY, +): (...funcArgs: T) => Promise { + return async (...funcArgs: T) => { + for (let i = 1; i <= retries; i++) { + try { + return await func(...funcArgs); + } catch (error) { + if (i < retries) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + // This will never be reached, but TypeScript doesn't know that + return await func(...funcArgs); + }; +} diff --git a/packages/e2e/e2e-graph/queries/getAccount.ts b/packages/e2e/e2e-graph/queries/getAccount.ts index 6bbefab91e..4d5554e7cf 100644 --- a/packages/e2e/e2e-graph/queries/getAccount.ts +++ b/packages/e2e/e2e-graph/queries/getAccount.ts @@ -6,8 +6,10 @@ export function getAccountQuery(accountName: string) { chainAccounts { ...CoreChainAccountFields guard { - keys - predicate + ... on Keyset { + keys + predicate + } } } transactions {