diff --git a/docs/pages/misc-pages/upgrade-guide.md b/docs/pages/misc-pages/upgrade-guide.md index ddcfe5866..bd62edf67 100644 --- a/docs/pages/misc-pages/upgrade-guide.md +++ b/docs/pages/misc-pages/upgrade-guide.md @@ -1,3 +1,79 @@ +## web SDK version 11 + +This version of the web SDK focuses on forward compatibility, i.e. making it easier to keep applications and services +running through updates to the integration point (Concordium nodes). + +### Motivation + +Up until this release, certain extensions to the Concordium Node API have resulted in the SDK failing to parse query +responses, resulting in the entire query failing. This is the case even for the larger queries where the unknown/missing +information is only a small part of the response preventing the application to access the entire response. +Usually the fix was to update the SDK to a newer version which know how to parse the new information, but this imposes +work for the ecosystem for every change in the API (usually part of protocol version updates). + +This major release introduces `Upward` a type wrapper representing information which might be extended in future version +of the API, providing a variant representing unknown information such that queries can provide partial information. +It makes potentially extended information explicit in the types and allows each application to decide how to handle the +case of new unknown data on a case by case basis. + +### Handling `Upward` + +Handling upward depends on what you want to achieve. If you're running a service that integrates with a Concordium node, +you might want to fail safely by handling unknown variants gracefully. If you're writing a script it's probably easier +to fail fast. + +#### Fail safe + +```ts +import { isKnown, type TransactionHash } from '@concordium/web-sdk'; +... + +const transactionSummary: Upward = ... + +// branch on unknown +if (isKnown(transactionSummary)) { + // transactionSummary is statically known at this point +} else { + // handle the unknown case + console.warn('Encountered unknown transaction type.'); +} + +// Handle as optional +// `Unknown` variant is simply represented as `null` underneath. +const transactionHash: TransactionHash.Type | undefined = transactionSummary?.hash; +``` + +#### Fail fast + +```ts +import { assertError, type TransactionHash } from '@concordium/web-sdk'; +... + +const transactionSummary: Upward = ... + +// fail with an error message in case of the unexpected happening. +assertKnown(transctionSummary, 'Expected transaction type to be known'); +// transactionSummary is known at this point on. + +// or handle by simple null assertion, e.g. in the case where you just sent +// the transaction to a node. +handleKnownTransaction(transactionSummary!); +``` + +#### Optional + Unknown values + +In the SDK, `undefined` is used to represent when an optional value is _not_ present and `null` is used to represent +unknown values. If you want to branch on these different cases, it can be handled by targeting thesee specific types: + +```ts +const optionalUnknown: Upward | undefined = ... +switch (optionalUnknown) { + case undefined: ... // handle optionality + case null: ... // handle unknown variant + default: ... // at this point, we know the type is `Value` +} +``` + ## web SDK version 7 Several types have been replaced with a module containing the type itself together with functions for constructing and diff --git a/examples/nodejs/client/getBlockItemStatus.ts b/examples/nodejs/client/getBlockItemStatus.ts index 454a744e5..fbf6d8454 100644 --- a/examples/nodejs/client/getBlockItemStatus.ts +++ b/examples/nodejs/client/getBlockItemStatus.ts @@ -1,4 +1,4 @@ -import { BlockItemStatus, CcdAmount, TransactionHash } from '@concordium/web-sdk'; +import { BlockItemStatus, CcdAmount, TransactionHash, knownOrError } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -77,8 +77,7 @@ const client = new ConcordiumGRPCNodeClient( if (blockItemStatus.status === 'finalized') { console.log('blockItemStatus is "finalized" and therefore there is exactly one outcome \n'); - const { summary } = blockItemStatus.outcome; - + const summary = knownOrError(blockItemStatus.outcome.summary, 'unknown outcome encountered'); if (summary.type === 'accountTransaction') { console.log('The block item is an account transaction'); @@ -95,7 +94,7 @@ const client = new ConcordiumGRPCNodeClient( const { failedTransactionType, rejectReason } = summary; console.log( 'Transaction of type "' + failedTransactionType + '" failed because:', - rejectReason.tag + rejectReason?.tag ?? 'unknown' ); break; default: diff --git a/examples/nodejs/client/getBlockSpecialEvents.ts b/examples/nodejs/client/getBlockSpecialEvents.ts index 22337a246..771a7361f 100644 --- a/examples/nodejs/client/getBlockSpecialEvents.ts +++ b/examples/nodejs/client/getBlockSpecialEvents.ts @@ -1,4 +1,4 @@ -import { BlockHash, BlockSpecialEvent } from '@concordium/web-sdk'; +import { BlockHash, BlockSpecialEvent, Upward, isKnown } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -50,10 +50,14 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c (async () => { // #region documentation-snippet const blockHash = cli.flags.block === undefined ? undefined : BlockHash.fromHexString(cli.flags.block); - const events: AsyncIterable = client.getBlockSpecialEvents(blockHash); + const events: AsyncIterable> = client.getBlockSpecialEvents(blockHash); // #endregion documentation-snippet for await (const event of events) { - console.dir(event, { depth: null, colors: true }); + if (!isKnown(event)) { + console.warn('Unknown event encountered'); + } else { + console.dir(event, { depth: null, colors: true }); + } } })(); diff --git a/examples/nodejs/client/getBlockTransactionEvents.ts b/examples/nodejs/client/getBlockTransactionEvents.ts index d9764b958..201dfcfc9 100644 --- a/examples/nodejs/client/getBlockTransactionEvents.ts +++ b/examples/nodejs/client/getBlockTransactionEvents.ts @@ -1,4 +1,4 @@ -import { BlockHash, BlockItemSummary } from '@concordium/web-sdk'; +import { BlockHash, BlockItemSummary, Upward, isKnown } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -48,10 +48,14 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c (async () => { // #region documentation-snippet const blockHash = cli.flags.block === undefined ? undefined : BlockHash.fromHexString(cli.flags.block); - const events: AsyncIterable = client.getBlockTransactionEvents(blockHash); + const events: AsyncIterable> = client.getBlockTransactionEvents(blockHash); // #endregion documentation-snippet for await (const event of events) { - console.dir(event, { depth: null, colors: true }); + if (isKnown(event)) { + console.dir(event, { depth: null, colors: true }); + } else { + console.warn('Encountered unknown event'); + } } })(); diff --git a/examples/nodejs/client/getTokenomicsInfo.ts b/examples/nodejs/client/getTokenomicsInfo.ts index 0a426b033..32b96dfc1 100644 --- a/examples/nodejs/client/getTokenomicsInfo.ts +++ b/examples/nodejs/client/getTokenomicsInfo.ts @@ -1,4 +1,4 @@ -import { BlockHash } from '@concordium/web-sdk'; +import { BlockHash, isKnown } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -55,6 +55,11 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c // Protocol version 4 expanded the amount of information in the response, so one should check the type to access that. // This information includes information about the payday and total amount of funds staked. + if (!isKnown(tokenomics)) { + console.warn('Unknown tokenomics version found'); + return; + } + if (tokenomics.version === 1) { console.log('Next payday time:', tokenomics.nextPaydayTime); console.log('Total staked amount by bakers and delegators', tokenomics.totalStakedCapital); diff --git a/examples/nodejs/client/invokeContract.ts b/examples/nodejs/client/invokeContract.ts index 572d9e950..cc2be3191 100644 --- a/examples/nodejs/client/invokeContract.ts +++ b/examples/nodejs/client/invokeContract.ts @@ -9,6 +9,7 @@ import { Parameter, ReceiveName, ReturnValue, + Upward, } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; @@ -124,7 +125,7 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c console.log('The return value of the invoked method:', ReturnValue.toHexString(returnValue)); } - const events: ContractTraceEvent[] = result.events; + const events: Upward[] = result.events; console.log('A list of effects that the update would have:'); console.dir(events, { depth: null, colors: true }); } diff --git a/examples/nodejs/composed-examples/findAccountCreationBlock.ts b/examples/nodejs/composed-examples/findAccountCreationBlock.ts index 66e97971a..9e5ce3a7b 100644 --- a/examples/nodejs/composed-examples/findAccountCreationBlock.ts +++ b/examples/nodejs/composed-examples/findAccountCreationBlock.ts @@ -1,4 +1,4 @@ -import { AccountAddress, isRpcError } from '@concordium/web-sdk'; +import { AccountAddress, isKnown, isRpcError } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -75,6 +75,9 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c // If account is not a genesis account print account creation transaction hash for await (const summary of summaries) { + if (!isKnown(summary)) { + continue; + } if (summary.type === 'accountCreation' && AccountAddress.equals(summary.address, account)) { console.log('Hash of transaction that created the account:', summary.hash); } diff --git a/examples/nodejs/composed-examples/initAndUpdateContract.ts b/examples/nodejs/composed-examples/initAndUpdateContract.ts index 45e343670..caa530572 100644 --- a/examples/nodejs/composed-examples/initAndUpdateContract.ts +++ b/examples/nodejs/composed-examples/initAndUpdateContract.ts @@ -17,6 +17,8 @@ import { affectedContracts, buildAccountSigner, deserializeReceiveReturnValue, + isKnown, + knownOrError, parseWallet, serializeInitContractParameters, serializeUpdateContractParameters, @@ -118,7 +120,14 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c const initStatus = await client.waitForTransactionFinalization(initTrxHash); console.dir(initStatus, { depth: null, colors: true }); - const contractAddress = affectedContracts(initStatus.summary)[0]; + if (!isKnown(initStatus.summary)) { + throw new Error('Unexpected transaction outcome'); + } + + const contractAddress = knownOrError( + affectedContracts(initStatus.summary)[0], + 'Expected contract init event to be known' + ); // #endregion documentation-snippet-init-contract diff --git a/examples/nodejs/composed-examples/listInitialAccounts.ts b/examples/nodejs/composed-examples/listInitialAccounts.ts index 8ec2bf188..f278cca01 100644 --- a/examples/nodejs/composed-examples/listInitialAccounts.ts +++ b/examples/nodejs/composed-examples/listInitialAccounts.ts @@ -1,4 +1,4 @@ -import { unwrap } from '@concordium/web-sdk'; +import { isKnown, unwrap } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -71,6 +71,9 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c // Get transactions for block const trxStream = client.getBlockTransactionEvents(block.hash); for await (const trx of trxStream) { + if (!isKnown(trx)) { + continue; + } if (trx.type === 'accountCreation' && trx.credentialType === 'initial') { initAccounts.push(trx.address); } diff --git a/examples/nodejs/composed-examples/listNumberAccountTransactions.ts b/examples/nodejs/composed-examples/listNumberAccountTransactions.ts index 79bbebae7..e741f094b 100644 --- a/examples/nodejs/composed-examples/listNumberAccountTransactions.ts +++ b/examples/nodejs/composed-examples/listNumberAccountTransactions.ts @@ -1,4 +1,4 @@ -import { AccountAddress, isTransferLikeSummary, unwrap } from '@concordium/web-sdk'; +import { AccountAddress, isKnown, isTransferLikeSummary, unwrap } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -75,6 +75,9 @@ const client = new ConcordiumGRPCNodeClient(address, Number(port), credentials.c // For each transaction in the block: trxLoop: for await (const trx of trxStream) { + if (!isKnown(trx)) { + continue; + } if (isTransferLikeSummary(trx)) { const trxAcc = trx.sender; diff --git a/examples/nodejs/plt/modify-list.ts b/examples/nodejs/plt/modify-list.ts index 070b07ac8..b6dbcf30d 100644 --- a/examples/nodejs/plt/modify-list.ts +++ b/examples/nodejs/plt/modify-list.ts @@ -5,18 +5,20 @@ import { TransactionEventTag, TransactionKindString, TransactionSummaryType, + isKnown, serializeAccountTransactionPayload, } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { Cbor, + CborAccountAddress, Token, - TokenHolder, TokenId, TokenListUpdate, TokenOperation, TokenOperationType, createTokenUpdatePayload, + parseTokenModuleEvent, } from '@concordium/web-sdk/plt'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -105,7 +107,7 @@ const client = new ConcordiumGRPCNodeClient( // parse the arguments const tokenId = TokenId.fromString(id); - const targetAddress = TokenHolder.fromAccountAddress(AccountAddress.fromBase58(address)); + const targetAddress = AccountAddress.fromBase58(address); if (walletFile !== undefined) { // Read wallet-file @@ -137,22 +139,26 @@ const client = new ConcordiumGRPCNodeClient( const result = await client.waitForTransactionFinalization(transaction); console.log('Transaction finalized:', result); + if (!isKnown(result.summary)) { + throw new Error('Unexpected transaction outcome'); + } + if (result.summary.type !== TransactionSummaryType.AccountTransaction) { throw new Error('Unexpected transaction type: ' + result.summary.type); } switch (result.summary.transactionType) { case TransactionKindString.TokenUpdate: - result.summary.events.forEach((e) => { + result.summary.events.filter(isKnown).forEach((e) => { if (e.tag !== TransactionEventTag.TokenModuleEvent) { throw new Error('Unexpected event type: ' + e.tag); } - console.log('Token module event:', e, Cbor.decode(e.details, 'TokenListUpdateEventDetails')); + console.log('Token module event:', parseTokenModuleEvent(e)); }); break; case TransactionKindString.Failed: - if (result.summary.rejectReason.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { - throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason.tag); + if (result.summary.rejectReason?.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { + throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason?.tag); } const details = Cbor.decode(result.summary.rejectReason.contents.details); console.error(result.summary.rejectReason.contents, details); @@ -167,7 +173,7 @@ const client = new ConcordiumGRPCNodeClient( const operationType = `${action}-${list}-list` as TokenOperationType; // Or from a wallet perspective: // Create list payload. The payload is the same for both add and remove operations on all lists. - const listPayload: TokenListUpdate = { target: targetAddress }; + const listPayload: TokenListUpdate = { target: CborAccountAddress.fromAccountAddress(targetAddress) }; const listOperation = { [operationType]: listPayload, } as TokenOperation; // Normally the cast is not necessary unless done in the same dynamic way as here. diff --git a/examples/nodejs/plt/pause.ts b/examples/nodejs/plt/pause.ts index b82d56980..489d5a3bc 100644 --- a/examples/nodejs/plt/pause.ts +++ b/examples/nodejs/plt/pause.ts @@ -4,10 +4,18 @@ import { TransactionEventTag, TransactionKindString, TransactionSummaryType, + isKnown, serializeAccountTransactionPayload, } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; -import { Cbor, Token, TokenId, TokenOperation, createTokenUpdatePayload } from '@concordium/web-sdk/plt'; +import { + Cbor, + Token, + TokenId, + TokenOperation, + createTokenUpdatePayload, + parseTokenModuleEvent, +} from '@concordium/web-sdk/plt'; import { credentials } from '@grpc/grpc-js'; import meow from 'meow'; @@ -102,22 +110,26 @@ const client = new ConcordiumGRPCNodeClient( const result = await client.waitForTransactionFinalization(transaction); console.log('Transaction finalized:', result); + if (!isKnown(result.summary)) { + throw new Error('Unexpected transaction outcome'); + } + if (result.summary.type !== TransactionSummaryType.AccountTransaction) { throw new Error('Unexpected transaction type: ' + result.summary.type); } switch (result.summary.transactionType) { case TransactionKindString.TokenUpdate: - result.summary.events.forEach((e) => { + result.summary.events.filter(isKnown).forEach((e) => { if (e.tag !== TransactionEventTag.TokenModuleEvent) { throw new Error('Unexpected event type: ' + e.tag); } - console.log('Token module event:', e, Cbor.decode(e.details, 'TokenPauseEventDetails')); + console.log('Token module event:', parseTokenModuleEvent(e)); }); break; case TransactionKindString.Failed: - if (result.summary.rejectReason.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { - throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason.tag); + if (result.summary.rejectReason?.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { + throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason?.tag); } const details = Cbor.decode(result.summary.rejectReason.contents.details); console.error(result.summary.rejectReason.contents, details); diff --git a/examples/nodejs/plt/transfer.ts b/examples/nodejs/plt/transfer.ts index 36e563794..d98a29825 100644 --- a/examples/nodejs/plt/transfer.ts +++ b/examples/nodejs/plt/transfer.ts @@ -4,15 +4,16 @@ import { RejectReasonTag, TransactionKindString, TransactionSummaryType, + isKnown, serializeAccountTransactionPayload, } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; import { Cbor, + CborAccountAddress, CborMemo, Token, TokenAmount, - TokenHolder, TokenId, TokenTransfer, TokenTransferOperation, @@ -94,10 +95,10 @@ const client = new ConcordiumGRPCNodeClient( const tokenId = TokenId.fromString(cli.flags.tokenId); const token = await Token.fromId(client, tokenId); const amount = TokenAmount.fromDecimal(cli.flags.amount, token.info.state.decimals); - const recipient = TokenHolder.fromAccountAddress(AccountAddress.fromBase58(cli.flags.recipient)); + const recipient = AccountAddress.fromBase58(cli.flags.recipient); const memo = cli.flags.memo ? CborMemo.fromString(cli.flags.memo) : undefined; - const transfer: TokenTransfer = { + const transfer: Token.TransferInput = { recipient, amount, memo, @@ -116,6 +117,9 @@ const client = new ConcordiumGRPCNodeClient( const result = await client.waitForTransactionFinalization(transaction); console.log('Transaction finalized:', result); + if (!isKnown(result.summary)) { + throw new Error('Unexpected transaction outcome'); + } if (result.summary.type !== TransactionSummaryType.AccountTransaction) { throw new Error('Unexpected transaction type: ' + result.summary.type); } @@ -125,8 +129,8 @@ const client = new ConcordiumGRPCNodeClient( result.summary.events.forEach((e) => console.log(e)); break; case TransactionKindString.Failed: - if (result.summary.rejectReason.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { - throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason.tag); + if (result.summary.rejectReason?.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { + throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason?.tag); } const details = Cbor.decode(result.summary.rejectReason.contents.details); console.error(result.summary.rejectReason.contents, details); @@ -140,6 +144,11 @@ const client = new ConcordiumGRPCNodeClient( } else { // Or from a wallet perspective: // Create transfer payload + const transfer: TokenTransfer = { + recipient: CborAccountAddress.fromAccountAddress(recipient), + amount, + memo, + }; const transferOperation: TokenTransferOperation = { transfer, }; diff --git a/examples/nodejs/plt/update-supply.ts b/examples/nodejs/plt/update-supply.ts index db90ea608..4dad692e3 100644 --- a/examples/nodejs/plt/update-supply.ts +++ b/examples/nodejs/plt/update-supply.ts @@ -3,6 +3,7 @@ import { RejectReasonTag, TransactionKindString, TransactionSummaryType, + isKnown, serializeAccountTransactionPayload, } from '@concordium/web-sdk'; import { ConcordiumGRPCNodeClient } from '@concordium/web-sdk/nodejs'; @@ -110,6 +111,9 @@ const client = new ConcordiumGRPCNodeClient(addr, Number(port), credentials.crea const result = await client.waitForTransactionFinalization(transaction); console.log('Transaction finalized:', result); + if (!isKnown(result.summary)) { + throw new Error('Unexpected transaction outcome'); + } if (result.summary.type !== TransactionSummaryType.AccountTransaction) { throw new Error('Unexpected transaction type: ' + result.summary.type); } @@ -119,8 +123,8 @@ const client = new ConcordiumGRPCNodeClient(addr, Number(port), credentials.crea result.summary.events.forEach((e) => console.log(e)); break; case TransactionKindString.Failed: - if (result.summary.rejectReason.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { - throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason.tag); + if (result.summary.rejectReason?.tag !== RejectReasonTag.TokenUpdateTransactionFailed) { + throw new Error('Unexpected reject reason tag: ' + result.summary.rejectReason?.tag); } const details = Cbor.decode(result.summary.rejectReason.contents.details); console.error(result.summary.rejectReason.contents, details); diff --git a/examples/wallet/src/CreateAccount.tsx b/examples/wallet/src/CreateAccount.tsx index 11694adb4..30201b4cc 100644 --- a/examples/wallet/src/CreateAccount.tsx +++ b/examples/wallet/src/CreateAccount.tsx @@ -1,5 +1,5 @@ import { - CredentialDeploymentTransaction, + CredentialDeploymentPayload, CredentialInputNoSeed, IdentityObjectV1, getAccountAddress, @@ -43,7 +43,7 @@ export function CreateAccount({ identity }: { identity: IdentityObjectV1 }) { return; } - const listener = (worker.onmessage = async (e: MessageEvent) => { + const listener = (worker.onmessage = async (e: MessageEvent) => { worker.removeEventListener('message', listener); const credentialDeploymentTransaction = e.data; const signingKey = getAccountSigningKey(seedPhrase, credentialDeploymentTransaction.unsignedCdi.ipIdentity); diff --git a/examples/wallet/src/account-worker.ts b/examples/wallet/src/account-worker.ts index c0efda708..47dddc3ca 100644 --- a/examples/wallet/src/account-worker.ts +++ b/examples/wallet/src/account-worker.ts @@ -1,8 +1,8 @@ -import { createCredentialTransactionNoSeed } from '@concordium/web-sdk'; +import { createCredentialPayloadNoSeed } from '@concordium/web-sdk'; import { AccountWorkerInput } from './types'; self.onmessage = (e: MessageEvent) => { - const credentialTransaction = createCredentialTransactionNoSeed(e.data.credentialInput, e.data.expiry); + const credentialTransaction = createCredentialPayloadNoSeed(e.data.credentialInput, e.data.expiry); self.postMessage(credentialTransaction); }; diff --git a/examples/wallet/src/util.ts b/examples/wallet/src/util.ts index dce8f3d40..a9a8d05a5 100644 --- a/examples/wallet/src/util.ts +++ b/examples/wallet/src/util.ts @@ -1,6 +1,7 @@ import { AccountAddress, AccountInfo, + AccountInfoType, AccountTransaction, AccountTransactionHeader, AccountTransactionType, @@ -8,7 +9,7 @@ import { CcdAmount, ConcordiumGRPCWebClient, ConcordiumHdWallet, - CredentialDeploymentTransaction, + CredentialDeploymentPayload, CryptographicParameters, IdObjectRequestV1, IdRecoveryRequest, @@ -233,7 +234,7 @@ export function createCredentialDeploymentKeysAndRandomness( * @returns a promise with the transaction hash of the submitted credential deployment */ export async function sendCredentialDeploymentTransaction( - credentialDeployment: CredentialDeploymentTransaction, + credentialDeployment: CredentialDeploymentPayload, signature: string ) { const payload = serializeCredentialDeploymentPayload([signature], credentialDeployment); @@ -314,11 +315,15 @@ export async function getAccount(accountAddress: AccountAddress.Type): Promise { try { const accountInfo = await client.getAccountInfo(accountAddress); + if (accountInfo.type === AccountInfoType.Unknown) { + reject(new Error('Account info unknown')); + return false; + } resolve(accountInfo); return false; } catch { if (escapeCounter > maxRetries) { - reject(); + reject(new Error('Max retry counter reached')); return false; } else { escapeCounter += 1; diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index a22856220..e881b05d6 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 11.0.0 + ### Fixed - Fix decoding of `TokenAmount`s with 256 decimals. @@ -9,8 +11,62 @@ - Decoding a `TokenHolderAccount` distinguishes whether the account includes coin info. ### Added + - `fromAccountAddressNoCoinInfo` to `TokenHolderAccount`. - `coinInfo` is a public field of `TokenHolderAccount`. +- `Upward` as a means of representing possibly unknown variants of a type encounted when querying the GRPC API + of *future* Concordium node versions. This is a type alias of `T | null`, i.e. unknown variants will be represented + as `null`. +- `decodeTokenOperation`, which decodes `Cbor.Type` to `TokenOperation | UnknownTokenOperation`. +- `parseTokenUpdatePayload`, which decodes the CBOR encoded operations and returns a corresponding payload with the + operations decoded into `(TokenOperation | UnknownTokenOperation)[]` +- `parseTokenModuleRejectReason`, which decodes `Cbor.Type` into `TokenModuleRejectReason | UnknownTokenModuleRejectReason`. +- `CborContractAddress` which represents CIS-7 compatible contract addresses. + +### Breaking changes + +- Renamed `TokenModuleRejectReason` to `EncodedTokenModuleRejectReason`, aligning with the corresponding types for + `TokenModuleEvent`. `TokenModuleRejectReason` now describes the decoded version of `EncodedTokenModuleRejectReason`. +- `parseTokenModuleEvent` (previously `parseModuleEvent`) now returns `TokenModuleEvent | UnknownTokenModuleEvent` +- Rename `CredentialDeploymentTransaction` to `CredentialDeploymentPayload`, and correspondingly + - `createCredentialDeploymentTransaction` -> `createCredentialDeploymentPayload` + - `createCredentialTransaction` -> `createCredentialPayload` + - `createCredentialTransactionNoSeed` -> `createCredentialPayloadNoSeed` +- `CborAccountAddress` is now used instead of `TokenHolder` for CBOR encoded account addresses in PLT/CIS-7. +- `Token` functions now take `AccountAddress` anywhere `TokenHolder` was previously used. + +#### GRPC API query response types + +- `BlockItemSummaryInBlock.summary` now has the type `Upward`. +- `ConfigureBakerSummary`, `ConfigureDelegationSummary`, `TokenCreationSummary`, and `TokenUpdateSummary` events + have been wrapped in `Upward`. +- `UpdateSummary.payload` now has the type `Upward`. +- `UpdateEnqueuedEvent.payload` now has the type `Upward`. +- `PendingUpdate.effect` now has the type `Upward`. +- `PassiveCommitteeInfo` now has been wrapped in `Upward`. +- `NodeInfoConsensusStatus` and `NodeCatchupStatus` now have been wrapped in `Upward`. +- `RejectReason` now has been wrapped in `Upward` +- `RewardStatus` now has been wrapped in `Upward` +- `Cooldown.status` now has the type `Upward`. This affects all `AccountInfo` variants. +- `BakerPoolInfo.openStatus` now has the type `Upward`. + - Affects the `AccountInfoBaker` variant of `AccountInfo`. + - Affects `BakerPoolStatus`. +- `BakerSetOpenStatusEvent.openStatus` now has the type `Upward`. +- `AccountInfo` has been extended with a new variant `AccountInfoUnknown`. +- `ContractTraceEvent` uses in reponse types from the GRPC API have now been wrapped in `Upward`. + - Affects `InvokeContractResultSuccess` + - Affects `UpdateContractSummary` +- `ContractVersion` enum has been removed and replaced with `number` where it was used. +- `VerifyKey` uses has now been wrapped in `Upward`, affecting the types + - `CredentialPublicKeys`, bleeding into top-level types `CredentialDeploymentInfo`, `InitialAccountCredential` and + `NormalAccountCredential` + +#### `ConcordiumGRPCClient`: + +- `waitForTransactionFinalization` is affected by the changes to `BlockItemSummaryInBlock` +- `getBlockTransactionEvents` now returns `AsyncIterable>`. +- `getBlockSpecialEvents` now returns `AsyncIterable>`. +- `getPoolInfo` is affected by the changes to `BakerPoolInfo` ## 10.0.2 diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 47c446c34..1e5379769 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@concordium/web-sdk", - "version": "10.0.2", + "version": "11.0.0", "license": "Apache-2.0", "engines": { "node": ">=16" @@ -99,6 +99,7 @@ "build": "yarn clean && yarn mkdirp src/grpc-api && yarn generate && yarn build-dev", "build-dev": "tsc -p tsconfig.build.json && yarn webpack", "webpack": "tsx ../../node_modules/webpack-cli/bin/cli.js --config webpack.config.ts", + "typecheck": "tsc -p tsconfig.json", "clean": "rimraf -- lib src/grpc-api", "size": "yarn build && size-limit", "size:no-build": "size-limit" diff --git a/packages/sdk/src/accountTransactions.ts b/packages/sdk/src/accountTransactions.ts index aa6c8b3b3..fb5d33b96 100644 --- a/packages/sdk/src/accountTransactions.ts +++ b/packages/sdk/src/accountTransactions.ts @@ -1,8 +1,7 @@ import { Buffer } from 'buffer/index.js'; import { Cursor } from './deserializationHelpers.js'; -import { Cbor, TokenId } from './plt/index.js'; -import { TokenOperation, TokenOperationType } from './plt/module.js'; +import { Cbor, TokenId, TokenOperationType } from './plt/index.js'; import { ContractAddress, ContractName, Energy, ModuleReference } from './pub/types.js'; import { serializeCredentialDeploymentInfo } from './serialization.js'; import { @@ -57,6 +56,7 @@ export interface AccountTransactionHandler< * * @param payload - The payload to serialize. * @returns The serialized payload. + * @throws If serializing the type was not possible. */ serialize: (payload: PayloadType) => Buffer; @@ -64,6 +64,7 @@ export interface AccountTransactionHandler< * Deserializes the serialized payload into the payload type. * @param serializedPayload - The serialized payload to be deserialized. * @returns The deserialized payload. + * @throws If deserializing the type was not possible. */ deserialize: (serializedPayload: Cursor) => PayloadType; @@ -290,7 +291,7 @@ export class UpdateContractHandler const serializeIndex = encodeWord64(payload.address.index); const serializeSubindex = encodeWord64(payload.address.subindex); const serializedContractAddress = Buffer.concat([serializeIndex, serializeSubindex]); - const receiveNameBuffer = Buffer.from(ReceiveName.toString(payload.receiveName), 'utf8'); + const receiveNameBuffer = Buffer.from(payload.receiveName.toString(), 'utf8'); const serializedReceiveName = packBufferWithWord16Length(receiveNameBuffer); const parameterBuffer = Parameter.toBuffer(payload.message); const serializedParameters = packBufferWithWord16Length(parameterBuffer); @@ -539,7 +540,7 @@ export class TokenUpdateHandler implements AccountTransactionHandler { + async getTokenomicsInfo(blockHash?: BlockHash.Type): Promise> { const blockHashInput = getBlockHashInput(blockHash); const response = await this.client.getTokenomicsInfo(blockHashInput).response; @@ -946,8 +947,14 @@ export class ConcordiumGRPCClient { * @param blockHash an optional block hash to get the transaction events at, otherwise retrieves from last finalized block. * @param abortSignal an optional AbortSignal to close the stream. * @returns a stream of block item summaries + * + * **Please note**, any of these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. */ - getBlockTransactionEvents(blockHash?: BlockHash.Type, abortSignal?: AbortSignal): AsyncIterable { + getBlockTransactionEvents( + blockHash?: BlockHash.Type, + abortSignal?: AbortSignal + ): AsyncIterable> { const blockItemSummaries = this.client.getBlockTransactionEvents(getBlockHashInput(blockHash), { abort: abortSignal, }).responses; @@ -1140,8 +1147,14 @@ export class ConcordiumGRPCClient { * @param blockHash an optional block hash to get the special events at, otherwise retrieves from last finalized block. * @param abortSignal an optional AbortSignal to close the stream. * @returns a stream of block item summaries + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. */ - getBlockSpecialEvents(blockHash?: BlockHash.Type, abortSignal?: AbortSignal): AsyncIterable { + getBlockSpecialEvents( + blockHash?: BlockHash.Type, + abortSignal?: AbortSignal + ): AsyncIterable> { const blockSpecialEvents = this.client.getBlockSpecialEvents(getBlockHashInput(blockHash), { abort: abortSignal, }).responses; diff --git a/packages/sdk/src/grpc/index.ts b/packages/sdk/src/grpc/index.ts index f4788b031..e2d30ef0e 100644 --- a/packages/sdk/src/grpc/index.ts +++ b/packages/sdk/src/grpc/index.ts @@ -4,3 +4,4 @@ export { getAccountIdentifierInput, getBlockHashInput, } from './GRPCClient.js'; +export * from './upward.js'; diff --git a/packages/sdk/src/grpc/translation.ts b/packages/sdk/src/grpc/translation.ts index 8a73abdd4..c052f9724 100644 --- a/packages/sdk/src/grpc/translation.ts +++ b/packages/sdk/src/grpc/translation.ts @@ -23,6 +23,7 @@ import * as SequenceNumber from '../types/SequenceNumber.js'; import * as Timestamp from '../types/Timestamp.js'; import * as TransactionHash from '../types/TransactionHash.js'; import { mapRecord, unwrap } from '../util.js'; +import type { Upward } from './upward.js'; function unwrapToHex(bytes: Uint8Array | undefined): SDK.HexString { return Buffer.from(unwrap(bytes)).toString('hex'); @@ -69,14 +70,15 @@ function trCommits(cmm: GRPC.CredentialCommitments): SDK.CredentialDeploymentCom }; } -function trVerifyKey(verifyKey: GRPC.AccountVerifyKey): SDK.VerifyKey { - if (verifyKey.key.oneofKind === 'ed25519Key') { - return { - schemeId: 'Ed25519', - verifyKey: unwrapToHex(verifyKey.key.ed25519Key), - }; - } else { - throw Error('AccountVerifyKey was expected to be of type "ed25519Key", but found' + verifyKey.key.oneofKind); +function trVerifyKey(verifyKey: GRPC.AccountVerifyKey): Upward { + switch (verifyKey.key.oneofKind) { + case 'ed25519Key': + return { + schemeId: 'Ed25519', + verifyKey: unwrapToHex(verifyKey.key.ed25519Key), + }; + case undefined: + return null; } } @@ -206,14 +208,16 @@ function trAmountFraction(amount: GRPC.AmountFraction | undefined): number { return unwrap(amount?.partsPerHundredThousand) / 100000; } -function trOpenStatus(openStatus: GRPC.OpenStatus | undefined): SDK.OpenStatusText { - switch (unwrap(openStatus)) { +function trOpenStatus(openStatus: GRPC.OpenStatus | undefined): Upward { + switch (openStatus) { case GRPC.OpenStatus.OPEN_FOR_ALL: return SDK.OpenStatusText.OpenForAll; case GRPC.OpenStatus.CLOSED_FOR_NEW: return SDK.OpenStatusText.ClosedForNew; case GRPC.OpenStatus.CLOSED_FOR_ALL: return SDK.OpenStatusText.ClosedForAll; + case undefined: + return null; } } @@ -240,15 +244,10 @@ function trBaker(baker: GRPC.AccountStakingInfo_Baker): SDK.AccountBakerDetails return v0; } - const bakerPoolInfo: SDK.BakerPoolInfo = { - openStatus: trOpenStatus(baker.poolInfo?.openStatus), - metadataUrl: unwrap(baker.poolInfo?.url), - commissionRates: trCommissionRates(baker.poolInfo?.commissionRates), - }; return { ...v0, version: 1, - bakerPoolInfo: bakerPoolInfo, + bakerPoolInfo: transPoolInfo(baker.poolInfo), }; } @@ -418,21 +417,26 @@ export function accountInfo(acc: GRPC.AccountInfo): SDK.AccountInfo { accountTokens: acc.tokens.map(trTokenAccountInfo), }; - if (acc.stake?.stakingInfo.oneofKind === 'delegator') { - return { - ...accInfoCommon, - type: SDK.AccountInfoType.Delegator, - accountDelegation: trDelegator(acc.stake.stakingInfo.delegator), - }; - } else if (acc.stake?.stakingInfo.oneofKind === 'baker') { - return { - ...accInfoCommon, - type: SDK.AccountInfoType.Baker, - accountBaker: trBaker(acc.stake.stakingInfo.baker), - }; - } else { + if (acc.stake === undefined) { return accInfoCommon; } + + switch (acc.stake.stakingInfo.oneofKind) { + case 'delegator': + return { + ...accInfoCommon, + type: SDK.AccountInfoType.Delegator, + accountDelegation: trDelegator(acc.stake.stakingInfo.delegator), + }; + case 'baker': + return { + ...accInfoCommon, + type: SDK.AccountInfoType.Baker, + accountBaker: trBaker(acc.stake.stakingInfo.baker), + }; + case undefined: + return { ...accInfoCommon, type: SDK.AccountInfoType.Unknown, accountBaker: null }; + } } export function nextAccountSequenceNumber(nasn: GRPC.NextAccountSequenceNumber): SDK.NextAccountNonce { @@ -632,7 +636,7 @@ function translateProtocolVersion(pv: GRPC.ProtocolVersion): bigint { return BigInt(pv + 1); // Protocol version enum indexes from 0, i.e. pv.PROTOCOL_VERSION_1 = 0. } -export function tokenomicsInfo(info: GRPC.TokenomicsInfo): SDK.RewardStatus { +export function tokenomicsInfo(info: GRPC.TokenomicsInfo): Upward { switch (info.tokenomics.oneofKind) { case 'v0': { const v0 = info.tokenomics.v0; @@ -663,7 +667,7 @@ export function tokenomicsInfo(info: GRPC.TokenomicsInfo): SDK.RewardStatus { }; } case undefined: - throw new Error('Missing tokenomics info'); + return null; } } @@ -759,7 +763,7 @@ function trAddress(address: GRPC.Address): SDK.Address { } } -function trContractTraceElement(contractTraceElement: GRPC.ContractTraceElement): SDK.ContractTraceEvent { +function trContractTraceElement(contractTraceElement: GRPC.ContractTraceElement): Upward { const element = contractTraceElement.element; switch (element.oneofKind) { case 'updated': @@ -799,12 +803,12 @@ function trContractTraceElement(contractTraceElement: GRPC.ContractTraceElement) from: unwrapValToHex(element.upgraded.from), to: unwrapValToHex(element.upgraded.to), }; - default: - throw Error('Invalid ContractTraceElement received, not able to translate to Transaction Event!'); + case undefined: + return null; } } -function trBakerEvent(bakerEvent: GRPC.BakerEvent, account: AccountAddress.Type): SDK.BakerEvent { +function trBakerEvent(bakerEvent: GRPC.BakerEvent, account: AccountAddress.Type): Upward { const event = bakerEvent.event; switch (event.oneofKind) { case 'bakerAdded': { @@ -925,7 +929,7 @@ function trBakerEvent(bakerEvent: GRPC.BakerEvent, account: AccountAddress.Type) }; } case undefined: - throw Error('Unrecognized event type. This should be impossible.'); + return null; } } @@ -945,7 +949,10 @@ function trDelegTarget(delegationTarget: GRPC.DelegationTarget | undefined): SDK } } -function trDelegationEvent(delegationEvent: GRPC.DelegationEvent, account: AccountAddress.Type): SDK.DelegationEvent { +function trDelegationEvent( + delegationEvent: GRPC.DelegationEvent, + account: AccountAddress.Type +): Upward { const event = delegationEvent.event; switch (event.oneofKind) { case 'delegationStakeIncreased': { @@ -1002,11 +1009,11 @@ function trDelegationEvent(delegationEvent: GRPC.DelegationEvent, account: Accou bakerId: unwrap(event.bakerRemoved.bakerId?.value), }; case undefined: - throw Error('Unrecognized event type. This should be impossible.'); + return null; } } -function trRejectReason(rejectReason: GRPC.RejectReason | undefined): SDK.RejectReason { +function trRejectReason(rejectReason: GRPC.RejectReason | undefined): Upward { function simpleReason(tag: SDK.SimpleRejectReasonTag): SDK.RejectReason { return { tag: SDK.RejectReasonTag[tag], @@ -1205,7 +1212,7 @@ function trRejectReason(rejectReason: GRPC.RejectReason | undefined): SDK.Reject }, }; case undefined: - throw Error('Failed translating RejectReason, encountered undefined value'); + return null; } } @@ -1421,7 +1428,7 @@ export function pendingUpdate(pendingUpdate: GRPC.PendingUpdate): SDK.PendingUpd }; } -export function trPendingUpdateEffect(pendingUpdate: GRPC.PendingUpdate): SDK.PendingUpdateEffect { +export function trPendingUpdateEffect(pendingUpdate: GRPC.PendingUpdate): Upward { const effect = pendingUpdate.effect; switch (effect.oneofKind) { case 'protocol': @@ -1506,13 +1513,17 @@ export function trPendingUpdateEffect(pendingUpdate: GRPC.PendingUpdate): SDK.Pe }, }; case undefined: - throw Error('Unexpected missing pending update'); + return null; } } -function trUpdatePayload(updatePayload: GRPC.UpdatePayload | undefined): SDK.UpdateInstructionPayload { - const payload = updatePayload?.payload; - switch (payload?.oneofKind) { +function trUpdatePayload(updatePayload: GRPC.UpdatePayload | undefined): Upward { + if (updatePayload === undefined) { + throw new Error('Unexpected missing update payload'); + } + + const payload = updatePayload.payload; + switch (payload.oneofKind) { case 'protocolUpdate': return trProtocolUpdate(payload.protocolUpdate); case 'electionDifficultyUpdate': @@ -1590,7 +1601,7 @@ function trUpdatePayload(updatePayload: GRPC.UpdatePayload | undefined): SDK.Upd }, }; case undefined: - throw new Error('Unexpected missing update payload'); + return null; } } @@ -1600,9 +1611,8 @@ function trCommissionRange(range: GRPC.InclusiveRangeAmountFraction | undefined) max: trAmountFraction(range?.max), }; } -function trUpdatePublicKey(key: GRPC.UpdatePublicKey): SDK.VerifyKey { +function trUpdatePublicKey(key: GRPC.UpdatePublicKey): SDK.UpdatePublicKey { return { - schemeId: 'Ed25519', verifyKey: unwrapValToHex(key), }; } @@ -2040,7 +2050,7 @@ function trAccountTransactionSummary( } } -function tokenEvent(event: GRPC_PLT.TokenEvent): TokenEvent { +function tokenEvent(event: GRPC_PLT.TokenEvent): Upward { switch (event.event.oneofKind) { case 'transferEvent': const transferEvent: TokenTransferEvent = { @@ -2077,7 +2087,7 @@ function tokenEvent(event: GRPC_PLT.TokenEvent): TokenEvent { target: PLT.TokenHolder.fromProto(unwrap(event.event.burnEvent.target)), }; case undefined: - throw Error('Failed translating "TokenEvent", encountered undefined value'); + return null; } } @@ -2090,7 +2100,7 @@ function trCreatePltPayload(payload: GRPC_PLT.CreatePLT): PLT.CreatePLTPayload { }; } -export function blockItemSummary(summary: GRPC.BlockItemSummary): SDK.BlockItemSummary { +export function blockItemSummary(summary: GRPC.BlockItemSummary): Upward { const base = { index: unwrap(summary.index?.value), energyCost: Energy.fromProto(unwrap(summary.energyCost)), @@ -2124,8 +2134,8 @@ export function blockItemSummary(summary: GRPC.BlockItemSummary): SDK.BlockItemS payload: trCreatePltPayload(unwrap(summary.details.tokenCreation.createPlt)), events: summary.details.tokenCreation.events.map(tokenEvent), }; - default: - throw Error('Invalid BlockItemSummary encountered!'); + case undefined: + return null; } } @@ -2365,7 +2375,7 @@ export function nextUpdateSequenceNumbers(nextNums: GRPC.NextUpdateSequenceNumbe function trPassiveCommitteeInfo( passiveCommitteeInfo: GRPC.NodeInfo_BakerConsensusInfo_PassiveCommitteeInfo -): SDK.PassiveCommitteeInfo { +): Upward { const passiveCommitteeInfoV2 = GRPC.NodeInfo_BakerConsensusInfo_PassiveCommitteeInfo; switch (passiveCommitteeInfo) { case passiveCommitteeInfoV2.NOT_IN_COMMITTEE: @@ -2374,6 +2384,8 @@ function trPassiveCommitteeInfo( return SDK.PassiveCommitteeInfo.AddedButNotActiveInCommittee; case passiveCommitteeInfoV2.ADDED_BUT_WRONG_KEYS: return SDK.PassiveCommitteeInfo.AddedButWrongKeys; + case undefined: + return null; } } @@ -2452,7 +2464,7 @@ export function nodeInfo(nodeInfo: GRPC.NodeInfo): SDK.NodeInfo { }; } -function trCatchupStatus(catchupStatus: GRPC.PeersInfo_Peer_CatchupStatus): SDK.NodeCatchupStatus { +function trCatchupStatus(catchupStatus: GRPC.PeersInfo_Peer_CatchupStatus): Upward { const CatchupStatus = GRPC.PeersInfo_Peer_CatchupStatus; switch (catchupStatus) { case CatchupStatus.CATCHINGUP: @@ -2461,6 +2473,8 @@ function trCatchupStatus(catchupStatus: GRPC.PeersInfo_Peer_CatchupStatus): SDK. return SDK.NodeCatchupStatus.Pending; case CatchupStatus.UPTODATE: return SDK.NodeCatchupStatus.UpToDate; + case undefined: + return null; } } @@ -2504,7 +2518,7 @@ function trAccountAmount( }; } -export function blockSpecialEvent(specialEvent: GRPC.BlockSpecialEvent): SDK.BlockSpecialEvent { +export function blockSpecialEvent(specialEvent: GRPC.BlockSpecialEvent): Upward { const event = specialEvent.event; switch (event.oneofKind) { case 'bakingRewards': { @@ -2595,7 +2609,7 @@ export function blockSpecialEvent(specialEvent: GRPC.BlockSpecialEvent): SDK.Blo }; } case undefined: { - throw Error('Error translating BlockSpecialEvent: unexpected undefined'); + return null; } } } diff --git a/packages/sdk/src/grpc/upward.ts b/packages/sdk/src/grpc/upward.ts new file mode 100644 index 000000000..4341c17ec --- /dev/null +++ b/packages/sdk/src/grpc/upward.ts @@ -0,0 +1,86 @@ +import { bail } from '../util.js'; + +export type Unknown = null; + +/** + * Represents types returned by the GRPC API of a Concordium node which are + * possibly unknown to the SDK version. {@linkcode Unknown} means that the type is unknown. + * + * @template T - The type representing the known variants + * + * @example + * // fail on unknown value + * const upwardValue: Upward = ... + * if (!isKnown(upwardValue)) { + * throw new Error('Uncountered unknown value') + * } + * // the value is known from this point + * + * @example + * // gracefully handle unknown values + * const upwardValue: Upward = ... + * if (!isKnown(upwardValue)) { + * console.warn('Uncountered unknown value') + * } else { + * // the value is known from this point + * } + */ +export type Upward = T | Unknown; + +// Recursively remove all occurrences of `null` (or `Unknown`) from a type. Since `null` is only +// used via the Upward sentinel (and never intentionally in other field types), +// this yields a type appropriate for constructing outbound payloads where all +// values must be known. +export type Known = T extends Unknown + ? never + : T extends Function + ? T + : T extends Map + ? Map, Known> + : T extends Set + ? Set> + : T extends readonly (infer U)[] + ? T extends readonly [any, ...any[]] + ? { [I in keyof T]: Known } + : Known[] + : T extends object + ? { [P in keyof T]: Known } + : T; + +/** + * Type guard that checks whether an Upward holds a known value. + * + * @template T - The type representing the known variants + * @param value - The possibly {@linkcode Unknown} value returned from gRPC. + * @returns True if value is not {@linkcode Unknown} (i.e., is T). + */ +export function isKnown(value: Upward): value is T { + return value !== null; +} + +/** + * Asserts that an Upward is known, otherwise throws the provided error. + * + * Useful when {@linkcode Unknown} values should be treated as hard failures. + * + * @template T - The type representing the known variants + * @param value - The possibly {@linkcode Unknown} value returned from gRPC. + * @param error - Error to throw if value is unknown. + * @returns True as a type predicate when value is known. + */ +export function assertKnown(value: Upward, error: Error | string): value is T { + return isKnown(value) || bail(error); +} + +/** + * Returns the known value or throws the provided error when unknown. + * + * @template T - The type representing the known variants + * @param value - The possibly {@linkcode Unknown} value returned from gRPC. + * @param error - Error to throw if value is unknown. + * @returns The unwrapped known value of type T. + */ +export function knownOrError(value: Upward, error: Error | string): T { + if (!isKnown(value)) throw error instanceof Error ? error : new Error(error); + return value; +} diff --git a/packages/sdk/src/plt/Cbor.ts b/packages/sdk/src/plt/Cbor.ts index 883e8e191..735ae4bb4 100644 --- a/packages/sdk/src/plt/Cbor.ts +++ b/packages/sdk/src/plt/Cbor.ts @@ -2,15 +2,7 @@ import { Buffer } from 'buffer/index.js'; import * as Proto from '../grpc-api/v2/concordium/protocol-level-tokens.js'; import { HexString } from '../types.js'; -import { cborDecode, cborEncode } from '../types/cbor.js'; -import { TokenAmount, TokenHolder, TokenMetadataUrl } from './index.js'; -import { - TokenInitializationParameters, - TokenListUpdateEventDetails, - TokenModuleAccountState, - TokenModuleState, - TokenPauseEventDetails, -} from './module.js'; +import { cborEncode } from '../types/cbor.js'; export type JSON = HexString; @@ -121,158 +113,7 @@ export function toProto(cbor: Cbor): Proto.CBor { }; } -function decodeTokenModuleState(value: Cbor): TokenModuleState { - const decoded = cborDecode(value.bytes); - if (typeof decoded !== 'object' || decoded === null) { - throw new Error('Invalid CBOR data for TokenModuleState'); - } - - // Validate required fields - if (!('governanceAccount' in decoded && TokenHolder.instanceOf(decoded.governanceAccount))) { - throw new Error('Invalid TokenModuleState: missing or invalid governanceAccount'); - } - if (!('metadata' in decoded)) { - throw new Error('Invalid TokenModuleState: missing metadataUrl'); - } - let metadata = TokenMetadataUrl.fromCBORValue(decoded.metadata); - if (!('name' in decoded && typeof decoded.name === 'string')) { - throw new Error('Invalid TokenModuleState: missing or invalid name'); - } - - // Validate optional fields - if ('allowList' in decoded && typeof decoded.allowList !== 'boolean') { - throw new Error('Invalid TokenModuleState: allowList must be a boolean'); - } - if ('denyList' in decoded && typeof decoded.denyList !== 'boolean') { - throw Error('Invalid TokenModuleState: denyList must be a boolean'); - } - if ('mintable' in decoded && typeof decoded.mintable !== 'boolean') { - throw new Error('Invalid TokenModuleState: mintable must be a boolean'); - } - if ('burnable' in decoded && typeof decoded.burnable !== 'boolean') { - throw new Error('Invalid TokenModuleState: burnable must be a boolean'); - } - if ('paused' in decoded && typeof decoded.paused !== 'boolean') { - throw new Error('Invalid TokenModuleState: paused must be a boolean'); - } - - return { ...decoded, metadata } as TokenModuleState; -} - -function decodeTokenModuleAccountState(value: Cbor): TokenModuleAccountState { - const decoded = cborDecode(value.bytes); - if (typeof decoded !== 'object' || decoded === null) { - throw new Error('Invalid CBOR data for TokenModuleAccountState'); - } - - // Validate optional fields - if ('allowList' in decoded && typeof decoded.allowList !== 'boolean') { - throw new Error('Invalid TokenModuleState: allowList must be a boolean'); - } - if ('denyList' in decoded && typeof decoded.denyList !== 'boolean') { - throw Error('Invalid TokenModuleState: denyList must be a boolean'); - } - - return decoded as TokenModuleAccountState; -} - -function decodeTokenListUpdateEventDetails(value: Cbor): TokenListUpdateEventDetails { - const decoded = cborDecode(value.bytes); - if (typeof decoded !== 'object' || decoded === null) { - throw new Error(`Invalid event details: ${JSON.stringify(decoded)}. Expected an object.`); - } - if (!('target' in decoded && TokenHolder.instanceOf(decoded.target))) { - throw new Error(`Invalid event details: ${JSON.stringify(decoded)}. Expected 'target' to be a TokenHolder`); - } - - return decoded as TokenListUpdateEventDetails; -} - -function decodeTokenPauseEventDetails(value: Cbor): TokenPauseEventDetails { - const decoded = cborDecode(value.bytes); - if (typeof decoded !== 'object' || decoded === null) { - throw new Error(`Invalid event details: ${JSON.stringify(decoded)}. Expected an object.`); - } - - return decoded as TokenPauseEventDetails; -} - -function decodeTokenInitializationParameters(value: Cbor): TokenInitializationParameters { - const decoded = cborDecode(value.bytes); - if (typeof decoded !== 'object' || decoded === null) { - throw new Error('Invalid CBOR data for TokenInitializationParameters'); - } - - // Validate required fields - if (!('governanceAccount' in decoded && TokenHolder.instanceOf(decoded.governanceAccount))) { - throw new Error('Invalid TokenInitializationParameters: missing or invalid governanceAccount'); - } - if (!('metadata' in decoded)) { - throw new Error('Invalid TokenInitializationParameters: missing metadataUrl'); - } - let metadata = TokenMetadataUrl.fromCBORValue(decoded.metadata); - if (!('name' in decoded && typeof decoded.name === 'string')) { - throw new Error('Invalid TokenInitializationParameters: missing or invalid name'); - } - - // Validate optional fields - if ('allowList' in decoded && typeof decoded.allowList !== 'boolean') { - throw new Error('Invalid TokenInitializationParameters: allowList must be a boolean'); - } - if ('denyList' in decoded && typeof decoded.denyList !== 'boolean') { - throw Error('Invalid TokenInitializationParameters: denyList must be a boolean'); - } - if ('mintable' in decoded && typeof decoded.mintable !== 'boolean') { - throw new Error('Invalid TokenInitializationParameters: mintable must be a boolean'); - } - if ('burnable' in decoded && typeof decoded.burnable !== 'boolean') { - throw new Error('Invalid TokenInitializationParameters: burnable must be a boolean'); - } - if ('paused' in decoded && typeof decoded.paused !== 'boolean') { - throw new Error('Invalid TokenInitializationParameters: paused must be a boolean'); - } - - // Optional initial supply - if ('initialSupply' in decoded && !TokenAmount.instanceOf(decoded.initialSupply)) { - throw new Error(`Invalid TokenInitializationParameters: Expected 'initialSupply' to be of type 'TokenAmount'`); - } - - return { ...decoded, metadata } as TokenInitializationParameters; -} - -type DecodeTypeMap = { - TokenModuleState: TokenModuleState; - TokenModuleAccountState: TokenModuleAccountState; - TokenListUpdateEventDetails: TokenListUpdateEventDetails; - TokenPauseEventDetails: TokenPauseEventDetails; - TokenInitializationParameters: TokenInitializationParameters; -}; - -export function decode(cbor: Cbor, type: T): DecodeTypeMap[T]; -export function decode(cbor: Cbor, type?: undefined): unknown; - -/** - * Decode CBOR encoded data into its original representation. - * @param {Cbor} cbor - The CBOR encoded data. - * @param {string | undefined} type - Optional type hint for decoding. - * @returns {unknown} The decoded data. - */ -export function decode(cbor: Cbor, type: T): unknown { - switch (type) { - case 'TokenModuleState': - return decodeTokenModuleState(cbor); - case 'TokenModuleAccountState': - return decodeTokenModuleAccountState(cbor); - case 'TokenListUpdateEventDetails': - return decodeTokenListUpdateEventDetails(cbor); - case 'TokenPauseEventDetails': - return decodeTokenPauseEventDetails(cbor); - case 'TokenInitializationParameters': - return decodeTokenInitializationParameters(cbor); - default: - return cborDecode(cbor.bytes); - } -} +export { decode } from './decode.js'; /** * Encode a value into CBOR format. diff --git a/packages/sdk/src/plt/CborAccountAddress.ts b/packages/sdk/src/plt/CborAccountAddress.ts new file mode 100644 index 000000000..4a7194b2e --- /dev/null +++ b/packages/sdk/src/plt/CborAccountAddress.ts @@ -0,0 +1,305 @@ +import { Tag, decode } from 'cbor2'; +import { encode, registerEncoder } from 'cbor2/encoder'; + +import { Base58String } from '../index.js'; +import { AccountAddress } from '../types/index.js'; +import { bail } from '../util.js'; + +const CCD_NETWORK_ID = 919; // Concordium network identifier - Did you know 919 is a palindromic prime and a centred hexagonal number? + +/** JSON representation of a {@link Type}. */ +export type JSON = { + /** The address of the account holding the token. */ + address: Base58String; + /** Optional coininfo describing the network for the account. */ + coinInfo?: typeof CCD_NETWORK_ID; +}; + +class CborAccountAddress { + #nominal = true; + + constructor( + /** The address of the account holding the token. */ + public readonly address: AccountAddress.Type, + /** + * Optional coin info describing the network for the account. If this is `undefined` + * it is interpreted as a Concordium account. + */ + public readonly coinInfo: typeof CCD_NETWORK_ID | undefined + ) {} + + public toString(): string { + return this.address.toString(); + } + + /** + * Get a JSON-serializable representation of the account address. This is called implicitly when serialized with JSON.stringify. + * @returns {JSON} The JSON representation. + */ + public toJSON(): JSON { + return { + address: this.address.toJSON(), + coinInfo: this.coinInfo, + }; + } +} + +/** + * Public type alias for the CBOR aware AccountAddress wrapper. + * Instances are created via the helper factory functions rather than the class constructor. + */ +export type Type = CborAccountAddress; + +/** + * Construct a {@link Type} from an existing {@link AccountAddress.Type}. + * Coin information will default to the Concordium network id (919). + */ +export function fromAccountAddress(address: AccountAddress.Type): CborAccountAddress { + return new CborAccountAddress(address, CCD_NETWORK_ID); +} + +/** + * Recreate a {@link Type} from its JSON form. + * @throws {Error} If the supplied coinInfo is present and not the Concordium network id. + */ +export function fromJSON(json: JSON): Type { + if (json.coinInfo !== undefined && json.coinInfo !== CCD_NETWORK_ID) { + throw new Error(`Unsupported coin info for account address: ${json.coinInfo}. Expected ${CCD_NETWORK_ID}.`); + } + return new CborAccountAddress(AccountAddress.fromJSON(json.address), json.coinInfo); +} + +/** + * Construct a CborAccountAddress from a base58check string. + * + * @param {string} address String of base58check encoded account address, must use a byte version of 1. + * @returns {CborAccountAddress} The CborAccountAddress. + * @throws If the provided string is not: exactly 50 characters, a valid base58check encoding using version byte 1. + */ +export function fromBase58(address: string): CborAccountAddress { + return fromAccountAddress(AccountAddress.fromBase58(address)); +} + +/** + * Get a base58check string of the account address. + * @param {CborAccountAddress} accountAddress The account address. + */ +export function toBase58(accountAddress: CborAccountAddress): string { + return accountAddress.address.address; +} + +/** + * Type predicate which checks if a value is an instance of {@linkcode Type} + */ +export function instanceOf(value: unknown): value is Type { + return value instanceof CborAccountAddress; +} + +// CBOR +const TAGGED_ADDRESS = 40307; +const TAGGED_COININFO = 40305; + +/** + * Converts an {@linkcode Account} to a CBOR tagged value. + * This encodes the account address as a CBOR tagged value with tag 40307, containing both + * the coin information (tagged as 40305) and the account's decoded address. + */ +function toCBORValue(value: Type): Tag { + let mapContents: [number, any][]; + if (value.coinInfo === undefined) { + mapContents = [[3, value.address.decodedAddress]]; + } else { + const taggedCoinInfo = new Tag(TAGGED_COININFO, new Map([[1, CCD_NETWORK_ID]])); + mapContents = [ + [1, taggedCoinInfo], + [3, value.address.decodedAddress], + ]; + } + + const map = new Map(mapContents); + return new Tag(TAGGED_ADDRESS, map); +} + +/** + * Converts an CborAccountAddress to its CBOR (Concise Binary Object Representation) encoding. + * This encodes the account address as a CBOR tagged value with tag 40307, containing both + * the coin information (tagged as 40305) and the account's decoded address. + * + * This corresponds to a concordium-specific subtype of the `tagged-address` type from + * [BCR-2020-009]{@link https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-009-address.md}, + * identified by `tagged-coininfo` corresponding to the Concordium network from + * [BCR-2020-007]{@link https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-007-hdkey.md} + * + * Example of CBOR diagnostic notation for an encoded account address: + * ``` + * 40307({ + * 1: 40305({1: 919}), + * 3: h'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789' + * }) + * ``` + * Where 919 is the Concordium network identifier and the hex string is the raw account address. + * + * @param {Type} value - The token holder to convert to CBOR format. + * @throws {Error} - If an unsupported CBOR encoding is specified. + * @returns {Uint8Array} The CBOR encoded representation of the token holder. + */ +export function toCBOR(value: Type): Uint8Array { + return new Uint8Array(encode(toCBORValue(value))); +} + +/** + * Registers a CBOR encoder for the CborAccountAddress type with the `cbor2` library. + * This allows CborAccountAddress instances to be automatically encoded when used with + * the `cbor2` library's encode function. + * + * @returns {void} + * @example + * // Register the encoder + * registerCBOREncoder(); + * // Now CborAccountAddress instances can be encoded directly + * const encoded = encode(myCborAccountAddress); + */ +export function registerCBOREncoder(): void { + registerEncoder(CborAccountAddress, (value) => [TAGGED_ADDRESS, toCBORValue(value).contents]); +} + +/** + * Decodes a CBOR-encoded token holder account into an {@linkcode Account} instance. + * @param {unknown} decoded - The CBOR decoded value, expected to be a tagged value with tag 40307. + * @throws {Error} - If the decoded value is not a valid CBOR encoded token holder account. + * @returns {Account} The decoded account address as a CborAccountAddress instance. + */ +function fromCBORValueAccount(decoded: unknown): CborAccountAddress { + // Verify we have a tagged value with tag 40307 (tagged-address) + if (!(decoded instanceof Tag) || decoded.tag !== TAGGED_ADDRESS) { + throw new Error(`Invalid CBOR encoded token holder account: expected tag ${TAGGED_ADDRESS}`); + } + + const value = decoded.contents; + + if (!(value instanceof Map)) { + throw new Error('Invalid CBOR encoded token holder account: expected a map'); + } + + // Verify the map corresponds to the BCR-2020-009 `address` format + const validKeys = [1, 2, 3]; // we allow 2 here, as it is in the spec for BCR-2020-009 `address`, but we don't use it + for (const key of value.keys()) { + validKeys.includes(key) || bail(`Invalid CBOR encoded token holder account: unexpected key ${key}`); + } + + // Extract the token holder account bytes (key 3) + const addressBytes = value.get(3); + if ( + !addressBytes || + !(addressBytes instanceof Uint8Array) || + addressBytes.byteLength !== AccountAddress.BYTES_LENGTH + ) { + throw new Error('Invalid CBOR encoded token holder account: missing or invalid address bytes'); + } + + // Optional validation for coin information if present (key 1) + const coinInfo = value.get(1); + let coinInfoValue = undefined; + if (coinInfo !== undefined) { + // Verify coin info has the correct tag if present + if (!(coinInfo instanceof Tag) || coinInfo.tag !== TAGGED_COININFO) { + throw new Error( + `Invalid CBOR encoded token holder account: coin info has incorrect tag (expected ${TAGGED_COININFO})` + ); + } + + // Verify coin info contains Concordium network identifier if present + const coinInfoMap = coinInfo.contents; + if (!(coinInfoMap instanceof Map) || coinInfoMap.get(1) !== CCD_NETWORK_ID) { + throw new Error( + `Invalid CBOR token holder account: coin info does not contain Concordium network identifier ${CCD_NETWORK_ID}` + ); + } + coinInfoValue = coinInfoMap.get(1); + + // Verify the map corresponds to the BCR-2020-007 `coininfo` format + const validKeys = [1, 2]; // we allow 2 here, as it is in the spec for BCR-2020-007 `coininfo`, but we don't use it + for (const key of coinInfoMap.keys()) { + validKeys.includes(key) || bail(`Invalid CBOR encoded coininfo: unexpected key ${key}`); + } + } + + // Create the AccountAddress from the extracted bytes + return new CborAccountAddress(AccountAddress.fromBuffer(addressBytes), coinInfoValue); +} + +/** + * Decodes a CBOR value into a CborAccountAddress instance. + * This function checks if the value is a tagged address (40307) and decodes it accordingly. + * + * @param {unknown} value - The CBOR decoded value, expected to be a tagged address. + * @throws {Error} - If the value is not a valid CBOR encoded token holder account. + * @returns {Type} The decoded CborAccountAddress instance. + */ +export function fromCBORValue(value: unknown): Type { + if (value instanceof Tag && value.tag === TAGGED_ADDRESS) { + return fromCBORValueAccount(value); + } + + throw new Error(`Failed to decode 'CborAccountAddress.Type' from CBOR value: ${value}`); +} + +/** + * Decodes a CBOR-encoded account address into an CborAccountAddress instance. + * This function can handle both the full tagged format (with coin information) + * and a simplified format with just the address bytes. + * + * 1. With `tagged-coininfo` (40305): + * ``` + * 40307({ + * 1: 40305({1: 919}), // Optional coin information + * 3: h'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789' + * }) + * ``` + * + * 2. Without `tagged-coininfo`: + * ``` + * 40307({ + * 3: h'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789' + * }) // The address is assumed to be a Concordium address + * ``` + * + * @param {Uint8Array} bytes - The CBOR encoded representation of an account address. + * @throws {Error} - If the input is not a valid CBOR encoding of an account address. + * @returns {Type} The decoded CborAccountAddress instance. + */ +export function fromCBOR(bytes: Uint8Array): Type { + return fromCBORValue(decode(bytes)); +} + +/** + * Registers a CBOR decoder for the tagged-address (40307) format with the `cbor2` library. + * This enables automatic decoding of CBOR data containing Concordium account addresses + * when using the `cbor2` library's decode function. + * + * @returns {() => void} A cleanup function that, when called, will restore the previous + * decoder (if any) that was registered for the tagged-address format. This is useful + * when used in an existing `cbor2` use-case. + * + * @example + * // Register the decoder + * const cleanup = registerCBORDecoder(); + * // Use the decoder + * const tokenHolder = decode(cborBytes); // Returns CborAccountAddress if format matches + * // Later, unregister the decoder + * cleanup(); + */ +export function registerCBORDecoder(): () => void { + const old = [Tag.registerDecoder(TAGGED_ADDRESS, fromCBORValue)]; + + // return cleanup function to restore the old decoder + return () => { + for (const decoder of old) { + if (decoder) { + Tag.registerDecoder(TAGGED_ADDRESS, decoder); + } else { + Tag.clearDecoder(TAGGED_ADDRESS); + } + } + }; +} diff --git a/packages/sdk/src/plt/CborContractAddress.ts b/packages/sdk/src/plt/CborContractAddress.ts new file mode 100644 index 000000000..64148e6af --- /dev/null +++ b/packages/sdk/src/plt/CborContractAddress.ts @@ -0,0 +1,282 @@ +import { Tag, decode } from 'cbor2'; +import { encode, registerEncoder } from 'cbor2/encoder'; + +import { MAX_U64 } from '../constants.js'; +import { ContractAddress } from '../types/index.js'; +import { isDefined } from '../util.js'; + +/** + * Enum representing the types of errors that can occur when creating a contract address. + */ +export enum ErrorType { + /** Error type indicating a contract index exceeds the maximum allowed value. */ + EXCEEDS_MAX_VALUE = 'EXCEEDS_MAX_VALUE', + /** Error type indicating a contract index is negative. */ + NEGATIVE = 'NEGATIVE', +} + +/** + * Custom error to represent issues with contract addresses. + */ +export class Err extends Error { + private constructor( + /** The {@linkcode ErrorType} of the error. Can be used to distinguish different types of errors. */ + public readonly type: ErrorType, + message: string + ) { + super(message); + this.name = `CborContractAddress.Err.${type}`; + } + + /** + * Creates a CborContractAddress.Err indicating that the contract address index exceeds the maximum allowed value. + */ + public static exceedsMaxValue(): Err { + return new Err(ErrorType.EXCEEDS_MAX_VALUE, `Contract indices cannot be larger than ${MAX_U64}`); + } + + /** + * Creates a CborContractAddress.Err indicating that the contract address index is negative. + */ + public static negative(): Err { + return new Err(ErrorType.NEGATIVE, 'Contract indices cannot be negative'); + } +} + +/** + * CIS-7 CBOR representation of a `ContractAddress`. + */ +class CborContractAddress { + #nominal = true; + + constructor( + /** The index of the smart contract address. */ + public readonly index: bigint, + /** The subindex of the smart contract address. Interpreted as `0` if not specified. */ + public readonly subindex?: bigint + ) { + const values = [index, subindex].filter(isDefined); + if (values.some((v) => v < 0n)) { + throw Err.negative(); + } + if (values.some((v) => v > MAX_U64)) { + throw Err.exceedsMaxValue(); + } + } + + /** + * Get a string representation of the contract address using the `` format. + * @returns {string} The string representation. + */ + public toString(): string { + return toContractAddress(this).toString(); + } + + /** + * Get the JSON representation (i.e. object format) of the contract address. + * It's up to the user to process this, as bigints are not JSON serializable. + * @returns {JSON} The JSON representation. + */ + public toJSON(): JSON { + if (this.subindex === undefined) return { index: this.index }; + return { index: this.index, subindex: this.subindex }; + } +} + +/** + * CIS-7 CBOR representation of a `ContractAddress`. + */ +export type Type = CborContractAddress; + +/** + * Type predicate for {@linkcode Type}. + * + * @param v - a value of unknown type to check + * @returns whether the type is an instance of {@linkcode Type} + */ +export const instanceOf = (v: unknown): v is CborContractAddress => v instanceof CborContractAddress; + +/** + * The JSON representation of a {@linkcode Type} cbor contract address. + * It's up to the user to process this, as bigints are not JSON serializable. + */ +export type JSON = { index: bigint; subindex?: bigint }; + +type NumLike = string | number | bigint; + +/** + * Create a CBOR-compatible contract address from numeric index (and optional subindex). + * + * @param index Index of the contract (string | number | bigint accepted, coerced via BigInt()). + * @param subindex Optional subindex of the contract (same coercion rules). If `0`, the value is omitted. + * + * @returns CborContractAddress instance representing the provided (index, subindex). + * @throws {Err} If index or subindex is negative ({@link Err.negative}). + * @throws {Err} if index of subindex exceed MAX_U64 ({@link Err.exceedsMaxValue}). + */ +export function create(index: NumLike, subindex?: NumLike): CborContractAddress { + if (subindex === undefined) { + return new CborContractAddress(BigInt(index)); + } + + const sub = BigInt(subindex); + if (sub === 0n) { + return new CborContractAddress(BigInt(index)); + } + + return new CborContractAddress(BigInt(index), BigInt(subindex)); +} + +/** + * Convert a public `ContractAddress.Type` (sdk representation) into its CBOR wrapper form. + * + * @param address The contract address value (with bigint index/subindex) to wrap. + * @returns Equivalent CborContractAddress instance. + */ +export function fromContractAddress(address: ContractAddress.Type): CborContractAddress { + return create(address.index, address.subindex); +} + +/** + * Convert a CBOR wrapper contract address to the public `ContractAddress.Type`. + * + * @param address The CBOR contract address wrapper to convert. + * @returns New `ContractAddress.Type` constructed from wrapper values. + */ +export function toContractAddress(address: CborContractAddress): ContractAddress.Type { + return ContractAddress.create(address.index, address.subindex); +} + +/** + * Create a CborContractAddress from its JSON-like object representation. + * + * @param address Object with index and optional subindex. + * @returns Corresponding CborContractAddress instance. + */ +export function fromJSON(address: JSON): CborContractAddress { + if (address.subindex === undefined || address.subindex === null) { + return new CborContractAddress(BigInt(address.index)); + } + return new CborContractAddress(BigInt(address.index), BigInt(address.subindex)); +} + +const TAGGED_CONTRACT_ADDRESS = 40919; + +/** + * Produce the tagged CBOR value representation (tag + contents) for a contract address. + * Tag format simple: 40919(index). + * Tag format full: 40919([index, subindex]). + * + * @param address The CBOR contract address wrapper instance. + * @returns cbor2.Tag carrying the encoded address. + */ +export function toCBORValue(address: CborContractAddress): Tag { + let contents = address.subindex === undefined ? address.index : [address.index, address.subindex]; + return new Tag(TAGGED_CONTRACT_ADDRESS, contents); +} + +/** + * Encode a contract address to raw CBOR binary (Uint8Array) using its tagged representation. + * + * @param address The CBOR contract address wrapper to encode. + * @returns Uint8Array containing the canonical CBOR encoding. + */ +export function toCBOR(address: CborContractAddress): Uint8Array { + return new Uint8Array(encode(toCBORValue(address))); +} + +/** + * Registers a CBOR encoder for the CborContractAddress type with the `cbor2` library. + * This allows CborContractAddress instances to be automatically encoded when used with + * the `cbor2` library's encode function. + * + * @returns {void} + * @example + * // Register the encoder + * registerCBOREncoder(); + * // Now CborContractAddress instances can be encoded directly + * const encoded = encode(myCborContractAddress); + */ +export function registerCBOREncoder(): void { + registerEncoder(CborContractAddress, (value) => { + const { tag, contents } = toCBORValue(value); + return [tag, contents]; + }); +} + +/** + * Decodes a CBOR-encoded contract address tagged value (tag 40919) into a {@linkcode CborContractAddress} instance. + * + * @param {unknown} decoded - The CBOR decoded value, expected to be a tagged value with tag 40919. + * @throws {Error} - If the decoded value is not a valid CBOR encoded contract address. + * @returns {CborContractAddress} The decoded contract address as a CborContractAddress instance. + */ +function fromCBORValue(decoded: unknown): CborContractAddress { + // Verify we have a tagged value with tag 40919 (tagged-contract-address) + if (!(decoded instanceof Tag) || decoded.tag !== TAGGED_CONTRACT_ADDRESS) { + throw new Error(`Invalid CBOR encoded contract address: expected tag ${TAGGED_CONTRACT_ADDRESS}`); + } + + const validateUint = (val: unknown): val is number | bigint => typeof val === 'number' || typeof val === 'bigint'; + + const value = decoded.contents; + if (Array.isArray(value) && value.length === 2 && value.every(validateUint)) + return new CborContractAddress(BigInt(value[0]), BigInt(value[1])); + else if (validateUint(value)) return new CborContractAddress(BigInt(value)); + else throw new Error('Invalid CBOR encoded contract address: expected uint value or tuple with 2 uint values.'); +} + +/** + * Decodes a CBOR-encoded contract address into an CborContractAddress instance. + * This function can handle both the full format (with subindex) + * and a simplified format with just the index. + * + * 1. With subindex: + * ``` + * [uint, uint] + * ``` + * + * 2. Without subindex: + * ``` + * uint + * ``` + * + * @param {Uint8Array} bytes - The CBOR encoded representation of an contract address. + * @throws {Error} - If the input is not a valid CBOR encoding of an contract address. + * @returns {Type} The decoded CborContractAddress instance. + */ +export function fromCBOR(bytes: Uint8Array): Type { + return fromCBORValue(decode(bytes)); +} + +/** + * Registers a CBOR decoder for the tagged-contract address (40919) format with the `cbor2` library. + * This enables automatic decoding of CBOR data containing Concordium contract addresses + * when using the `cbor2` library's decode function. + * + * @returns {() => void} A cleanup function that, when called, will restore the previous + * decoder (if any) that was registered for the tagged-address format. This is useful + * when used in an existing `cbor2` use-case. + * + * @example + * // Register the decoder + * const cleanup = registerCBORDecoder(); + * // Use the decoder + * const tokenHolder = decode(cborBytes); // Returns CborContractAddress if format matches + * // Later, unregister the decoder + * cleanup(); + */ +export function registerCBORDecoder(): () => void { + const old = [Tag.registerDecoder(TAGGED_CONTRACT_ADDRESS, fromCBORValue)]; + + // return cleanup function to restore the old decoder + return () => { + for (const decoder of old) { + if (decoder) { + Tag.registerDecoder(TAGGED_CONTRACT_ADDRESS, decoder); + } else { + Tag.clearDecoder(TAGGED_CONTRACT_ADDRESS); + } + } + }; +} diff --git a/packages/sdk/src/plt/CborMemo.ts b/packages/sdk/src/plt/CborMemo.ts index 429338220..33632be28 100644 --- a/packages/sdk/src/plt/CborMemo.ts +++ b/packages/sdk/src/plt/CborMemo.ts @@ -4,7 +4,8 @@ import { encode, registerEncoder } from 'cbor2/encoder'; import { Tag } from 'cbor2/tag'; import * as Proto from '../grpc-api/v2/concordium/kernel.js'; -import { HexString, cborDecode } from '../index.js'; +import type { HexString } from '../types.js'; +import { cborDecode } from '../types/cbor.js'; const TAGGED_MEMO = 24; diff --git a/packages/sdk/src/plt/Token.ts b/packages/sdk/src/plt/Token.ts index e645cc459..32ddf84a9 100644 --- a/packages/sdk/src/plt/Token.ts +++ b/packages/sdk/src/plt/Token.ts @@ -12,13 +12,17 @@ import { import { AccountSigner, signTransaction } from '../signHelpers.js'; import { SequenceNumber } from '../types/index.js'; import { bail } from '../util.js'; -import { Cbor, TokenAmount, TokenHolder, TokenId, TokenInfo, TokenModuleReference } from './index.js'; import { + Cbor, + CborAccountAddress, TokenAddAllowListOperation, TokenAddDenyListOperation, + TokenAmount, TokenBurnOperation, + TokenId, + TokenInfo, TokenMintOperation, - TokenModuleAccountState, + TokenModuleReference, TokenModuleState, TokenOperation, TokenOperationType, @@ -29,7 +33,7 @@ import { TokenTransferOperation, TokenUnpauseOperation, createTokenUpdatePayload, -} from './module.js'; +} from './index.js'; /** * Enum representing the types of errors that can occur when interacting with PLT instances through the client. @@ -132,9 +136,9 @@ export class NotAllowedError extends TokenError { /** * Constructs a new NotAllowedError. - * @param {TokenHolder.Type} receiver - The account address of the receiver. + * @param {AccountAddress.Type} receiver - The account address of the receiver. */ - constructor(public readonly receiver: TokenHolder.Type) { + constructor(public readonly receiver: AccountAddress.Type) { super( `Transfering funds from or to the account specified is currently not allowed (${receiver}) because of the allow/deny list.` ); @@ -490,12 +494,10 @@ export async function validateTransfer( const accountModuleState = accountToken?.moduleState === undefined ? undefined - : (Cbor.decode(accountToken.moduleState) as TokenModuleAccountState); + : Cbor.decode(accountToken.moduleState, 'TokenModuleAccountState'); - if (token.moduleState.denyList && accountModuleState?.denyList) - throw new NotAllowedError(TokenHolder.fromAccountAddress(r.accountAddress)); - if (token.moduleState.allowList && !accountModuleState?.allowList) - throw new NotAllowedError(TokenHolder.fromAccountAddress(r.accountAddress)); + if (token.moduleState.denyList && accountModuleState?.denyList) throw new NotAllowedError(r.accountAddress); + if (token.moduleState.allowList && !accountModuleState?.allowList) throw new NotAllowedError(r.accountAddress); }); return true; @@ -615,12 +617,17 @@ type TransferOtions = { validate?: boolean; }; +export type TransferInput = Omit & { + /** The recipient of the transfer. */ + recipient: AccountAddress.Type; +}; + /** * Transfers tokens from the sender to the specified recipients. * * @param {Token} token - The token to transfer. * @param {AccountAddress.Type} sender - The account address of the sender. - * @param {TokenTransfer | TokenTransfer[]} payload - The transfer payload. + * @param {TransferInput | TransferInput[]} payload - The transfer payload. * @param {AccountSigner} signer - The signer responsible for signing the transaction. * @param {TokenUpdateMetadata} [metadata={ expiry: TransactionExpiry.futureMinutes(5) }] - The metadata for the token update. * @param {TransferOtions} [opts={ autoScale: true, validate: false }] - Options for the transfer. @@ -634,15 +641,16 @@ type TransferOtions = { export async function transfer( token: Token, sender: AccountAddress.Type, - payload: TokenTransfer | TokenTransfer[], + payload: TransferInput | TransferInput[], signer: AccountSigner, metadata?: TokenUpdateMetadata, { autoScale = true, validate = false }: TransferOtions = {} ): Promise { - let transfers: TokenTransfer[] = [payload].flat(); - if (autoScale) { - transfers = transfers.map((p) => ({ ...p, amount: scaleAmount(token, p.amount) })); - } + let transfers: TokenTransfer[] = [payload].flat().map((p) => ({ + ...p, + recipient: CborAccountAddress.fromAccountAddress(p.recipient), + amount: autoScale ? scaleAmount(token, p.amount) : p.amount, + })); if (validate) { await validateTransfer(token, sender, transfers); @@ -746,7 +754,7 @@ type UpdateListOptions = { * * @param {Token} token - The token for which to add the list entry. * @param {AccountAddress.Type} sender - The account address of the sender. - * @param {TokenHolder.Type | TokenHolder.Type[]} targets - The account address(es) to be added to the list. + * @param {AccountAddress.Type | AccountAddress.Type[]} targets - The account address(es) to be added to the list. * @param {AccountSigner} signer - The signer responsible for signing the transaction. * @param {TokenUpdateMetadata} [metadata={ expiry: TransactionExpiry.futureMinutes(5) }] - The metadata for the token update. * @param {UpdateListOptions} [opts={ validate: false }] - Options for updating the allow/deny list. @@ -757,7 +765,7 @@ type UpdateListOptions = { export async function addAllowList( token: Token, sender: AccountAddress.Type, - targets: TokenHolder.Type | TokenHolder.Type[], + targets: AccountAddress.Type | AccountAddress.Type[], signer: AccountSigner, metadata?: TokenUpdateMetadata, { validate = false }: UpdateListOptions = {} @@ -766,9 +774,9 @@ export async function addAllowList( await validateAllowListUpdate(token); } - const ops: TokenAddAllowListOperation[] = [targets] - .flat() - .map((target) => ({ [TokenOperationType.AddAllowList]: { target } })); + const ops: TokenAddAllowListOperation[] = [targets].flat().map((target) => ({ + [TokenOperationType.AddAllowList]: { target: CborAccountAddress.fromAccountAddress(target) }, + })); return sendOperations(token, sender, ops, signer, metadata); } @@ -777,7 +785,7 @@ export async function addAllowList( * * @param {Token} token - The token for which to add the list entry. * @param {AccountAddress.Type} sender - The account address of the sender. - * @param {TokenHolder.Type | TokenHolder.Type[]} targets - The account address(es) to be added to the list. + * @param {AccountAddress.Type | AccountAddress.Type[]} targets - The account address(es) to be added to the list. * @param {AccountSigner} signer - The signer responsible for signing the transaction. * @param {TokenUpdateMetadata} [metadata={ expiry: TransactionExpiry.futureMinutes(5) }] - The metadata for the token update. * @param {UpdateListOptions} [opts={ validate: false }] - Options for updating the allow/deny list. @@ -788,7 +796,7 @@ export async function addAllowList( export async function removeAllowList( token: Token, sender: AccountAddress.Type, - targets: TokenHolder.Type | TokenHolder.Type[], + targets: AccountAddress.Type | AccountAddress.Type[], signer: AccountSigner, metadata?: TokenUpdateMetadata, { validate = false }: UpdateListOptions = {} @@ -797,9 +805,9 @@ export async function removeAllowList( await validateAllowListUpdate(token); } - const ops: TokenRemoveAllowListOperation[] = [targets] - .flat() - .map((target) => ({ [TokenOperationType.RemoveAllowList]: { target } })); + const ops: TokenRemoveAllowListOperation[] = [targets].flat().map((target) => ({ + [TokenOperationType.RemoveAllowList]: { target: CborAccountAddress.fromAccountAddress(target) }, + })); return sendOperations(token, sender, ops, signer, metadata); } @@ -808,7 +816,7 @@ export async function removeAllowList( * * @param {Token} token - The token for which to add the list entry. * @param {AccountAddress.Type} sender - The account address of the sender. - * @param {TokenHolder.Type | TokenHolder.Type[]} targets - The account address(es) to be added to the list. + * @param {AccountAddress.Type | AccountAddress.Type[]} targets - The account address(es) to be added to the list. * @param {AccountSigner} signer - The signer responsible for signing the transaction. * @param {TokenUpdateMetadata} [metadata={ expiry: TransactionExpiry.futureMinutes(5) }] - The metadata for the token update. * @param {UpdateListOptions} [opts={ validate: false }] - Options for updating the allow/deny list. @@ -819,7 +827,7 @@ export async function removeAllowList( export async function addDenyList( token: Token, sender: AccountAddress.Type, - targets: TokenHolder.Type | TokenHolder.Type[], + targets: AccountAddress.Type | AccountAddress.Type[], signer: AccountSigner, metadata?: TokenUpdateMetadata, { validate = false }: UpdateListOptions = {} @@ -828,9 +836,9 @@ export async function addDenyList( await validateDenyListUpdate(token); } - const ops: TokenAddDenyListOperation[] = [targets] - .flat() - .map((target) => ({ [TokenOperationType.AddDenyList]: { target } })); + const ops: TokenAddDenyListOperation[] = [targets].flat().map((target) => ({ + [TokenOperationType.AddDenyList]: { target: CborAccountAddress.fromAccountAddress(target) }, + })); return sendOperations(token, sender, ops, signer, metadata); } @@ -839,7 +847,7 @@ export async function addDenyList( * * @param {Token} token - The token for which to add the list entry. * @param {AccountAddress.Type} sender - The account address of the sender. - * @param {TokenHolder.Type | TokenHolder.Type[]} targets - The account address(es) to be added to the list. + * @param {AccountAddress.Type | AccountAddress.Type[]} targets - The account address(es) to be added to the list. * @param {AccountSigner} signer - The signer responsible for signing the transaction. * @param {TokenUpdateMetadata} [metadata={ expiry: TransactionExpiry.futureMinutes(5) }] - The metadata for the token update. * @param {UpdateListOptions} [opts={ validate: false }] - Options for updating the allow/deny list. @@ -850,7 +858,7 @@ export async function addDenyList( export async function removeDenyList( token: Token, sender: AccountAddress.Type, - targets: TokenHolder.Type | TokenHolder.Type[], + targets: AccountAddress.Type | AccountAddress.Type[], signer: AccountSigner, metadata?: TokenUpdateMetadata, { validate = false }: UpdateListOptions = {} @@ -859,9 +867,9 @@ export async function removeDenyList( await validateDenyListUpdate(token); } - const ops: TokenRemoveDenyListOperation[] = [targets] - .flat() - .map((target) => ({ [TokenOperationType.RemoveDenyList]: { target } })); + const ops: TokenRemoveDenyListOperation[] = [targets].flat().map((target) => ({ + [TokenOperationType.RemoveDenyList]: { target: CborAccountAddress.fromAccountAddress(target) }, + })); return sendOperations(token, sender, ops, signer, metadata); } @@ -906,7 +914,7 @@ export async function unpause( } /** - * Executes a batch of governance operations on a token. + * Executes a batch of operations on a token. * * @param {Token} token - The token on which to perform the operations. * @param {AccountAddress.Type} sender - The account address of the sender. diff --git a/packages/sdk/src/plt/TokenHolder.ts b/packages/sdk/src/plt/TokenHolder.ts index eca9f82c4..eebddb691 100644 --- a/packages/sdk/src/plt/TokenHolder.ts +++ b/packages/sdk/src/plt/TokenHolder.ts @@ -1,74 +1,100 @@ -import { Tag, decode } from 'cbor2'; -import { encode, registerEncoder } from 'cbor2/encoder'; - import type * as Proto from '../grpc-api/v2/concordium/protocol-level-tokens.js'; +import type { + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used for docs + Unknown, + Upward, +} from '../grpc/index.js'; import { Base58String } from '../index.js'; import { AccountAddress } from '../types/index.js'; -import { bail } from '../util.js'; interface TokenHolder { - readonly type: T; + /** The type of the token holder. */ + type: T; } -const CCD_NETWORK_ID = 919; // Concordium network identifier - Did you know 919 is a palindromic prime and a centred hexagonal number? - -type TokenHolderAccountJSON = { - type: 'account'; +type TokenHolderAccountJSON = TokenHolder<'account'> & { + /** The address of the token holder account. */ address: Base58String; - coinInfo?: typeof CCD_NETWORK_ID; }; class TokenHolderAccount implements TokenHolder<'account'> { - #nominal = true; public readonly type = 'account'; constructor( /** The address of the account holding the token. */ - public readonly address: AccountAddress.Type, - public readonly coinInfo: typeof CCD_NETWORK_ID | undefined + public readonly address: AccountAddress.Type ) {} public toString(): string { return this.address.toString(); } + /** + * Get a JSON-serializable representation of the token holder account. This is called implicitly when serialized with JSON.stringify. + * @returns {TokenHolderAccountJSON} The JSON representation. + */ public toJSON(): TokenHolderAccountJSON { return { - type: this.type, + type: 'account', address: this.address.toJSON(), - coinInfo: this.coinInfo, }; } } +/** Describes the `Account` variant of a `TokenHolder`. */ export type Account = TokenHolderAccount; +/** Describes the `Account` variant of a `TokenHolder.JSON`. */ export type AccountJSON = TokenHolderAccountJSON; +/** Describes any variant of a `TokenHolder`. */ export type Type = Account; // Can be extended to include other token holder types in the future +/** Describes the JSON representation of variant of any `TokenHolder`. */ export type JSON = AccountJSON; // Can be extended to include other token holder types in the future export function fromAccountAddress(address: AccountAddress.Type): TokenHolderAccount { - return new TokenHolderAccount(address, CCD_NETWORK_ID); -} - -export function fromAccountAddressNoCoinInfo(address: AccountAddress.Type): TokenHolderAccount { - return new TokenHolderAccount(address, undefined); + return new TokenHolderAccount(address); } +/** + * Recreate a token holder {@link Account} from its JSON form. + */ export function fromJSON(json: AccountJSON): Account; -export function fromJSON(json: JSON): Type; -export function fromJSON(json: JSON): Type { +/** + * Recreate a {@link Type} from its JSON form. + * If the `type` field is unknown, {@linkcode Unknown} is returned. + */ +export function fromJSON(json: JSON): Upward; +export function fromJSON(json: JSON): Upward { switch (json.type) { case 'account': - if (json.coinInfo !== undefined && json.coinInfo !== CCD_NETWORK_ID) { - throw new Error( - `Unsupported coin info for token holder account: ${json.coinInfo}. Expected ${CCD_NETWORK_ID}.` - ); - } - return new TokenHolderAccount(AccountAddress.fromJSON(json.address), json.coinInfo); + return new TokenHolderAccount(AccountAddress.fromJSON(json.address)); + default: + return null; } } +/** + * Construct a {@linkcode Account} from a base58check string. + * + * @param {string} address String of base58check encoded account address, must use a byte version of 1. + * @returns {Account} The token holder account. + * @throws If the provided string is not: exactly 50 characters, a valid base58check encoding using version byte 1. + */ +export function fromBase58(address: string): Account { + return fromAccountAddress(AccountAddress.fromBase58(address)); +} + +/** + * Get a base58check string of the token holder account address. + * @param {Account} accountAddress The token holder account. + */ +export function toBase58(accountAddress: Account): string { + return accountAddress.address.address; +} + +/** + * Type predicate which checks if a value is an instance of {@linkcode Type} + */ export function instanceOf(value: unknown): value is Account { return value instanceof TokenHolderAccount; } @@ -79,13 +105,13 @@ export function instanceOf(value: unknown): value is Account { * @returns {Type} The token holder. * @throws {Error} If the token holder type is unsupported. */ -export function fromProto(tokenHolder: Proto.TokenHolder): Type { +export function fromProto(tokenHolder: Proto.TokenHolder): Upward { switch (tokenHolder.address.oneofKind) { case 'account': return fromAccountAddress(AccountAddress.fromProto(tokenHolder.address.account)); // Add other token holder types here as needed case undefined: - throw new Error(`Encountered unsupported token holder type`); + return null; } } @@ -102,214 +128,3 @@ export function toProto(tokenHolder: Type): Proto.TokenHolder { }, }; } - -// CBOR -const TAGGED_ADDRESS = 40307; -const TAGGED_COININFO = 40305; - -/** - * Converts an {@linkcode Account} to a CBOR tagged value. - * This encodes the account address as a CBOR tagged value with tag 40307, containing both - * the coin information (tagged as 40305) and the account's decoded address. - */ -function toCBORValue(value: Account): Tag; -function toCBORValue(value: Type): unknown; -function toCBORValue(value: Type): unknown { - let mapContents: [number, any][]; - if (value.coinInfo === undefined) { - mapContents = [[3, value.address.decodedAddress]]; - } else { - const taggedCoinInfo = new Tag(TAGGED_COININFO, new Map([[1, CCD_NETWORK_ID]])); - mapContents = [ - [1, taggedCoinInfo], - [3, value.address.decodedAddress], - ]; - } - - const map = new Map(mapContents); - return new Tag(TAGGED_ADDRESS, map); -} - -/** - * Converts an TokenHolder to its CBOR (Concise Binary Object Representation) encoding. - * This encodes the account address as a CBOR tagged value with tag 40307, containing both - * the coin information (tagged as 40305) and the account's decoded address. - * - * This corresponds to a concordium-specific subtype of the `tagged-address` type from - * [BCR-2020-009]{@link https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-009-address.md}, - * identified by `tagged-coininfo` corresponding to the Concordium network from - * [BCR-2020-007]{@link https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-007-hdkey.md} - * - * Example of CBOR diagnostic notation for an encoded account address: - * ``` - * 40307({ - * 1: 40305({1: 919}), - * 3: h'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789' - * }) - * ``` - * Where 919 is the Concordium network identifier and the hex string is the raw account address. - * - * @param {Type} value - The token holder to convert to CBOR format. - * @throws {Error} - If an unsupported CBOR encoding is specified. - * @returns {Uint8Array} The CBOR encoded representation of the token holder. - */ -export function toCBOR(value: Type): Uint8Array { - return new Uint8Array(encode(toCBORValue(value))); -} - -/** - * Registers a CBOR encoder for the TokenHolder type with the `cbor2` library. - * This allows TokenHolder instances to be automatically encoded when used with - * the `cbor2` library's encode function. - * - * @returns {void} - * @example - * // Register the encoder - * registerCBOREncoder(); - * // Now TokenHolder instances can be encoded directly - * const encoded = encode(myTokenHolder); - */ -export function registerCBOREncoder(): void { - registerEncoder(TokenHolderAccount, (value) => [TAGGED_ADDRESS, toCBORValue(value).contents]); -} - -/** - * Decodes a CBOR-encoded token holder account into an {@linkcode Account} instance. - * @param {unknown} decoded - The CBOR decoded value, expected to be a tagged value with tag 40307. - * @throws {Error} - If the decoded value is not a valid CBOR encoded token holder account. - * @returns {Account} The decoded account address as a TokenHolderAccount instance. - */ -function fromCBORValueAccount(decoded: unknown): TokenHolderAccount { - // Verify we have a tagged value with tag 40307 (tagged-address) - if (!(decoded instanceof Tag) || decoded.tag !== TAGGED_ADDRESS) { - throw new Error(`Invalid CBOR encoded token holder account: expected tag ${TAGGED_ADDRESS}`); - } - - const value = decoded.contents; - - if (!(value instanceof Map)) { - throw new Error('Invalid CBOR encoded token holder account: expected a map'); - } - - // Verify the map corresponds to the BCR-2020-009 `address` format - const validKeys = [1, 2, 3]; // we allow 2 here, as it is in the spec for BCR-2020-009 `address`, but we don't use it - for (const key of value.keys()) { - validKeys.includes(key) || bail(`Invalid CBOR encoded token holder account: unexpected key ${key}`); - } - - // Extract the token holder account bytes (key 3) - const addressBytes = value.get(3); - if ( - !addressBytes || - !(addressBytes instanceof Uint8Array) || - addressBytes.byteLength !== AccountAddress.BYTES_LENGTH - ) { - throw new Error('Invalid CBOR encoded token holder account: missing or invalid address bytes'); - } - - // Optional validation for coin information if present (key 1) - const coinInfo = value.get(1); - let coinInfoValue = undefined; - if (coinInfo !== undefined) { - // Verify coin info has the correct tag if present - if (!(coinInfo instanceof Tag) || coinInfo.tag !== TAGGED_COININFO) { - throw new Error( - `Invalid CBOR encoded token holder account: coin info has incorrect tag (expected ${TAGGED_COININFO})` - ); - } - - // Verify coin info contains Concordium network identifier if present - const coinInfoMap = coinInfo.contents; - if (!(coinInfoMap instanceof Map) || coinInfoMap.get(1) !== CCD_NETWORK_ID) { - throw new Error( - `Invalid CBOR token holder account: coin info does not contain Concordium network identifier ${CCD_NETWORK_ID}` - ); - } - coinInfoValue = coinInfoMap.get(1); - - // Verify the map corresponds to the BCR-2020-007 `coininfo` format - const validKeys = [1, 2]; // we allow 2 here, as it is in the spec for BCR-2020-007 `coininfo`, but we don't use it - for (const key of coinInfoMap.keys()) { - validKeys.includes(key) || bail(`Invalid CBOR encoded coininfo: unexpected key ${key}`); - } - } - - // Create the AccountAddress from the extracted bytes - return new TokenHolderAccount(AccountAddress.fromBuffer(addressBytes), coinInfoValue); -} - -/** - * Decodes a CBOR value into a TokenHolder instance. - * This function checks if the value is a tagged address (40307) and decodes it accordingly. - * - * @param {unknown} value - The CBOR decoded value, expected to be a tagged address. - * @throws {Error} - If the value is not a valid CBOR encoded token holder account. - * @returns {Type} The decoded TokenHolder instance. - */ -export function fromCBORValue(value: unknown): Type { - if (value instanceof Tag && value.tag === TAGGED_ADDRESS) { - return fromCBORValueAccount(value); - } - - throw new Error(`Failed to decode 'TokenHolder.Type' from CBOR value: ${value}`); -} - -/** - * Decodes a CBOR-encoded account address into an TokenHolder instance. - * This function can handle both the full tagged format (with coin information) - * and a simplified format with just the address bytes. - * - * 1. With `tagged-coininfo` (40305): - * ``` - * 40307({ - * 1: 40305({1: 919}), // Optional coin information - * 3: h'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789' - * }) - * ``` - * - * 2. Without `tagged-coininfo`: - * ``` - * 40307({ - * 3: h'abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789' - * }) // The address is assumed to be a Concordium address - * ``` - * - * @param {Uint8Array} bytes - The CBOR encoded representation of an account address. - * @throws {Error} - If the input is not a valid CBOR encoding of an account address. - * @returns {Type} The decoded TokenHolder instance. - */ -export function fromCBOR(bytes: Uint8Array): Type { - return fromCBORValue(decode(bytes)); -} - -/** - * Registers a CBOR decoder for the tagged-address (40307) format with the `cbor2` library. - * This enables automatic decoding of CBOR data containing Concordium account addresses - * when using the `cbor2` library's decode function. - * - * @returns {() => void} A cleanup function that, when called, will restore the previous - * decoder (if any) that was registered for the tagged-address format. This is useful - * when used in an existing `cbor2` use-case. - * - * @example - * // Register the decoder - * const cleanup = registerCBORDecoder(); - * // Use the decoder - * const tokenHolder = decode(cborBytes); // Returns TokenHolder if format matches - * // Later, unregister the decoder - * cleanup(); - */ -export function registerCBORDecoder(): () => void { - const old = [Tag.registerDecoder(TAGGED_ADDRESS, fromCBORValue)]; - - // return cleanup function to restore the old decoder - return () => { - for (const decoder of old) { - if (decoder) { - Tag.registerDecoder(TAGGED_ADDRESS, decoder); - } else { - Tag.clearDecoder(TAGGED_ADDRESS); - } - } - }; -} diff --git a/packages/sdk/src/plt/TokenModuleEvent.ts b/packages/sdk/src/plt/TokenModuleEvent.ts new file mode 100644 index 000000000..d584e73f0 --- /dev/null +++ b/packages/sdk/src/plt/TokenModuleEvent.ts @@ -0,0 +1,140 @@ +import { EncodedTokenModuleEvent, TransactionEventTag } from '../types.js'; +import { cborDecode } from '../types/cbor.js'; +import { TokenOperationType } from './TokenOperation.js'; +import { CborAccountAddress, TokenId } from './index.js'; + +type GenTokenModuleEvent = { + /** The tag of the event. */ + tag: TransactionEventTag.TokenModuleEvent; + /** The ID of the token. */ + tokenId: TokenId.Type; + /** The type of the event. */ + type: E; + /** The details of the event. */ + details: T; +}; + +/** + * Represents a token module event (found when decoding) unknown to the SDK. + */ +export type UnknownTokenModuleEvent = { + /** The tag of the event. */ + tag: TransactionEventTag.TokenModuleEvent; + /** The ID of the token. */ + tokenId: TokenId.Type; + /** The type of the event. */ + type: string; + /** The details of the event. */ + details: unknown; +}; + +/** + * The structure of any list update event for a PLT. + */ +export type TokenListUpdateEventDetails = { + /** The target of the list update. */ + target: CborAccountAddress.Type; +}; + +/** + * The structure of a pause event for a PLT. + */ +export type TokenPauseEventDetails = {}; + +export type TokenEventDetails = TokenListUpdateEventDetails | TokenPauseEventDetails; + +/** + * An event occuring as the result of an "addAllowList" operation. + */ +export type TokenAddAllowListEvent = GenTokenModuleEvent; +/** + * An event occuring as the result of an "addDenyList" operation. + */ +export type TokenAddDenyListEvent = GenTokenModuleEvent; +/** + * An event occuring as the result of an "removeAllowList" operation. + */ +export type TokenRemoveAllowListEvent = GenTokenModuleEvent< + TokenOperationType.RemoveAllowList, + TokenListUpdateEventDetails +>; +/** + * An event occuring as the result of an "removeDenyList" operation. + */ +export type TokenRemoveDenyListEvent = GenTokenModuleEvent< + TokenOperationType.RemoveDenyList, + TokenListUpdateEventDetails +>; + +/** + * An event occuring as the result of a "pause" operation, describing whether execution + * of the associated token operations are paused or not. + */ +export type TokenPauseEvent = GenTokenModuleEvent; + +/** + * An event occuring as the result of a "pause" operation, describing whether execution + * of the associated token operations are paused or not. + */ +export type TokenUnpauseEvent = GenTokenModuleEvent; + +/** + * A union of all token module events. + */ +export type TokenModuleEvent = + | TokenAddAllowListEvent + | TokenAddDenyListEvent + | TokenRemoveAllowListEvent + | TokenRemoveDenyListEvent + | TokenPauseEvent + | TokenUnpauseEvent; + +function parseTokenListUpdateEventDetails(decoded: unknown): TokenListUpdateEventDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid event details: ${JSON.stringify(decoded)}. Expected an object.`); + } + if (!('target' in decoded && CborAccountAddress.instanceOf(decoded.target))) { + throw new Error(`Invalid event details: ${JSON.stringify(decoded)}. Expected 'target' to be a TokenHolder`); + } + + return decoded as TokenListUpdateEventDetails; +} + +function parseTokenPauseEventDetails(decoded: unknown): TokenPauseEventDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid event details: ${JSON.stringify(decoded)}. Expected an object.`); + } + + return decoded as TokenPauseEventDetails; +} + +/** + * Parses a token module event, decoding the details from CBOR format. + * + * @param event - The token module event to parse. + * @returns The parsed token module event with decoded details. + * + * @example + * const parsedEvent = parseTokenModuleEvent(encodedEvent); + * switch (parsedEvent.type) { + * // typed details are now available, e.g.: + * case TokenOperationType.AddAllowList: console.log(parsedEvent.details.target); + * ... + * default: console.warn('Unknown event encountered:', parsedEvent); + * } + */ +export function parseTokenModuleEvent(event: EncodedTokenModuleEvent): TokenModuleEvent | UnknownTokenModuleEvent { + const decoded = cborDecode(event.details.bytes); + switch (event.type) { + case TokenOperationType.AddAllowList: + case TokenOperationType.RemoveAllowList: + case TokenOperationType.AddDenyList: + case TokenOperationType.RemoveDenyList: + return { ...event, type: event.type, details: parseTokenListUpdateEventDetails(decoded) }; + case TokenOperationType.Pause: + case TokenOperationType.Unpause: + return { ...event, type: event.type, details: parseTokenPauseEventDetails(decoded) }; + default: + return { ...event, details: decoded }; + } +} diff --git a/packages/sdk/src/plt/TokenModuleRejectReason.ts b/packages/sdk/src/plt/TokenModuleRejectReason.ts new file mode 100644 index 000000000..913bec539 --- /dev/null +++ b/packages/sdk/src/plt/TokenModuleRejectReason.ts @@ -0,0 +1,323 @@ +import { cborDecode } from '../types/cbor.js'; +import { CborAccountAddress, TokenAmount } from './index.js'; +import { EncodedTokenModuleRejectReason } from './types.js'; + +export enum TokenRejectReasonType { + AddressNotFound = 'addressNotFound', + TokenBalanceInsufficient = 'tokenBalanceInsufficient', + DeserializationFailure = 'deserializationFailure', + UnsupportedOperation = 'unsupportedOperation', + OperationNotPermitted = 'operationNotPermitted', + MintWouldOverflow = 'mintWouldOverflow', +} + +type RejectReasonGen = Omit & { + /** The type of rejection. */ + type: T; + /** Additional details about the rejection. */ + details: D; +}; + +/** + * Represents a token module reject reason (found when decoding) unknown to the SDK. + */ +export type UnknownTokenRejectReason = Omit & { + /** Additional details about the rejection. */ + details: unknown; +}; + +/** + * The details of an "addressNotFound": an account address was not valid. + */ +export type AddressNotFoundDetails = { + /** The index in the list of operations of the failing operation. */ + index: number; + /** The address that could not be resolved. */ + address: CborAccountAddress.Type; +}; + +/** + * An account address was not valid. + */ +export type AddressNotFoundRejectReason = RejectReasonGen< + TokenRejectReasonType.AddressNotFound, + AddressNotFoundDetails +>; + +/** + * Details for a reject reason where the account's token balance is insufficient + * for the attempted operation. + * + * See CIS-7: reject-reasons/tokenBalanceInsufficient + */ +export type TokenBalanceInsufficientDetails = { + /** The index in the list of operations of the failing operation. */ + index: number; + /** The available balance for the sender at the time of the operation. */ + availableBalance: TokenAmount.Type; + /** The minimum required balance to perform the operation. */ + requiredBalance: TokenAmount.Type; +}; + +/** Typed reject reason for "tokenBalanceInsufficient". */ +export type TokenBalanceInsufficientRejectReason = RejectReasonGen< + TokenRejectReasonType.TokenBalanceInsufficient, + TokenBalanceInsufficientDetails +>; + +/** + * Details for a reject reason where the operation payload could not be deserialized. + * + * See CIS-7: reject-reasons/deserializationFailure + */ +export type DeserializationFailureDetails = { + /** Text description of the failure mode. */ + cause?: string; +}; + +/** Typed reject reason for "deserializationFailure". */ +export type DeserializationFailureRejectReason = RejectReasonGen< + TokenRejectReasonType.DeserializationFailure, + DeserializationFailureDetails +>; + +/** + * Details for a reject reason where the specified operation is not supported by the module. + * + * See CIS-7: reject-reasons/unsupportedOperation + */ +export type UnsupportedOperationDetails = { + /** The index in the list of operations of the failing operation. */ + index: number; + /** The type of operation that was not supported. */ + operationType: string; + /** The reason why the operation was not supported. */ + reason?: string; +}; + +/** Typed reject reason for "unsupportedOperation". */ +export type UnsupportedOperationRejectReason = RejectReasonGen< + TokenRejectReasonType.UnsupportedOperation, + UnsupportedOperationDetails +>; + +/** + * Details for a reject reason where the operation is recognized but not permitted + * under the current state or policy (e.g., paused, allow/deny list). + * + * See CIS-7: reject-reasons/operationNotPermitted + */ +export type OperationNotPermittedDetails = { + /** The index in the list of operations of the failing operation. */ + index: number; + /** (Optionally) the address that does not have the necessary permissions to perform the operation. */ + address?: CborAccountAddress.Type; + /** The reason why the operation is not permitted. */ + reason?: string; +}; + +/** Typed reject reason for "operationNotPermitted". */ +export type OperationNotPermittedRejectReason = RejectReasonGen< + TokenRejectReasonType.OperationNotPermitted, + OperationNotPermittedDetails +>; + +/** + * Details for a reject reason where minting would overflow supply constraints. + * + * See CIS-7: reject-reasons/mintWouldOverflow + */ +export type MintWouldOverflowDetails = { + /** The index in the list of operations of the failing operation. */ + index: number; + /** The requested amount to mint. */ + requestedAmount: TokenAmount.Type; + /** The current supply of the token. */ + currentSupply: TokenAmount.Type; + /** The maximum representable token amount. */ + maxRepresentableAmount: TokenAmount.Type; +}; + +/** Typed reject reason for "mintWouldOverflow". */ +export type MintWouldOverflowRejectReason = RejectReasonGen< + TokenRejectReasonType.MintWouldOverflow, + MintWouldOverflowDetails +>; + +/** + * Union of all token module reject reasons defined by CIS-7, + * with strongly-typed details per reason. + * + * @see https://proposals.concordium.com/CIS/cis-7.html#reject-reasons + */ +export type TokenModuleRejectReason = + | AddressNotFoundRejectReason + | TokenBalanceInsufficientRejectReason + | DeserializationFailureRejectReason + | UnsupportedOperationRejectReason + | OperationNotPermittedRejectReason + | MintWouldOverflowRejectReason; + +function parseAddressNotFound(decoded: unknown): AddressNotFoundDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected an object.`); + } + // required + if (!('index' in decoded) || typeof decoded.index !== 'number') { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected 'index' to be a number`); + } + // required + if (!('address' in decoded) || !CborAccountAddress.instanceOf(decoded.address)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'address' to be a CborAccountAddress` + ); + } + + return decoded as AddressNotFoundDetails; +} + +function parseMintWouldOverflow(decoded: unknown): MintWouldOverflowDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected an object.`); + } + // required + if (!('index' in decoded) || typeof decoded.index !== 'number') { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected 'index' to be a number`); + } + // required + if (!('requestedAmount' in decoded) || !TokenAmount.instanceOf(decoded.requestedAmount)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'requestedAmount' to be a TokenAmount` + ); + } + // required + if (!('currentSupply' in decoded) || !TokenAmount.instanceOf(decoded.currentSupply)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'currentSupply' to be a TokenAmount` + ); + } + // required + if (!('maxRepresentableAmount' in decoded) || !TokenAmount.instanceOf(decoded.maxRepresentableAmount)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'maxRepresentableAmount' to be a TokenAmount` + ); + } + return decoded as MintWouldOverflowDetails; +} + +function parseTokenBalanceInsufficient(decoded: unknown): TokenBalanceInsufficientDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected an object.`); + } + // required + if (!('index' in decoded) || typeof decoded.index !== 'number') { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected 'index' to be a number`); + } + // required + if (!('availableBalance' in decoded) || !TokenAmount.instanceOf(decoded.availableBalance)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'availableBalance' to be a TokenAmount` + ); + } + // required + if (!('requiredBalance' in decoded) || !TokenAmount.instanceOf(decoded.requiredBalance)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'requiredBalance' to be a TokenAmount` + ); + } + return decoded as TokenBalanceInsufficientDetails; +} + +function parseDeserializationFailure(decoded: unknown): DeserializationFailureDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected an object.`); + } + // optional + if ('cause' in decoded && typeof decoded.cause !== 'string') { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'cause' to be a string if present` + ); + } + return decoded as DeserializationFailureDetails; +} + +function parseUnsupportedOperation(decoded: unknown): UnsupportedOperationDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected an object.`); + } + // required + if (!('index' in decoded) || typeof decoded.index !== 'number') { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected 'index' to be a number`); + } + // required + if (!('operationType' in decoded) || typeof decoded.operationType !== 'string') { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected 'operationType' to be a string`); + } + // optional + if ('reason' in decoded && typeof decoded.reason !== 'string') { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'reason' to be a string if present` + ); + } + return decoded as UnsupportedOperationDetails; +} + +function parseOperationNotPermitted(decoded: unknown): OperationNotPermittedDetails { + if (typeof decoded !== 'object' || decoded === null) { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected an object.`); + } + // required + if (!('index' in decoded) || typeof decoded.index !== 'number') { + throw new Error(`Invalid reason details: ${JSON.stringify(decoded)}. Expected 'index' to be a number`); + } + // optional + if ('address' in decoded && !CborAccountAddress.instanceOf(decoded.address)) { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'address' to be a CborAccountAddress if present` + ); + } + // optional + if ('reason' in decoded && typeof decoded.reason !== 'string') { + throw new Error( + `Invalid reason details: ${JSON.stringify(decoded)}. Expected 'reason' to be a string if present` + ); + } + return decoded as OperationNotPermittedDetails; +} + +/** + * Parses a token module reject reason, decoding the details from CBOR format. + * + * @param rejectReason - The token module reject reason to parse. + * @returns The parsed token module reject reason with decoded details. + * + * @example + * const parsedReason = parseTokenModuleRejectReason(encodedReason); + * switch (parsedReason.type) { + * // typed details are now available, e.g.: + * case TokenRejectReasonType.MintWouldOverflow: console.log(parsedReason.requestedAmount); + * ... + * default: console.warn('Unknown reject reason:', parsedReason); + * } + */ +export function parseTokenModuleRejectReason( + rejectReason: EncodedTokenModuleRejectReason +): TokenModuleRejectReason | UnknownTokenRejectReason { + const decoded = cborDecode(rejectReason.details.bytes); + switch (rejectReason.type) { + case TokenRejectReasonType.AddressNotFound: + return { ...rejectReason, type: rejectReason.type, details: parseAddressNotFound(decoded) }; + case TokenRejectReasonType.MintWouldOverflow: + return { ...rejectReason, type: rejectReason.type, details: parseMintWouldOverflow(decoded) }; + case TokenRejectReasonType.TokenBalanceInsufficient: + return { ...rejectReason, type: rejectReason.type, details: parseTokenBalanceInsufficient(decoded) }; + case TokenRejectReasonType.DeserializationFailure: + return { ...rejectReason, type: rejectReason.type, details: parseDeserializationFailure(decoded) }; + case TokenRejectReasonType.UnsupportedOperation: + return { ...rejectReason, type: rejectReason.type, details: parseUnsupportedOperation(decoded) }; + case TokenRejectReasonType.OperationNotPermitted: + return { ...rejectReason, type: rejectReason.type, details: parseOperationNotPermitted(decoded) }; + default: + return { ...rejectReason, details: decoded }; + } +} diff --git a/packages/sdk/src/plt/TokenOperation.ts b/packages/sdk/src/plt/TokenOperation.ts new file mode 100644 index 000000000..f6336ce59 --- /dev/null +++ b/packages/sdk/src/plt/TokenOperation.ts @@ -0,0 +1,300 @@ +import { TokenUpdatePayload } from '../types.js'; +import { Cbor, CborAccountAddress, CborMemo, TokenAmount, TokenId } from './index.js'; + +/** + * Enum representing the types of token operations. + */ +export enum TokenOperationType { + Transfer = 'transfer', + Mint = 'mint', + Burn = 'burn', + AddAllowList = 'addAllowList', + RemoveAllowList = 'removeAllowList', + AddDenyList = 'addDenyList', + RemoveDenyList = 'removeDenyList', + Pause = 'pause', + Unpause = 'unpause', +} + +export type Memo = CborMemo.Type | Uint8Array; + +/** + * The structure of a PLT transfer. + */ +export type TokenTransfer = { + /** The amount to transfer. */ + amount: TokenAmount.Type; + /** The recipient of the transfer. */ + recipient: CborAccountAddress.Type; + /** An optional memo for the transfer. A string will be CBOR encoded, while raw bytes are included in the + * transaction as is. */ + memo?: Memo; +}; + +/** + * Generic type for a token operation. + * @template TokenOperationType - The type of the token operation. + * @template T - The specific operation details. + */ +type TokenOperationGen = { + [K in Type]: T; +}; + +/** + * Represents a token transfer operation. + */ +export type TokenTransferOperation = TokenOperationGen; + +/** + * The structure of a PLT mint/burn operation. + */ +export type TokenSupplyUpdate = { + /** The amount to mint/burn. */ + amount: TokenAmount.Type; +}; + +/** + * Represents a token mint operation. + */ +export type TokenMintOperation = TokenOperationGen; + +/** + * Represents a token burn operation. + */ +export type TokenBurnOperation = TokenOperationGen; + +/** + * The structure of any list update operation for a PLT. + */ +export type TokenListUpdate = { + /** The target of the list update. */ + target: CborAccountAddress.Type; +}; + +/** + * Represents an operation to add an account to the allow list. + */ +export type TokenAddAllowListOperation = TokenOperationGen; + +/** + * Represents an operation to remove an account from the allow list. + */ +export type TokenRemoveAllowListOperation = TokenOperationGen; + +/** + * Represents an operation to add an account to the deny list. + */ +export type TokenAddDenyListOperation = TokenOperationGen; + +/** + * Represents an operation to remove an account from the deny list. + */ +export type TokenRemoveDenyListOperation = TokenOperationGen; + +/** + * Represents an operation to pause the execution any operation that involves token balance + * changes. + */ +export type TokenPauseOperation = TokenOperationGen; + +/** + * Represents an operation to unpause the execution any operation that involves token balance + * changes. + */ +export type TokenUnpauseOperation = TokenOperationGen; + +/** + * Union type representing all possible operations for a token. + */ +export type TokenOperation = + | TokenTransferOperation + | TokenMintOperation + | TokenBurnOperation + | TokenAddAllowListOperation + | TokenRemoveAllowListOperation + | TokenAddDenyListOperation + | TokenRemoveDenyListOperation + | TokenPauseOperation + | TokenUnpauseOperation; + +/** + * Creates a payload for token operations. + * This function encodes the provided token operation(s) into a CBOR format. + * + * @param tokenId - The unique identifier of the token for which the operation(s) is being performed. + * @param operations - A single token operation or an array of token operations. + * + * @returns The encoded token governance payload. + */ +export function createTokenUpdatePayload( + tokenId: TokenId.Type, + operations: TokenOperation | TokenOperation[] +): TokenUpdatePayload { + const ops = [operations].flat(); + return { + tokenId: tokenId, + operations: Cbor.encode(ops), + }; +} + +/** + * Represents a token operation (found when decoding) unknown to the SDK. + */ +export type UnknownTokenOperation = { [key: string]: unknown }; + +function parseTransfer(details: unknown): TokenTransfer { + if (typeof details !== 'object' || details === null) + throw new Error(`Invalid transfer details: ${JSON.stringify(details)}. Expected an object.`); + if (!('amount' in details) || !TokenAmount.instanceOf(details.amount)) + throw new Error(`Invalid transfer details: ${JSON.stringify(details)}. Expected 'amount' to be a TokenAmount`); + if (!('recipient' in details) || !CborAccountAddress.instanceOf(details.recipient)) + throw new Error( + `Invalid transfer details: ${JSON.stringify(details)}. Expected 'recipient' to be a TokenHolder` + ); + if ('memo' in details && !(details.memo instanceof Uint8Array || CborMemo.instanceOf(details.memo))) + throw new Error( + `Invalid transfer details: ${JSON.stringify(details)}. Expected 'memo' to be Uint8Array | CborMemo` + ); + return details as TokenTransfer; +} + +function parseSupplyUpdate(details: unknown): TokenSupplyUpdate { + if (typeof details !== 'object' || details === null) { + throw new Error(`Invalid supply update details: ${JSON.stringify(details)}. Expected an object.`); + } + if (!('amount' in details) || !TokenAmount.instanceOf(details.amount)) + throw new Error( + `Invalid supply update details: ${JSON.stringify(details)}. Expected 'amount' to be a TokenAmount` + ); + return details as TokenSupplyUpdate; +} + +function parseListUpdate(details: unknown): TokenListUpdate { + if (typeof details !== 'object' || details === null) + throw new Error(`Invalid list update details: ${JSON.stringify(details)}. Expected an object.`); + if (!('target' in details) || !CborAccountAddress.instanceOf(details.target)) + throw new Error( + `Invalid list update details: ${JSON.stringify(details)}. Expected 'target' to be a TokenHolder` + ); + return details as TokenListUpdate; +} + +function parseEmpty(details: unknown): {} { + if (typeof details !== 'object' || details === null || Object.keys(details as object).length !== 0) + throw new Error(`Invalid operation details: ${JSON.stringify(details)}. Expected empty object {}`); + return details; +} + +/** + * Decode a single token operation from CBOR. Throws on invalid shapes, only returns Unknown variant when the key is unrecognized. + */ +function parseTokenOperation(decoded: unknown): TokenOperation | UnknownTokenOperation { + if (typeof decoded !== 'object' || decoded === null) + throw new Error(`Invalid token operation: ${JSON.stringify(decoded)}. Expected an object.`); + + const keys = Object.keys(decoded); + if (keys.length !== 1) + throw new Error( + `Invalid token operation: ${JSON.stringify(decoded)}. Expected an object with a single key identifying the operation type.` + ); + + const type = keys[0]; + const details = (decoded as Record)[type]; + switch (type) { + case TokenOperationType.Transfer: + return { [type]: parseTransfer(details) }; + case TokenOperationType.Mint: + return { [type]: parseSupplyUpdate(details) }; + case TokenOperationType.Burn: + return { [type]: parseSupplyUpdate(details) }; + case TokenOperationType.AddAllowList: + return { [type]: parseListUpdate(details) }; + case TokenOperationType.RemoveAllowList: + return { [type]: parseListUpdate(details) }; + case TokenOperationType.AddDenyList: + return { [type]: parseListUpdate(details) }; + case TokenOperationType.RemoveDenyList: + return { [type]: parseListUpdate(details) }; + case TokenOperationType.Pause: + return { [type]: parseEmpty(details) }; + case TokenOperationType.Unpause: + return { [type]: parseEmpty(details) }; + default: + return decoded as UnknownTokenOperation; + } +} + +/** + * Decodes a token operation. + * + * @param cbor - The CBOR encoding to decode. + * @returns The decoded token operation. + * + * @example + * const op = decodeTokenOperation(cbor); + * switch (true) { + * case TokenOperationType.Transfer in op: { + * const details = op[TokenOperationType.Transfer]; // type is known at this point. + * console.log(details); + * } + * ... + * default: console.warn('Unknown operation', op); + * } + */ +export function decodeTokenOperation(cbor: Cbor.Type): TokenOperation | UnknownTokenOperation { + const decoded = Cbor.decode(cbor); + return parseTokenOperation(decoded); +} + +/** + * Decodes a list of token operations. + * + * @param cbor - The CBOR encoding to decode. + * @returns The decoded token operations. + * + * @example + * const ops = decodeTokenOperations(cbor); + * ops.forEach(op => { + * switch (true) { + * case TokenOperationType.Transfer in op: { + * const details = op[TokenOperationType.Transfer]; // type is known at this point. + * console.log(details); + * } + * ... + * default: console.warn('Unknown operation', op); + * } + * }); + */ +export function decodeTokenOperations(cbor: Cbor.Type): (TokenOperation | UnknownTokenOperation)[] { + const decoded = Cbor.decode(cbor); + if (!Array.isArray(decoded)) + throw new Error(`Invalid token update operations: ${JSON.stringify(decoded)}. Expected a list of operations.`); + + return decoded.map(parseTokenOperation); +} + +/** + * Parses a token update payload, decoding the operations from CBOR format. + * + * @param payload - The token update payload to parse. + * @returns The parsed token update payload with decoded operations. + * + * @example + * const parsedPayload = parseTokenUpdatePayload(encodedPayload); + * parsedPayload.operations.forEach(op => { + * switch (true) { + * case TokenOperationType.Transfer in op: { + * const details = op[TokenOperationType.Transfer]; // type is known at this point. + * console.log(details); + * } + * ... + * default: console.warn('Unknown operation', op); + * } + * }); + */ +export function parseTokenUpdatePayload( + payload: TokenUpdatePayload +): Omit & { operations: (TokenOperation | UnknownTokenOperation)[] } { + const operations = decodeTokenOperations(payload.operations); + return { ...payload, operations }; +} diff --git a/packages/sdk/src/plt/decode.ts b/packages/sdk/src/plt/decode.ts new file mode 100644 index 000000000..d0718bdb6 --- /dev/null +++ b/packages/sdk/src/plt/decode.ts @@ -0,0 +1,138 @@ +import { cborDecode } from '../types/cbor.js'; +import { + Cbor, + CborAccountAddress, + TokenAmount, + TokenInitializationParameters, + TokenMetadataUrl, + TokenModuleAccountState, + TokenModuleState, + TokenOperation, + UnknownTokenOperation, + decodeTokenOperations, +} from './index.js'; + +function decodeTokenModuleState(value: Cbor.Type): TokenModuleState { + const decoded = cborDecode(value.bytes); + if (typeof decoded !== 'object' || decoded === null) throw new Error('Invalid CBOR data for TokenModuleState'); + + // Validate optional fields + if ('governanceAccount' in decoded && !CborAccountAddress.instanceOf(decoded.governanceAccount)) + throw new Error('Invalid TokenModuleState: missing or invalid governanceAccount'); + + let metadata: TokenMetadataUrl.Type | undefined; + try { + if ('metadata' in decoded) metadata = TokenMetadataUrl.fromCBORValue(decoded.metadata); + } catch { + throw new Error('Invalid TokenModuleState: invalid metadata'); + } + + if ('name' in decoded && typeof decoded.name !== 'string') + throw new Error('Invalid TokenModuleState: invalid name'); + if ('allowList' in decoded && typeof decoded.allowList !== 'boolean') + throw new Error('Invalid TokenModuleState: allowList must be a boolean'); + if ('denyList' in decoded && typeof decoded.denyList !== 'boolean') + throw Error('Invalid TokenModuleState: denyList must be a boolean'); + if ('mintable' in decoded && typeof decoded.mintable !== 'boolean') + throw new Error('Invalid TokenModuleState: mintable must be a boolean'); + if ('burnable' in decoded && typeof decoded.burnable !== 'boolean') + throw new Error('Invalid TokenModuleState: burnable must be a boolean'); + if ('paused' in decoded && typeof decoded.paused !== 'boolean') + throw new Error('Invalid TokenModuleState: paused must be a boolean'); + + return { ...decoded, metadata } as TokenModuleState; +} + +function decodeTokenModuleAccountState(value: Cbor.Type): TokenModuleAccountState { + const decoded = cborDecode(value.bytes); + if (typeof decoded !== 'object' || decoded === null) { + throw new Error('Invalid CBOR data for TokenModuleAccountState'); + } + + // Validate optional fields + if ('allowList' in decoded && typeof decoded.allowList !== 'boolean') { + throw new Error('Invalid TokenModuleState: allowList must be a boolean'); + } + if ('denyList' in decoded && typeof decoded.denyList !== 'boolean') { + throw Error('Invalid TokenModuleState: denyList must be a boolean'); + } + + return decoded as TokenModuleAccountState; +} + +function decodeTokenInitializationParameters(value: Cbor.Type): TokenInitializationParameters { + const decoded = cborDecode(value.bytes); + if (typeof decoded !== 'object' || decoded === null) { + throw new Error('Invalid CBOR data for TokenInitializationParameters'); + } + + // Validate optional fields + if ('governanceAccount' in decoded && !CborAccountAddress.instanceOf(decoded.governanceAccount)) + throw new Error('Invalid TokenModuleState: invalid governanceAccount'); + + let metadata: TokenMetadataUrl.Type | undefined; + try { + if ('metadata' in decoded) metadata = TokenMetadataUrl.fromCBORValue(decoded.metadata); + } catch { + throw new Error('Invalid TokenModuleState: invalid metadata'); + } + + if ('allowList' in decoded && typeof decoded.allowList !== 'boolean') + throw new Error('Invalid TokenInitializationParameters: allowList must be a boolean'); + if ('denyList' in decoded && typeof decoded.denyList !== 'boolean') + throw Error('Invalid TokenInitializationParameters: denyList must be a boolean'); + if ('mintable' in decoded && typeof decoded.mintable !== 'boolean') + throw new Error('Invalid TokenInitializationParameters: mintable must be a boolean'); + if ('burnable' in decoded && typeof decoded.burnable !== 'boolean') + throw new Error('Invalid TokenInitializationParameters: burnable must be a boolean'); + if ('paused' in decoded && typeof decoded.paused !== 'boolean') + throw new Error('Invalid TokenInitializationParameters: paused must be a boolean'); + + // Optional initial supply + if ('initialSupply' in decoded && !TokenAmount.instanceOf(decoded.initialSupply)) + throw new Error(`Invalid TokenInitializationParameters: Expected 'initialSupply' to be of type 'TokenAmount'`); + + return { ...decoded, metadata } as TokenInitializationParameters; +} + +type DecodeTypeMap = { + TokenModuleState: TokenModuleState; + TokenModuleAccountState: TokenModuleAccountState; + TokenInitializationParameters: TokenInitializationParameters; + 'TokenOperation[]': (TokenOperation | UnknownTokenOperation)[]; +}; + +/** + * Decode CBOR encoded data into its original representation. + * @param {Cbor.Type} cbor - The CBOR encoded data. + * @param {string} type - type hint for decoding. + * @returns {unknown} The decoded data. + */ +export function decode(cbor: Cbor.Type, type: T): DecodeTypeMap[T]; +/** + * Decode CBOR encoded data into its original representation. + * @param {Cbor.Type} cbor - The CBOR encoded data. + * @returns {unknown} The decoded data. + */ +export function decode(cbor: Cbor.Type, type?: undefined): unknown; + +/** + * Decode CBOR encoded data into its original representation. + * @param {Cbor.Type} cbor - The CBOR encoded data. + * @param {string | undefined} type - Optional type hint for decoding. + * @returns {unknown} The decoded data. + */ +export function decode(cbor: Cbor.Type, type: T): unknown { + switch (type) { + case 'TokenModuleState': + return decodeTokenModuleState(cbor); + case 'TokenModuleAccountState': + return decodeTokenModuleAccountState(cbor); + case 'TokenInitializationParameters': + return decodeTokenInitializationParameters(cbor); + case 'TokenOperation[]': + return decodeTokenOperations(cbor); + default: + return cborDecode(cbor.bytes); + } +} diff --git a/packages/sdk/src/plt/index.ts b/packages/sdk/src/plt/index.ts index 6094832ea..f196b7a13 100644 --- a/packages/sdk/src/plt/index.ts +++ b/packages/sdk/src/plt/index.ts @@ -1,5 +1,8 @@ export * from './types.js'; export * from './module.js'; +export * from './TokenModuleRejectReason.js'; +export * from './TokenModuleEvent.js'; +export * from './TokenOperation.js'; export * as TokenId from './TokenId.js'; export * as TokenModuleReference from './TokenModuleReference.js'; @@ -8,4 +11,6 @@ export * as TokenAmount from './TokenAmount.js'; export * as Token from './Token.js'; export * as Cbor from './Cbor.js'; export * as CborMemo from './CborMemo.js'; +export * as CborAccountAddress from './CborAccountAddress.js'; export * as TokenHolder from './TokenHolder.js'; +export * as CborContractAddress from './CborContractAddress.js'; diff --git a/packages/sdk/src/plt/module.ts b/packages/sdk/src/plt/module.ts index ef3a07f35..0c97ae564 100644 --- a/packages/sdk/src/plt/module.ts +++ b/packages/sdk/src/plt/module.ts @@ -1,141 +1,5 @@ -import { EncodedTokenModuleEvent, TokenUpdatePayload, TransactionEventTag } from '../types.js'; -import { Cbor, CborMemo, CreatePLTPayload, TokenAmount, TokenHolder, TokenId, TokenMetadataUrl } from './index.js'; - -/** - * Enum representing the types of token operations. - */ -export enum TokenOperationType { - Transfer = 'transfer', - Mint = 'mint', - Burn = 'burn', - AddAllowList = 'addAllowList', - RemoveAllowList = 'removeAllowList', - AddDenyList = 'addDenyList', - RemoveDenyList = 'removeDenyList', - Pause = 'pause', - Unpause = 'unpause', -} - -export type Memo = CborMemo.Type | Uint8Array; - -/** - * The structure of a PLT transfer. - */ -export type TokenTransfer = { - /** The amount to transfer. */ - amount: TokenAmount.Type; - /** The recipient of the transfer. */ - recipient: TokenHolder.Type; - /** An optional memo for the transfer. A string will be CBOR encoded, while raw bytes are included in the - * transaction as is. */ - memo?: Memo; -}; - -/** - * Generic type for a token operation. - * @template TokenOperationType - The type of the token operation. - * @template T - The specific operation details. - */ -type TokenOperationGen = { - [K in Type]: T; -}; - -/** - * Represents a token transfer operation. - */ -export type TokenTransferOperation = TokenOperationGen; - -/** - * The structure of a PLT mint/burn operation. - */ -export type TokenSupplyUpdate = { - /** The amount to mint/burn. */ - amount: TokenAmount.Type; -}; - -/** - * Represents a token mint operation. - */ -export type TokenMintOperation = TokenOperationGen; - -/** - * Represents a token burn operation. - */ -export type TokenBurnOperation = TokenOperationGen; - -/** - * The structure of any list update operation for a PLT. - */ -export type TokenListUpdate = { - /** The target of the list update. */ - target: TokenHolder.Type; -}; - -/** - * Represents an operation to add an account to the allow list. - */ -export type TokenAddAllowListOperation = TokenOperationGen; - -/** - * Represents an operation to remove an account from the allow list. - */ -export type TokenRemoveAllowListOperation = TokenOperationGen; - -/** - * Represents an operation to add an account to the deny list. - */ -export type TokenAddDenyListOperation = TokenOperationGen; - -/** - * Represents an operation to remove an account from the deny list. - */ -export type TokenRemoveDenyListOperation = TokenOperationGen; - -/** - * Represents an operation to pause the execution any operation that involves token balance - * changes. - */ -export type TokenPauseOperation = TokenOperationGen; - -/** - * Represents an operation to unpause the execution any operation that involves token balance - * changes. - */ -export type TokenUnpauseOperation = TokenOperationGen; - -/** - * Union type representing all possible operations for a token. - */ -export type TokenOperation = - | TokenTransferOperation - | TokenMintOperation - | TokenBurnOperation - | TokenAddAllowListOperation - | TokenRemoveAllowListOperation - | TokenAddDenyListOperation - | TokenRemoveDenyListOperation - | TokenPauseOperation - | TokenUnpauseOperation; - -/** - * Creates a payload for token operations. - * This function encodes the provided token operation(s) into a CBOR format. - * - * @param tokenId - The unique identifier of the token for which the operation(s) is being performed. - * @param operations - A single token operation or an array of token operations. - * - * @returns The encoded token governance payload. - */ -export function createTokenUpdatePayload( - tokenId: TokenId.Type, - operations: TokenOperation | TokenOperation[] -): TokenUpdatePayload { - const ops = [operations].flat(); - return { - tokenId: tokenId, - operations: Cbor.encode(ops), - }; -} +import { MAX_U8 } from '../constants.js'; +import { Cbor, CborAccountAddress, CreatePLTPayload, TokenAmount, TokenMetadataUrl } from './index.js'; /** * The Token Module state represents global state information that is maintained by the Token Module, @@ -152,11 +16,11 @@ export function createTokenUpdatePayload( */ export type TokenModuleState = { /** The name of the token. */ - name: string; + name?: string; /** A URL pointing to the metadata of the token. */ - metadata: TokenMetadataUrl.Type; + metadata?: TokenMetadataUrl.Type; /** The governance account for the token. */ - governanceAccount: TokenHolder.Type; + governanceAccount?: CborAccountAddress.Type; /** Whether the token supports an allow list */ allowList?: boolean; /** Whether the token supports an deny list */ @@ -198,11 +62,11 @@ export type TokenModuleAccountState = { */ export type TokenInitializationParameters = { /** The name of the token. */ - name: string; + name?: string; /** A URL pointing to the metadata of the token. */ - metadata: TokenMetadataUrl.Type; + metadata?: TokenMetadataUrl.Type; /** The governance account for the token. */ - governanceAccount: TokenHolder.Type; + governanceAccount?: CborAccountAddress.Type; /** Whether the token supports an allow list */ allowList?: boolean; /** Whether the token supports an deny list */ @@ -226,125 +90,11 @@ export function createPltPayload( payload: Omit, params: TokenInitializationParameters ): CreatePLTPayload { + if (payload.decimals < 0 || payload.decimals > MAX_U8) { + throw new Error('Token decimals must be in the range 0..255 (inclusive).'); + } return { ...payload, initializationParameters: Cbor.encode(params), }; } - -type GenericTokenModuleEvent = { - /** The tag of the event. */ - tag: TransactionEventTag.TokenModuleEvent; - /** The ID of the token. */ - tokenId: TokenId.Type; - /** The type of the event. */ - type: E; - /** The details of the event. */ - details: T; -}; - -/** - * The structure of any list update event for a PLT. - */ -export type TokenListUpdateEventDetails = { - /** The target of the list update. */ - target: TokenHolder.Type; -}; - -/** - * The structure of a pause event for a PLT. - */ -export type TokenPauseEventDetails = {}; - -export type TokenEventDetails = TokenListUpdateEventDetails | TokenPauseEventDetails; - -/** - * An event occuring as the result of an "addAllowList" operation. - */ -export type TokenAddAllowListEvent = GenericTokenModuleEvent< - TokenOperationType.AddAllowList, - TokenListUpdateEventDetails ->; -/** - * An event occuring as the result of an "addDenyList" operation. - */ -export type TokenAddDenyListEvent = GenericTokenModuleEvent< - TokenOperationType.AddDenyList, - TokenListUpdateEventDetails ->; -/** - * An event occuring as the result of an "removeAllowList" operation. - */ -export type TokenRemoveAllowListEvent = GenericTokenModuleEvent< - TokenOperationType.RemoveAllowList, - TokenListUpdateEventDetails ->; -/** - * An event occuring as the result of an "removeDenyList" operation. - */ -export type TokenRemoveDenyListEvent = GenericTokenModuleEvent< - TokenOperationType.RemoveDenyList, - TokenListUpdateEventDetails ->; - -/** - * An event occuring as the result of a "pause" operation, describing whether execution - * of the associated token operations are paused or not. - */ -export type TokenPauseEvent = GenericTokenModuleEvent; - -/** - * An event occuring as the result of a "pause" operation, describing whether execution - * of the associated token operations are paused or not. - */ -export type TokenUnpauseEvent = GenericTokenModuleEvent; - -/** - * A union of all token module events. - */ -export type TokenModuleEvent = - | TokenAddAllowListEvent - | TokenAddDenyListEvent - | TokenRemoveAllowListEvent - | TokenRemoveDenyListEvent - | TokenPauseEvent - | TokenUnpauseEvent; - -/** - * Parses a token module event, decoding the details from CBOR format. If the desired outcome is to be able to handle - * arbitrary token events, it's recommended to use {@link Cbor.decode} instead. - * - * @param event - The token module event to parse. - * @returns The parsed token module event with decoded details. - * @throws {Error} If the event cannot be parsed as a token module event. - * - * @example - * try { - * const parsedEvent = parseModuleEvent(encodedEvent); - * switch (parsedEvent.type) { - * // typed details are now available, e.g.: - * case TokenOperationType.AddAllowList: console.log(parsedEvent.details.target); - * ... - * } - * } catch (error) { - * // Fall back to using Cbor.decode - * const decodedDetails = Cbor.decode(encodedEvent.details); - * switch (encodedEvent.type) { - * // do something with the decoded details - * } - * } - */ -export function parseModuleEvent(event: EncodedTokenModuleEvent): TokenModuleEvent { - switch (event.type) { - case TokenOperationType.AddAllowList: - case TokenOperationType.RemoveAllowList: - case TokenOperationType.AddDenyList: - case TokenOperationType.RemoveDenyList: - return { ...event, type: event.type, details: Cbor.decode(event.details, 'TokenListUpdateEventDetails') }; - case TokenOperationType.Pause: - case TokenOperationType.Unpause: - return { ...event, type: event.type, details: Cbor.decode(event.details, 'TokenPauseEventDetails') }; - default: - throw new Error(`Cannot parse event as token module event: ${event.type}`); - } -} diff --git a/packages/sdk/src/plt/types.ts b/packages/sdk/src/plt/types.ts index 53421b4c0..87907d116 100644 --- a/packages/sdk/src/plt/types.ts +++ b/packages/sdk/src/plt/types.ts @@ -50,7 +50,7 @@ export type TokenAccountInfo = { /** * Represents the reason for a token module operation rejection. */ -export type TokenModuleRejectReason = { +export type EncodedTokenModuleRejectReason = { /** The ID of the token for which the operation was rejected. */ tokenId: TokenId.Type; /** The type of rejection. */ @@ -67,7 +67,8 @@ export type CreatePLTPayload = { /** * The number of decimal places used in the representation of amounts of this token. This determines the smallest * representable fraction of the token. - * This can be at most `255`. + * + * This MUST be an integer in the range `0..255` (inclusive). */ decimals: number; /** The module specific initialization parameters. */ diff --git a/packages/sdk/src/pub/plt.ts b/packages/sdk/src/pub/plt.ts index a8267ff5f..f3bb50f6c 100644 --- a/packages/sdk/src/pub/plt.ts +++ b/packages/sdk/src/pub/plt.ts @@ -1,6 +1,8 @@ import * as Token from '../plt/Token.js'; // To limit the exports meant only for internal use, we re-create the module exports. import * as Cbor from './plt/Cbor.js'; +import * as CborAccountAddress from './plt/CborAccountAddress.js'; +import * as CborContractAddress from './plt/CborContractAddress.js'; import * as CborMemo from './plt/CborMemo.js'; import * as TokenAmount from './plt/TokenAmount.js'; import * as TokenHolder from './plt/TokenHolder.js'; @@ -10,5 +12,19 @@ import * as TokenModuleReference from './plt/TokenModuleReference.js'; export * from '../plt/types.js'; export * from '../plt/module.js'; +export * from '../plt/TokenModuleRejectReason.js'; +export * from '../plt/TokenModuleEvent.js'; +export * from '../plt/TokenOperation.js'; -export { Token, Cbor, TokenAmount, CborMemo, TokenId, TokenModuleReference, TokenMetadataUrl, TokenHolder }; +export { + Token, + Cbor, + TokenAmount, + CborMemo, + TokenId, + TokenModuleReference, + TokenMetadataUrl, + TokenHolder, + CborAccountAddress, + CborContractAddress, +}; diff --git a/packages/sdk/src/pub/plt/CborAccountAddress.ts b/packages/sdk/src/pub/plt/CborAccountAddress.ts new file mode 100644 index 000000000..2d37136f2 --- /dev/null +++ b/packages/sdk/src/pub/plt/CborAccountAddress.ts @@ -0,0 +1,13 @@ +export { + Type, + JSON, + toCBOR, + fromCBOR, + registerCBOREncoder, + registerCBORDecoder, + fromAccountAddress, + instanceOf, + fromJSON, + fromBase58, + toBase58, +} from '../../plt/CborAccountAddress.js'; diff --git a/packages/sdk/src/pub/plt/CborContractAddress.ts b/packages/sdk/src/pub/plt/CborContractAddress.ts new file mode 100644 index 000000000..a78dcbe91 --- /dev/null +++ b/packages/sdk/src/pub/plt/CborContractAddress.ts @@ -0,0 +1,15 @@ +export { + Type, + Err, + ErrorType, + JSON, + fromJSON, + toCBOR, + fromCBOR, + toContractAddress, + fromContractAddress, + create, + instanceOf, + registerCBOREncoder, + registerCBORDecoder, +} from '../../plt/CborContractAddress.js'; diff --git a/packages/sdk/src/pub/plt/TokenHolder.ts b/packages/sdk/src/pub/plt/TokenHolder.ts index ee253805d..e72e6e339 100644 --- a/packages/sdk/src/pub/plt/TokenHolder.ts +++ b/packages/sdk/src/pub/plt/TokenHolder.ts @@ -1,14 +1,11 @@ export { Type, - JSON, - toCBOR, - fromCBOR, - registerCBOREncoder, - registerCBORDecoder, + fromJSON, + instanceOf, fromAccountAddress, - fromAccountAddressNoCoinInfo, + JSON, Account, AccountJSON, - instanceOf, - fromJSON, + fromBase58, + toBase58, } from '../../plt/TokenHolder.js'; diff --git a/packages/sdk/src/serialization.ts b/packages/sdk/src/serialization.ts index 2b3051907..48cd336db 100644 --- a/packages/sdk/src/serialization.ts +++ b/packages/sdk/src/serialization.ts @@ -2,6 +2,7 @@ import { Buffer } from 'buffer/index.js'; import { getAccountTransactionHandler } from './accountTransactions.js'; import { calculateEnergyCost } from './energyCost.js'; +import { Known, isKnown } from './grpc/upward.js'; import { sha256 } from './hash.js'; import { encodeWord8, @@ -195,9 +196,18 @@ export function serializeAccountTransactionForSubmission( * @returns the serialization of CredentialDeploymentValues */ function serializeCredentialDeploymentValues(credential: CredentialDeploymentValues) { + // Check that we don't attempt to serialize unknown variants + if (Object.values(credential.credentialPublicKeys.keys).some((v) => !isKnown(v))) + throw new Error('Cannot serialize unknown key variants'); + const buffers = []; buffers.push( - serializeMap(credential.credentialPublicKeys.keys, encodeWord8, encodeWord8FromString, serializeVerifyKey) + serializeMap( + credential.credentialPublicKeys.keys as Known, + encodeWord8, + encodeWord8FromString, + serializeVerifyKey + ) ); buffers.push(encodeWord8(credential.credentialPublicKeys.threshold)); diff --git a/packages/sdk/src/signHelpers.ts b/packages/sdk/src/signHelpers.ts index 75a523555..8874704c6 100644 --- a/packages/sdk/src/signHelpers.ts +++ b/packages/sdk/src/signHelpers.ts @@ -307,16 +307,17 @@ export async function verifyMessageSignature( } for (const keyIndex of Object.keys(credentialSignature)) { - if (!credentialKeys.keys[Number(keyIndex)]) { - throw new Error('Signature contains signature for non-existing keyIndex'); + const key = credentialKeys.keys[Number(keyIndex)]; + switch (key) { + case undefined: + throw new Error('Signature contains signature for non-existing keyIndex'); + case null: + throw new Error('Found "null" (represents unknown key variants) in credential keys'); + default: + break; } - if ( - !(await ed.verifyAsync( - credentialSignature[Number(keyIndex)], - digest, - credentialKeys.keys[Number(keyIndex)].verifyKey - )) - ) { + + if (!(await ed.verifyAsync(credentialSignature[Number(keyIndex)], digest, key.verifyKey))) { // Incorrect signature; return false; } diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 45f58b6d8..5daf63dbe 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,28 +1,29 @@ /** * @module Common GRPC-Client */ -import { Cbor, TokenId } from './plt/index.js'; -import { TokenAccountInfo } from './plt/types.js'; -import * as AccountAddress from './types/AccountAddress.js'; +import type { Known, Upward } from './grpc/index.js'; +import type { Cbor, TokenId } from './plt/index.js'; +import type { TokenAccountInfo } from './plt/types.js'; +import type * as AccountAddress from './types/AccountAddress.js'; import type * as BlockHash from './types/BlockHash.js'; import type * as CcdAmount from './types/CcdAmount.js'; -import * as ContractAddress from './types/ContractAddress.js'; +import type * as ContractAddress from './types/ContractAddress.js'; import type * as ContractName from './types/ContractName.js'; -import * as CredentialRegistrationId from './types/CredentialRegistrationId.js'; -import { DataBlob } from './types/DataBlob.js'; -import * as Duration from './types/Duration.js'; -import * as Energy from './types/Energy.js'; +import type * as CredentialRegistrationId from './types/CredentialRegistrationId.js'; +import type { DataBlob } from './types/DataBlob.js'; +import type * as Duration from './types/Duration.js'; +import type * as Energy from './types/Energy.js'; import type * as InitName from './types/InitName.js'; import type * as ModuleReference from './types/ModuleReference.js'; -import * as Parameter from './types/Parameter.js'; +import type * as Parameter from './types/Parameter.js'; import type * as ReceiveName from './types/ReceiveName.js'; import type * as ReturnValue from './types/ReturnValue.js'; import type * as SequenceNumber from './types/SequenceNumber.js'; -import * as Timestamp from './types/Timestamp.js'; +import type * as Timestamp from './types/Timestamp.js'; import type * as TransactionExpiry from './types/TransactionExpiry.js'; import type * as TransactionHash from './types/TransactionHash.js'; -import { RejectReason } from './types/rejectReason.js'; -import { ContractTraceEvent } from './types/transactionEvent.js'; +import type { RejectReason } from './types/rejectReason.js'; +import type { ContractTraceEvent } from './types/transactionEvent.js'; export * from './types/NodeInfo.js'; export * from './types/PeerInfo.js'; @@ -514,7 +515,8 @@ interface AuthorizationsCommon { electionDifficulty: Authorization; addAnonymityRevoker: Authorization; addIdentityProvider: Authorization; - keys: VerifyKey[]; + /** The authorization keys. */ + keys: UpdatePublicKey[]; } /** @@ -538,7 +540,8 @@ export interface AuthorizationsV1 extends AuthorizationsCommon { export type Authorizations = AuthorizationsV0 | AuthorizationsV1; export interface KeysWithThreshold { - keys: VerifyKey[]; + /** The authorization keys. */ + keys: UpdatePublicKey[]; threshold: number; } @@ -796,8 +799,25 @@ export interface VerifyKey { verifyKey: HexString; } +/** + * Represents a public key used for chain updates. + */ +export type UpdatePublicKey = { + /** The key in hex format */ + verifyKey: HexString; +}; + export interface CredentialPublicKeys { - keys: Record; + /** + * keys for the credential + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + * + * In case this is used as part of a transaction sent to the node, none of the values contained can be `null`, + * as this will cause the transation to fail. + */ + keys: Record>; threshold: number; } @@ -885,7 +905,13 @@ export type BakerId = bigint; export type DelegatorId = bigint; export interface BakerPoolInfo { - openStatus: OpenStatusText; + /** + * The status of validator pool + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + openStatus: Upward; metadataUrl: UrlString; commissionRates: CommissionRates; } @@ -1102,6 +1128,7 @@ export enum AccountInfoType { Simple = 'simple', Baker = 'baker', Delegator = 'delegator', + Unknown = 'unknown', } interface AccountInfoCommon { @@ -1170,7 +1197,20 @@ export interface AccountInfoDelegator extends AccountInfoCommon { accountDelegation: AccountDelegationDetails; } -export type AccountInfo = AccountInfoSimple | AccountInfoBaker | AccountInfoDelegator; +export interface AccountInfoUnknown extends AccountInfoCommon { + type: AccountInfoType.Unknown; + /** + * This will only ever be `null`, which represents a variant of staking info for the account which is + * unknown to the SDK, for known staking variants this is represented by either {@linkcode AccountInfoBaker} + * or {@linkcode AccountInfoDelegator}. + * + * **Note**: This field is named `accountBaker` to align with the JSON representation produced by the + * corresponding rust SDK. + */ + accountBaker: Upward; +} + +export type AccountInfo = AccountInfoSimple | AccountInfoBaker | AccountInfoDelegator | AccountInfoUnknown; export interface Description { name: string; @@ -1583,14 +1623,26 @@ export interface ContractContext { export interface InvokeContractSuccessResult { tag: 'success'; usedEnergy: Energy.Type; - events: ContractTraceEvent[]; + /** + * The events related to the contract invocation. + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + events: Upward[]; returnValue?: ReturnValue.Type; } export interface InvokeContractFailedResult { tag: 'failure'; usedEnergy: Energy.Type; - reason: RejectReason; + /** + * The reject reason for the failed contract invocation. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + reason: Upward; /** * Return value from smart contract call, used to provide error messages. * Is only defined when smart contract instance is a V1 smart contract and @@ -1634,11 +1686,10 @@ interface CdiRandomness { randomness: CommitmentsRandomness; } -// TODO Should we rename this, As it is not actually the transaction that is sent to the node. (Note that this would be a breaking change) -export type CredentialDeploymentTransaction = CredentialDeploymentDetails & CdiRandomness; +export type CredentialDeploymentPayload = CredentialDeploymentDetails & CdiRandomness; /** Internal type used when building credentials */ export type UnsignedCdiWithRandomness = { - unsignedCdi: UnsignedCredentialDeploymentInformation; + unsignedCdi: Known; } & CdiRandomness; export interface CredentialDeploymentInfo extends CredentialDeploymentValues { @@ -1673,11 +1724,6 @@ export interface IdentityInput { randomness: string; } -export enum ContractVersion { - V0 = 0, - V1 = 1, -} - export enum SchemaVersion { V0 = 0, // Used by version 0 smart contracts. V1 = 1, // Used by version 1 smart contracts. @@ -2014,6 +2060,11 @@ export type Cooldown = { timestamp: Timestamp.Type; /** The amount that is in cooldown and set to be released at the end of the cooldown period */ amount: CcdAmount.Type; - /** The status of the cooldown */ - status: CooldownStatus; + /** + * The status of the cooldown + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + status: Upward; }; diff --git a/packages/sdk/src/types/AccountAddress.ts b/packages/sdk/src/types/AccountAddress.ts index acaebec05..1fa5a9e04 100644 --- a/packages/sdk/src/types/AccountAddress.ts +++ b/packages/sdk/src/types/AccountAddress.ts @@ -125,6 +125,7 @@ export function fromBase58(address: string): AccountAddress { export function toBuffer(accountAddress: AccountAddress): Uint8Array { return accountAddress.decodedAddress; } + /** * Get a base58check string of the account address. * @param {AccountAddress} accountAddress The account address. diff --git a/packages/sdk/src/types/NodeInfo.ts b/packages/sdk/src/types/NodeInfo.ts index b3be9d00f..c0079c044 100644 --- a/packages/sdk/src/types/NodeInfo.ts +++ b/packages/sdk/src/types/NodeInfo.ts @@ -1,3 +1,4 @@ +import type { Upward } from '../grpc/upward.js'; import type { BakerId, HexString } from '../types.js'; import type * as Duration from '../types/Duration.js'; import type * as Timestamp from '../types/Timestamp.js'; @@ -49,7 +50,7 @@ export interface BakerConsensusInfoStatusGeneric { export interface BakerConsensusInfoStatusPassiveCommitteeInfo { tag: 'passiveCommitteeInfo'; - passiveCommitteeInfo: PassiveCommitteeInfo; + passiveCommitteeInfo: Upward; } export enum PassiveCommitteeInfo { diff --git a/packages/sdk/src/types/PeerInfo.ts b/packages/sdk/src/types/PeerInfo.ts index 83c942fcd..4100a9a49 100644 --- a/packages/sdk/src/types/PeerInfo.ts +++ b/packages/sdk/src/types/PeerInfo.ts @@ -1,3 +1,4 @@ +import type { Upward } from '../grpc/upward.js'; import type { HexString, IpAddressString } from '../types.js'; export interface PeerInfo { @@ -22,7 +23,7 @@ export interface PeerConsensusInfoBootstrapper { export interface PeerConsensusInfoCatchupStatus { tag: 'nodeCatchupStatus'; - catchupStatus: NodeCatchupStatus; + catchupStatus: Upward; } export enum NodeCatchupStatus { diff --git a/packages/sdk/src/types/VersionedModuleSource.ts b/packages/sdk/src/types/VersionedModuleSource.ts index 19b4a41f4..09b14d7e3 100644 --- a/packages/sdk/src/types/VersionedModuleSource.ts +++ b/packages/sdk/src/types/VersionedModuleSource.ts @@ -96,9 +96,9 @@ export async function parseModuleInterface(moduleSource: VersionedModuleSource): } /** - * Extract the embedded smart contract schema bytes. Returns `null` if no schema is embedded. + * Extract the embedded smart contract schema bytes. Returns `undefined` if no schema is embedded. * @param {VersionedModuleSource} moduleSource The smart contract module source. - * @returns {RawModuleSchema | null} The raw module schema if found. + * @returns {RawModuleSchema | undefined} The raw module schema if found. * @throws If the module source cannot be parsed or contains duplicate schema sections. */ export async function getEmbeddedModuleSchema({ diff --git a/packages/sdk/src/types/blockItemSummary.ts b/packages/sdk/src/types/blockItemSummary.ts index a6ce20ad3..3c76a477a 100644 --- a/packages/sdk/src/types/blockItemSummary.ts +++ b/packages/sdk/src/types/blockItemSummary.ts @@ -1,10 +1,10 @@ -import { isEqualContractAddress } from '../contractHelpers.js'; +import { type Upward, isKnown } from '../grpc/upward.js'; import { CreatePLTPayload } from '../plt/types.js'; import { AccountTransactionType, TransactionStatusEnum, TransactionSummaryType } from '../types.js'; import { isDefined } from '../util.js'; import * as AccountAddress from './AccountAddress.js'; import type * as BlockHash from './BlockHash.js'; -import type * as ContractAddress from './ContractAddress.js'; +import * as ContractAddress from './ContractAddress.js'; import type * as ContractEvent from './ContractEvent.js'; import type * as Energy from './Energy.js'; import type * as TransactionHash from './TransactionHash.js'; @@ -131,7 +131,13 @@ export interface InitContractSummary { export interface UpdateContractSummary { transactionType: TransactionKindString.Update; - events: ContractTraceEvent[]; + /** + * The events related to the contract update. + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + events: Upward[]; } export interface DataRegisteredSummary { @@ -177,12 +183,24 @@ export interface UpdateBakerRestakeEarningsSummary { export interface ConfigureBakerSummary { transactionType: TransactionKindString.ConfigureBaker; - events: BakerEvent[]; + /** + * The events corresponding to the baker configuration + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + events: Upward[]; } export interface ConfigureDelegationSummary { transactionType: TransactionKindString.ConfigureDelegation; - events: DelegationEvent[]; + /** + * The events corresponding to the delegation configuration + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + events: Upward[]; } export interface UpdateCredentialKeysSummary { @@ -197,7 +215,13 @@ export interface UpdateCredentialsSummary { export interface FailedTransactionSummary { transactionType: TransactionKindString.Failed; failedTransactionType?: TransactionKindString; - rejectReason: RejectReason; + /** + * The reject reason for the failed transaction + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + rejectReason: Upward; } /** @@ -205,8 +229,13 @@ export interface FailedTransactionSummary { */ export type TokenUpdateSummary = { transactionType: TransactionKindString.TokenUpdate; - /** The update details */ - events: TokenEvent[]; + /** + * The token update details + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + events: Upward[]; }; /** @@ -249,13 +278,25 @@ export interface AccountCreationSummary extends BaseBlockItemSummary { export interface UpdateSummary extends BaseBlockItemSummary { type: TransactionSummaryType.UpdateTransaction; effectiveTime: bigint; - payload: UpdateInstructionPayload; + /** + * The payload of update. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + payload: Upward; } export type TokenCreationSummary = { type: TransactionSummaryType.TokenCreation; payload: CreatePLTPayload; - events: TokenEvent[]; + /** + * The token creation details + * + * **Please note**, these can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + events: Upward[]; }; export type BlockItemSummary = @@ -266,7 +307,13 @@ export type BlockItemSummary = export interface BlockItemSummaryInBlock { blockHash: BlockHash.Type; - summary: BlockItemSummary; + /** + * The summary/outcome of processing the block item. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + summary: Upward; } export interface PendingBlockItem { @@ -353,15 +400,15 @@ export const isSuccessTransaction = ( * * @param {BlockItemSummary} summary - The block item summary to check. * - * @returns {RejectReason | undfined} Reject reason if `summary` is a rejected transaction. Otherwise returns undefined. + * @returns {RejectReason | undefined} Reject reason if `summary` is a rejected transaction. Otherwise returns undefined. */ -export function getTransactionRejectReason(summary: T): RejectReason; +export function getTransactionRejectReason(summary: T): Upward; export function getTransactionRejectReason(summary: AccountCreationSummary | UpdateSummary): undefined; export function getTransactionRejectReason( summary: Exclude ): undefined; -export function getTransactionRejectReason(summary: BlockItemSummary): RejectReason | undefined; -export function getTransactionRejectReason(summary: BlockItemSummary): RejectReason | undefined { +export function getTransactionRejectReason(summary: BlockItemSummary): Upward | undefined; +export function getTransactionRejectReason(summary: BlockItemSummary): Upward | undefined { if (!isRejectTransaction(summary)) { return undefined; } @@ -406,22 +453,48 @@ export function getReceiverAccount(summary: BlockItemSummary): AccountAddress.Ty } } +/** + * + * Takes a list of items and appends another list of items to it, given they are not already in the list + * according to the eq function. + * + * @template T - the type of items in the list + * @param list - the list to append to + * @param items - the list to append to the existing list + * @param eq - the equality function to use for checking uniqueness + * @returns A new list consisting of unique items from both lists supplied + * + * NOTE: This is O(n*m) i.e. not great, but the expected data set is very small. If that ever changes, + * consider optimizing. + */ +function addUnique(list: Upward[], items: T | T[], eq: (a: T, b: T) => boolean): Upward[] { + const next = [...list]; + const flattened = Array.isArray(items) ? items : [items]; + for (let i = 0; i < flattened.length; i++) { + const item = flattened[i]; + if (!next.filter(isKnown).some((li) => eq(item, li))) { + next.push(item); + } + } + return next; +} + /** * Gets a list of {@link ContractAddress} contract addresses affected by the transaction. * * @param {BlockItemSummary} summary - The block item summary to check. * - * @returns {ContractAddress[]} List of contract addresses affected by the transaction. + * @returns {Upward[]} List of contract addresses affected by the transaction. */ export function affectedContracts( summary: T -): ContractAddress.Type[]; +): Upward[]; export function affectedContracts( summary: Exclude ): never[]; export function affectedContracts(summary: AccountCreationSummary | UpdateSummary): never[]; -export function affectedContracts(summary: BlockItemSummary): ContractAddress.Type[]; -export function affectedContracts(summary: BlockItemSummary): ContractAddress.Type[] { +export function affectedContracts(summary: BlockItemSummary): Upward[]; +export function affectedContracts(summary: BlockItemSummary): Upward[] { if (summary.type !== TransactionSummaryType.AccountTransaction) { return []; } @@ -431,15 +504,14 @@ export function affectedContracts(summary: BlockItemSummary): ContractAddress.Ty return [summary.contractInitialized.address]; } case TransactionKindString.Update: { - return summary.events.reduce((addresses: ContractAddress.Type[], event) => { - if ( - event.tag !== TransactionEventTag.Updated || - addresses.some(isEqualContractAddress(event.address)) - ) { + return summary.events.reduce((addresses: Upward[], event) => { + if (!isKnown(event)) { + return [...addresses, null]; + } + if (event.tag !== TransactionEventTag.Updated) { return addresses; } - - return [...addresses, event.address]; + return addUnique(addresses, event.address, ContractAddress.equals); }, []); } default: { @@ -448,21 +520,37 @@ export function affectedContracts(summary: BlockItemSummary): ContractAddress.Ty } } -/** - * Gets a list of {@link Base58String} account addresses affected by the transaction. - * - * @param {BlockItemSummary} summary - The block item summary to check. - * - * @returns {AccountAddress.Type[]} List of account addresses affected by the transaction. - */ -export function affectedAccounts(summary: AccountTransactionSummary): AccountAddress.Type[]; -export function affectedAccounts(summary: AccountCreationSummary | UpdateSummary): never[]; -export function affectedAccounts(summary: BlockItemSummary): AccountAddress.Type[]; -export function affectedAccounts(summary: BlockItemSummary): AccountAddress.Type[] { - if (summary.type !== TransactionSummaryType.AccountTransaction) { - return []; - } +function tokenEventsAffectedAccounts( + events: Upward[], + sender?: AccountAddress.Type +): Upward[] { + return events.reduce( + (addresses: Upward[], event) => { + if (!isKnown(event)) { + return [...addresses, null]; + } + + switch (event.tag) { + case TransactionEventTag.TokenTransfer: + return addUnique( + addresses, + [event.to?.address, event.from?.address].filter(isDefined), + AccountAddress.equals + ); + case TransactionEventTag.TokenBurn: + case TransactionEventTag.TokenMint: + return addUnique(addresses, [event.target?.address].filter(isDefined), AccountAddress.equals); + case TransactionEventTag.TokenModuleEvent: + // This only includes the encoded events pertaining to list updates and token pausation, + // thus not affecting any account's balance + return addresses; + } + }, + sender !== undefined ? [sender] : [] + ); +} +function accTransactionsAffectedAccounts(summary: AccountTransactionSummary): Upward[] { switch (summary.transactionType) { case TransactionKindString.EncryptedAmountTransfer: case TransactionKindString.EncryptedAmountTransferWithMemo: @@ -473,21 +561,23 @@ export function affectedAccounts(summary: BlockItemSummary): AccountAddress.Type return [summary.removed.account]; case TransactionKindString.Update: { return summary.events.reduce( - (addresses: AccountAddress.Type[], event) => { - if ( - event.tag === TransactionEventTag.Transferred && - !addresses.some(AccountAddress.equals.bind(undefined, event.to)) - ) { - return [...addresses, event.to]; + (addresses: Upward[], event) => { + if (!isKnown(event)) { + return [...addresses, null]; } - return addresses; + if (event.tag !== TransactionEventTag.Transferred) { + return addresses; + } + return addUnique(addresses, event.to, AccountAddress.equals); }, [summary.sender] ); } + case TransactionKindString.TokenUpdate: { + return tokenEventsAffectedAccounts(summary.events, summary.sender); + } default: { const receiver = getReceiverAccount(summary); - if (receiver === undefined || AccountAddress.equals(summary.sender, receiver)) { return [summary.sender]; } @@ -497,6 +587,28 @@ export function affectedAccounts(summary: BlockItemSummary): AccountAddress.Type } } +/** + * Gets a list of {@link Base58String} account addresses affected by the transaction. + * + * @param {BlockItemSummary} summary - The block item summary to check. + * + * @returns {Upward[]} List of account addresses affected by the transaction. + */ +export function affectedAccounts(summary: AccountTransactionSummary): Upward[]; +export function affectedAccounts(summary: TokenCreationSummary): Upward[]; +export function affectedAccounts(summary: AccountCreationSummary | UpdateSummary): never[]; +export function affectedAccounts(summary: BlockItemSummary): Upward[]; +export function affectedAccounts(summary: BlockItemSummary): Upward[] { + switch (summary.type) { + case TransactionSummaryType.AccountTransaction: + return accTransactionsAffectedAccounts(summary); + case TransactionSummaryType.TokenCreation: + return tokenEventsAffectedAccounts(summary.events); + default: + return []; + } +} + export type SummaryContractUpdateLog = { address: ContractAddress.Type; events: ContractEvent.Type[]; @@ -508,21 +620,27 @@ export type SummaryContractUpdateLog = { * * @param {BlockItemSummary} summary - The block item summary to check. * - * @returns {SummaryContractUpdateLog[]} List of update logs corresponding to the transaction. + * @returns {Upward[]} List of update logs corresponding to the transaction. */ -export function getSummaryContractUpdateLogs(summary: T): SummaryContractUpdateLog[]; +export function getSummaryContractUpdateLogs( + summary: T +): Upward[]; export function getSummaryContractUpdateLogs(summary: AccountCreationSummary | UpdateSummary): never[]; export function getSummaryContractUpdateLogs( summary: Exclude ): never[]; -export function getSummaryContractUpdateLogs(summary: BlockItemSummary): SummaryContractUpdateLog[]; -export function getSummaryContractUpdateLogs(summary: BlockItemSummary): SummaryContractUpdateLog[] { +export function getSummaryContractUpdateLogs(summary: BlockItemSummary): Upward[]; +export function getSummaryContractUpdateLogs(summary: BlockItemSummary): Upward[] { if (summary.type !== TransactionSummaryType.AccountTransaction || !isUpdateContractSummary(summary)) { return []; } return summary.events .map((event) => { + if (!isKnown(event)) { + return null; + } + switch (event.tag) { case TransactionEventTag.Updated: case TransactionEventTag.Interrupted: diff --git a/packages/sdk/src/types/cbor.ts b/packages/sdk/src/types/cbor.ts index 7554a5b29..9541db73f 100644 --- a/packages/sdk/src/types/cbor.ts +++ b/packages/sdk/src/types/cbor.ts @@ -1,6 +1,6 @@ import { decode, encode } from 'cbor2'; -import { CborMemo, TokenAmount, TokenHolder, TokenMetadataUrl } from '../plt/index.js'; +import { CborAccountAddress, CborMemo, TokenAmount, TokenMetadataUrl } from '../plt/index.js'; /** * Register CBOR encoders for all types. @@ -12,7 +12,7 @@ import { CborMemo, TokenAmount, TokenHolder, TokenMetadataUrl } from '../plt/ind * - `CborMemo`: For encoding protocol-level token memos in CBOR format */ export function registerCBOREncoders(): void { - TokenHolder.registerCBOREncoder(); + CborAccountAddress.registerCBOREncoder(); TokenAmount.registerCBOREncoder(); CborMemo.registerCBOREncoder(); TokenMetadataUrl.registerCBOREncoder(); @@ -86,7 +86,11 @@ export function cborEncode(value: unknown): Uint8Array { // We do NOT want to register all decoders, as only one decoder for each CBOR tag can exist at a time. // As such, it should be up to the end user to decide if they want to register the decoders globally in their application. export function registerCBORDecoders(): (() => void)[] { - return [TokenHolder.registerCBORDecoder(), TokenAmount.registerCBORDecoder(), CborMemo.registerCBORDecoder()]; + return [ + CborAccountAddress.registerCBORDecoder(), + TokenAmount.registerCBORDecoder(), + CborMemo.registerCBORDecoder(), + ]; } /** diff --git a/packages/sdk/src/types/chainUpdate.ts b/packages/sdk/src/types/chainUpdate.ts index 4983d6e2f..f38840991 100644 --- a/packages/sdk/src/types/chainUpdate.ts +++ b/packages/sdk/src/types/chainUpdate.ts @@ -1,3 +1,4 @@ +import { Upward } from '../index.js'; import { CreatePLTPayload } from '../plt/types.js'; import type { ArInfo, @@ -15,8 +16,8 @@ import type { MintRate, TimeoutParameters, TransactionFeeDistribution, + UpdatePublicKey, ValidatorScoreParameters, - VerifyKey, } from '../types.js'; import type * as CcdAmount from './CcdAmount.js'; import type * as Duration from './Duration.js'; @@ -141,8 +142,13 @@ export type UpdateInstructionPayload = CommonUpdate | RootUpdate | Level1Update; export type PendingUpdate = { /** The effective time of the update */ effectiveTime: Timestamp.Type; - /** The effect of the update */ - effect: PendingUpdateEffect; + /** + * The effect of the update. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + effect: Upward; }; /** A union of possible effects */ @@ -244,7 +250,7 @@ export enum KeyUpdateEntryStatus { } export interface KeyWithStatus { - key: VerifyKey; + key: UpdatePublicKey; status: KeyUpdateEntryStatus; } @@ -255,7 +261,13 @@ export enum HigherLevelKeyUpdateType { export interface HigherLevelKeyUpdate { typeOfUpdate: HigherLevelKeyUpdateType; - updateKeys: VerifyKey[]; + /** + * The authorization keys included in the update. + */ + updateKeys: UpdatePublicKey[]; + /** + * The key threshold needed to perform the update to higher level keys. + */ threshold: number; } diff --git a/packages/sdk/src/types/rejectReason.ts b/packages/sdk/src/types/rejectReason.ts index d17f47939..802820b51 100644 --- a/packages/sdk/src/types/rejectReason.ts +++ b/packages/sdk/src/types/rejectReason.ts @@ -1,4 +1,4 @@ -import { TokenId, TokenModuleRejectReason } from '../plt/index.js'; +import { EncodedTokenModuleRejectReason, TokenId } from '../plt/index.js'; import { Address, BakerId, Base58String, HexString } from '../types.js'; import type * as CcdAmount from './CcdAmount.js'; import type * as ContractAddress from './ContractAddress.js'; @@ -203,7 +203,7 @@ export type NonExistingTokenIdRejectReason = { export type TokenUpdateTransactionFailedRejectReason = { tag: RejectReasonTag.TokenUpdateTransactionFailed; /** The specific token module reject reason that caused the transaction to fail */ - contents: TokenModuleRejectReason; + contents: EncodedTokenModuleRejectReason; }; export type TokenRejectReason = NonExistingTokenIdRejectReason | TokenUpdateTransactionFailedRejectReason; diff --git a/packages/sdk/src/types/transactionEvent.ts b/packages/sdk/src/types/transactionEvent.ts index d26269a65..47df9be48 100644 --- a/packages/sdk/src/types/transactionEvent.ts +++ b/packages/sdk/src/types/transactionEvent.ts @@ -1,8 +1,8 @@ -import * as PLT from '../plt/index.js'; +import type { Upward } from '../grpc/index.js'; +import type * as PLT from '../plt/index.js'; import type { Address, BakerId, - ContractVersion, DelegatorId, EventDelegationTarget, HexString, @@ -105,7 +105,7 @@ export interface UpdatedEvent { address: ContractAddress.Type; instigator: Address; amount: CcdAmount.Type; - contractVersion: ContractVersion; + contractVersion: number; message: Parameter.Type; receiveName: ReceiveName.Type; events: ContractEvent.Type[]; @@ -138,7 +138,7 @@ export interface ContractInitializedEvent { amount: CcdAmount.Type; initName: InitName.Type; events: HexString[]; - contractVersion: ContractVersion; + contractVersion: number; ref: ModuleRef; } @@ -298,7 +298,13 @@ export interface BakerSetOpenStatusEvent { tag: TransactionEventTag.BakerSetOpenStatus; bakerId: BakerId; account: AccountAddress.Type; - openStatus: OpenStatusText; + /** + * The status of validator pool + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + openStatus: Upward; } export interface BakerSetMetadataURLEvent { @@ -347,7 +353,13 @@ export interface BakerResumedEvent { export interface UpdateEnqueuedEvent { tag: TransactionEventTag.UpdateEnqueued; effectiveTime: number; - payload: UpdateInstructionPayload; + /** + * The payload of the enqueued update. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + payload: Upward; } /** @@ -372,10 +384,20 @@ export type TokenTransferEvent = { tag: TransactionEventTag.TokenTransfer; /** The token ID of the token the event originates from */ tokenId: PLT.TokenId.Type; - /** The token holder sending the tokens. */ - from: PLT.TokenHolder.Type; - /** The token holder receiving the tokens. */ - to: PLT.TokenHolder.Type; + /** + * The token holder sending the tokens. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + from: Upward; + /** + * The token holder receiving the tokens. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + to: Upward; /** The amount of tokens transferred. */ amount: PLT.TokenAmount.Type; /** An optional memo associated with the transfer. */ @@ -390,8 +412,13 @@ export type TokenMintEvent = { tag: TransactionEventTag.TokenMint; /** The token ID of the token the event originates from */ tokenId: PLT.TokenId.Type; - /** The token holder whose supply is updated. */ - target: PLT.TokenHolder.Type; + /** + * The token holder whose supply is updated. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + target: Upward; /** The amount by which the token supply is updated. */ amount: PLT.TokenAmount.Type; }; @@ -404,8 +431,13 @@ export type TokenBurnEvent = { tag: TransactionEventTag.TokenBurn; /** The token ID of the token the event originates from */ tokenId: PLT.TokenId.Type; - /** The token holder whose supply is updated. */ - target: PLT.TokenHolder.Type; + /** + * The token holder whose supply is updated. + * + * **Please note**, this can possibly be unknown if the SDK is not fully compatible with the Concordium + * node queried, in which case `null` is returned. + */ + target: Upward; /** The amount by which the token supply is updated. */ amount: PLT.TokenAmount.Type; }; diff --git a/packages/sdk/src/wasm/credentialDeploymentTransactions.ts b/packages/sdk/src/wasm/credentialDeploymentTransactions.ts index 8bad1496c..cbf83fe15 100644 --- a/packages/sdk/src/wasm/credentialDeploymentTransactions.ts +++ b/packages/sdk/src/wasm/credentialDeploymentTransactions.ts @@ -4,6 +4,7 @@ import * as wasm from '@concordium/rust-bindings/wallet'; import * as ed from '@concordium/web-sdk/shims/ed25519'; import { Buffer } from 'buffer/index.js'; +import { Known } from '../grpc/upward.js'; import { sha256 } from '../hash.js'; import { getCredentialDeploymentSignDigest } from '../serialization.js'; import { @@ -12,7 +13,7 @@ import { AttributesKeys, CredentialDeploymentDetails, CredentialDeploymentInfo, - CredentialDeploymentTransaction, + CredentialDeploymentPayload, CredentialPublicKeys, CryptographicParameters, HexString, @@ -84,9 +85,9 @@ function createUnsignedCredentialInfo( } /** - * Create a credential deployment transaction, which is the transaction used - * when deploying a new account. - * @deprecated This function doesn't use allow supplying the randomness. {@link createCredentialTransaction} or {@link createCredentialTransactionNoSeed} should be used instead. + * Create a credential deployment transaction payload used when deploying a new account. + * + * @deprecated This function doesn't use allow supplying the randomness. {@link createCredentialPayload} or {@link createCredentialPayloadNoSeed} should be used instead. * @param identity the identity to create a credential for * @param cryptographicParameters the global cryptographic parameters from the chain * @param threshold the signature threshold for the credential, has to be less than number of public keys @@ -96,7 +97,7 @@ function createUnsignedCredentialInfo( * @param expiry the expiry of the transaction * @returns the details used in a credential deployment transaction */ -export function createCredentialDeploymentTransaction( +export function createCredentialDeploymentPayload( identity: IdentityInput, cryptographicParameters: CryptographicParameters, threshold: number, @@ -104,7 +105,7 @@ export function createCredentialDeploymentTransaction( credentialIndex: number, revealedAttributes: AttributeKey[], expiry: TransactionExpiry.Type -): CredentialDeploymentTransaction { +): CredentialDeploymentPayload { const unsignedCredentialInfo = createUnsignedCredentialInfo( identity, cryptographicParameters, @@ -200,17 +201,17 @@ export type CredentialInputNoSeed = CredentialInputCommon & { idCredSec: HexString; prfKey: HexString; sigRetrievelRandomness: HexString; - credentialPublicKeys: CredentialPublicKeys; + credentialPublicKeys: Known; attributeRandomness: Record; }; /** * Creates an unsigned credential for a new account, using the version 1 algorithm, which uses a seed to generate keys and commitments. */ -export function createCredentialTransaction( +export function createCredentialPayload( input: CredentialInput, expiry: TransactionExpiry.Type -): CredentialDeploymentTransaction { +): CredentialDeploymentPayload { const wallet = ConcordiumHdWallet.fromHex(input.seedAsHex, input.net); const publicKey = wallet .getAccountPublicKey(input.ipInfo.ipIdentity, input.identityIndex, input.credNumber) @@ -253,16 +254,16 @@ export function createCredentialTransaction( credNumber: input.credNumber, }; - return createCredentialTransactionNoSeed(noSeedInput, expiry); + return createCredentialPayloadNoSeed(noSeedInput, expiry); } /** * Creates an unsigned credential for a new account, using the version 1 algorithm, but without requiring the seed to be provided directly. */ -export function createCredentialTransactionNoSeed( +export function createCredentialPayloadNoSeed( input: CredentialInputNoSeed, expiry: TransactionExpiry.Type -): CredentialDeploymentTransaction { +): CredentialDeploymentPayload { const { sigRetrievelRandomness, ...other } = input; const internalInput = { ...other, diff --git a/packages/sdk/src/wasm/serialization.ts b/packages/sdk/src/wasm/serialization.ts index 83be602c6..69b5a97cd 100644 --- a/packages/sdk/src/wasm/serialization.ts +++ b/packages/sdk/src/wasm/serialization.ts @@ -2,7 +2,7 @@ import * as wasm from '@concordium/rust-bindings/wallet'; import { Buffer } from 'buffer/index.js'; import JSONbig from 'json-bigint'; -import type { CredentialDeploymentDetails, CredentialDeploymentTransaction } from '../types.js'; +import type { CredentialDeploymentDetails, CredentialDeploymentPayload } from '../types.js'; interface DeploymentDetailsResult { credInfo: string; @@ -54,7 +54,7 @@ export function serializeCredentialDeploymentTransactionForSubmission( export function serializeCredentialDeploymentPayload( signatures: string[], - credentialDeploymentTransaction: CredentialDeploymentTransaction + credentialDeploymentTransaction: CredentialDeploymentPayload ): Buffer { const payloadByteArray = wasm.serializeCredentialDeploymentPayload( signatures, diff --git a/packages/sdk/test/ci/credentialDeployment.test.ts b/packages/sdk/test/ci/credentialDeployment.test.ts index 5c673732a..352a090b8 100644 --- a/packages/sdk/test/ci/credentialDeployment.test.ts +++ b/packages/sdk/test/ci/credentialDeployment.test.ts @@ -8,16 +8,16 @@ import { getCredentialDeploymentSignDigest } from '../../src/serialization.js'; import { AttributeKey, BlockItemKind, - CredentialDeploymentTransaction, + CredentialDeploymentPayload, IdentityInput, IdentityObjectV1, VerifyKey, } from '../../src/types.js'; import { CredentialInput, - createCredentialDeploymentTransaction, - createCredentialTransaction, - createCredentialTransactionNoSeed, + createCredentialDeploymentPayload, + createCredentialPayload, + createCredentialPayloadNoSeed, } from '../../src/wasm/credentialDeploymentTransactions.js'; import { deserializeTransaction } from '../../src/wasm/deserialization.js'; import { serializeCredentialDeploymentTransactionForSubmission } from '../../src/wasm/serialization.js'; @@ -46,7 +46,7 @@ function createCredentialInput(revealedAttributes: AttributeKey[], idObject: Ide // This test was generated on an older version of the SDK to verify that the serialization remains the same. test('Test serialize v0 credential transaction', async () => { - const tx: CredentialDeploymentTransaction = JSON.parse(fs.readFileSync('./test/ci/resources/cdt.json').toString()); + const tx: CredentialDeploymentPayload = JSON.parse(fs.readFileSync('./test/ci/resources/cdt.json').toString()); tx.expiry = TransactionExpiry.fromEpochSeconds(tx.expiry as unknown as number); // convert JSON `TransactionExpiry`. const hashToSign = getCredentialDeploymentSignDigest(tx); @@ -91,7 +91,7 @@ test('test create + deserialize v0 credentialDeployment', async () => { const revealedAttributes: AttributeKey[] = ['firstName', 'nationality']; const expiry = TransactionExpiry.futureMinutes(60); - const credentialDeploymentTransaction: CredentialDeploymentTransaction = createCredentialDeploymentTransaction( + const credentialDeploymentTransaction: CredentialDeploymentPayload = createCredentialDeploymentPayload( identityInput, cryptographicParameters, threshold, @@ -135,7 +135,7 @@ test('Test createCredentialTransaction', () => { const revealedAttributes: AttributeKey[] = ['firstName']; const idObject = JSON.parse(fs.readFileSync('./test/ci/resources/identity-object.json').toString()).value; - const output = createCredentialTransaction( + const output = createCredentialPayload( createCredentialInput(revealedAttributes, idObject), TransactionExpiry.fromEpochSeconds(expiry) ); @@ -144,7 +144,7 @@ test('Test createCredentialTransaction', () => { expect(cdi.credId).toEqual( 'b317d3fea7de56f8c96f6e72820c5cd502cc0eef8454016ee548913255897c6b52156cc60df965d3efb3f160eff6ced4' ); - expect(cdi.credentialPublicKeys.keys[0].verifyKey).toEqual( + expect(cdi.credentialPublicKeys.keys[0]!.verifyKey).toEqual( '29723ec9a0b4ca16d5d548b676a1a0adbecdedc5446894151acb7699293d69b1' ); expect(cdi.credentialPublicKeys.threshold).toEqual(1); @@ -175,13 +175,13 @@ test('Test createCredentialTransactionNoSeed lastname with special characters', const input = JSON.parse(fs.readFileSync('./test/ci/resources/credential-input-no-seed.json').toString()); const expiry = 1722939941n; - const output = createCredentialTransactionNoSeed(input, TransactionExpiry.fromEpochSeconds(expiry)); + const output = createCredentialPayloadNoSeed(input, TransactionExpiry.fromEpochSeconds(expiry)); const cdi = output.unsignedCdi; expect(cdi.credId).toEqual( '930e1e148d2a08b14ed3b5569d4768c96dbea5f540822ee38a6c52ca6c172be408ca4b78d6e2956cfad157bd02804c2c' ); - expect(cdi.credentialPublicKeys.keys[0].verifyKey).toEqual( + expect(cdi.credentialPublicKeys.keys[0]!.verifyKey).toEqual( '3522291ef370e89424a2ed8a9e440963a783aec4e34377192360f763e1671d77' ); expect(cdi.credentialPublicKeys.threshold).toEqual(1); diff --git a/packages/sdk/test/ci/plt/Cbor.test.ts b/packages/sdk/test/ci/plt/Cbor.test.ts index e868353fb..a2ad760ea 100644 --- a/packages/sdk/test/ci/plt/Cbor.test.ts +++ b/packages/sdk/test/ci/plt/Cbor.test.ts @@ -1,28 +1,26 @@ -import * as Cbor from '../../../src/plt/Cbor.js'; -import * as CborMemo from '../../../src/plt/CborMemo.js'; -import * as TokenAmount from '../../../src/plt/TokenAmount.js'; -import * as TokenHolder from '../../../src/plt/TokenHolder.js'; -import * as TokenMetadataUrl from '../../../src/plt/TokenMetadataUrl.js'; -import { TokenId } from '../../../src/plt/index.ts'; +import { CborAccountAddress, CborMemo } from '../../../src/plt/index.ts'; import { - TokenListUpdateEventDetails, + Cbor, + TokenAddDenyListOperation, + TokenAmount, + TokenId, + TokenMetadataUrl, + TokenMintOperation, TokenOperationType, - TokenPauseEventDetails, createTokenUpdatePayload, -} from '../../../src/plt/module.js'; -import { AccountAddress } from '../../../src/types/index.js'; +} from '../../../src/pub/plt.ts'; +import { AccountAddress } from '../../../src/types/index.ts'; -describe('Cbor', () => { +describe('PLT Cbor', () => { describe('TokenModuleState', () => { - test('should encode and decode TokenModuleState correctly', () => { - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const tokenHolder = TokenHolder.fromAccountAddress(accountAddress); + test('should encode and decode full TokenModuleState correctly', () => { + const account = CborAccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); const metadataUrl = TokenMetadataUrl.fromString('https://example.com/metadata.json'); const state = { name: 'Test Token', metadata: metadataUrl, - governanceAccount: tokenHolder, + governanceAccount: account, allowList: true, denyList: false, mintable: true, @@ -43,46 +41,24 @@ describe('Cbor', () => { expect(decoded.customField).toBe(state.customField); }); - test('should throw error if TokenModuleState is missing required fields', () => { - // Missing governanceAccount - const invalidState1 = { - name: 'Test Token', - metadata: TokenMetadataUrl.fromString('https://example.com/metadata.json'), - // governanceAccount is missing - }; - const encoded1 = Cbor.encode(invalidState1); - expect(() => Cbor.decode(encoded1, 'TokenModuleState')).toThrow(/missing or invalid governanceAccount/); + test('should encode and decode minimal TokenModuleState correctly', () => { + const state = {}; - // Missing name - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const invalidState2 = { - // name is missing - metadata: TokenMetadataUrl.fromString('https://example.com/metadata.json'), - governanceAccount: TokenHolder.fromAccountAddress(accountAddress), - }; - const encoded2 = Cbor.encode(invalidState2); - expect(() => Cbor.decode(encoded2, 'TokenModuleState')).toThrow(/missing or invalid name/); + const encoded = Cbor.encode(state); + const decoded = Cbor.decode(encoded, 'TokenModuleState'); - // Missing metadata - const invalidState3 = { - name: 'Test Token', - // metadata is missing - governanceAccount: TokenHolder.fromAccountAddress(accountAddress), - }; - const encoded3 = Cbor.encode(invalidState3); - expect(() => Cbor.decode(encoded3, 'TokenModuleState')).toThrow(/missing metadataUrl/); + expect(decoded).toEqual({}); }); test('should throw error if TokenModuleState has invalid field types', () => { - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const tokenHolder = TokenHolder.fromAccountAddress(accountAddress); + const account = CborAccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); const metadataUrl = TokenMetadataUrl.fromString('https://example.com/metadata.json'); // Invalid allowList type const invalidState = { name: 'Test Token', metadata: metadataUrl, - governanceAccount: tokenHolder, + governanceAccount: account, allowList: 'yes', // Should be boolean }; const encoded = Cbor.encode(invalidState); @@ -91,16 +67,15 @@ describe('Cbor', () => { }); describe('TokenInitializationParameters', () => { - test('should encode and decode TokenInitializationParameters correctly', () => { - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const tokenHolder = TokenHolder.fromAccountAddress(accountAddress); + test('should encode and decode full TokenInitializationParameters correctly', () => { + const account = CborAccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); const metadataUrl = TokenMetadataUrl.fromString('https://example.com/metadata.json'); const initialSupply = TokenAmount.fromDecimal('1.002', 3); const params = { name: 'Test Token', metadata: metadataUrl, - governanceAccount: tokenHolder, + governanceAccount: account, allowList: true, denyList: false, initialSupply, @@ -122,48 +97,24 @@ describe('Cbor', () => { expect(decoded.burnable).toBe(params.burnable); }); - test('should throw error if TokenInitializationParameters is missing required fields', () => { - // Missing governanceAccount - const invalidParams1 = { - name: 'Test Token', - metadata: TokenMetadataUrl.fromString('https://example.com/metadata.json'), - // governanceAccount is missing - }; - const encoded1 = Cbor.encode(invalidParams1); - expect(() => Cbor.decode(encoded1, 'TokenInitializationParameters')).toThrow( - /missing or invalid governanceAccount/ - ); + test('should encode and decode minimal TokenInitializationParameters correctly', () => { + const params = {}; - // Missing name - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const invalidParams2 = { - // name is missing - metadata: TokenMetadataUrl.fromString('https://example.com/metadata.json'), - governanceAccount: TokenHolder.fromAccountAddress(accountAddress), - }; - const encoded2 = Cbor.encode(invalidParams2); - expect(() => Cbor.decode(encoded2, 'TokenInitializationParameters')).toThrow(/missing or invalid name/); + const encoded = Cbor.encode(params); + const decoded = Cbor.decode(encoded, 'TokenInitializationParameters'); - // Missing metadata - const invalidParams3 = { - name: 'Test Token', - // metadata is missing - governanceAccount: TokenHolder.fromAccountAddress(accountAddress), - }; - const encoded3 = Cbor.encode(invalidParams3); - expect(() => Cbor.decode(encoded3, 'TokenInitializationParameters')).toThrow(/missing metadataUrl/); + expect(decoded).toEqual({}); }); test('should throw error if TokenInitializationParameters has invalid field types', () => { - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const tokenHolder = TokenHolder.fromAccountAddress(accountAddress); + const account = CborAccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); const metadataUrl = TokenMetadataUrl.fromString('https://example.com/metadata.json'); // Invalid allowList type const invalidParams = { name: 'Test Token', metadata: metadataUrl, - governanceAccount: tokenHolder, + governanceAccount: account, allowList: 'yes', // Should be boolean }; const encoded = Cbor.encode(invalidParams); @@ -206,60 +157,78 @@ describe('Cbor', () => { }); }); - describe('TokenListUpdateEventDetails', () => { - test('should encode and decode TokenEventDetails correctly', () => { - const accountAddress = AccountAddress.fromBase58('3XSLuJcXg6xEua6iBPnWacc3iWh93yEDMCqX8FbE3RDSbEnT9P'); - const tokenHolder = TokenHolder.fromAccountAddress(accountAddress); - - const details: TokenListUpdateEventDetails = { - target: tokenHolder, - }; - - const encoded = Cbor.encode(details); - const decoded = Cbor.decode(encoded, 'TokenListUpdateEventDetails'); - expect(decoded.target).toEqual(details.target); - }); - - test('should throw error if TokenEventDetails is missing required fields', () => { - // Missing target - const invalidDetails = { - // target is missing - additionalInfo: 'Some extra information', - }; - const encoded = Cbor.encode(invalidDetails); - expect(() => Cbor.decode(encoded, 'TokenListUpdateEventDetails')).toThrow( - /Expected 'target' to be a TokenHolder/ + describe('TokenOperation[]', () => { + test('should (de)serialize multiple governance operations correctly', () => { + const account = CborAccountAddress.fromAccountAddress( + AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)) ); - }); + // - d99d73: A tagged (40307) item with a map (a2) containing: + // - a2: A map with 2 key-value pairs + // - 01: Key 1. + // - d99d71: A tagged (40305) item containing: + // - a1: A map with 1 key-value pair + // - 01: Key 1. + // - 190397: Uint16(919). + // - 03: Key 3. + // - 5820: A byte string of length 32, representing a 32-byte identifier. + // - 151515151515151515151515151515151515151515151515151515151515151: The account address + const accountCbor = ` + d99d73 a2 + 01 d99d71 a1 + 01 190397 + 03 5820 ${Buffer.from(account.address.decodedAddress).toString('hex')} + `.replace(/\s/g, ''); + const mint: TokenMintOperation = { + [TokenOperationType.Mint]: { + amount: TokenAmount.create(500n, 2), + }, + }; - test('should throw error if TokenEventDetails has invalid target type', () => { - // Invalid target type - const invalidDetails = { - target: 'not-a-token-holder', - additionalInfo: 'Some extra information', + const addDenyList: TokenAddDenyListOperation = { + [TokenOperationType.AddDenyList]: { + target: account, + }, }; - const encoded = Cbor.encode(invalidDetails); - expect(() => Cbor.decode(encoded, 'TokenListUpdateEventDetails')).toThrow( - /Expected 'target' to be a TokenHolder/ - ); - }); - }); - describe('TokenPauseEventDetails', () => { - test('should encode and decode TokenEventDetails correctly', () => { - const details: TokenPauseEventDetails = {}; - const encoded = Cbor.encode(details); - const decoded = Cbor.decode(encoded, 'TokenPauseEventDetails'); - expect(decoded).toEqual(details); - }); + const operations = [mint, addDenyList]; + const encoded = Cbor.encode(operations); - test('should throw error if TokenEventDetails has invalid target type', () => { - // Invalid target type - const invalidDetails = 'invalid'; - const encoded = Cbor.encode(invalidDetails); - expect(() => Cbor.decode(encoded, 'TokenPauseEventDetails')).toThrow( - /Invalid event details: "invalid". Expected an object./ + // This is a CBOR encoded byte sequence representing two operations: + // - 82: An array of 2 items + // - First item (mint operation): + // - a1: A map with 1 key-value pair + // - 646d696e74: Key "mint" (in UTF-8) + // - a1: A map with 1 key-value pair + // - 66616d6f756e74: Key "amount" (in UTF-8) + // - c4: A decfrac containing: + // - 82: An array of 2 items + // - 21: Integer(-2) + // - 1901f4: Uint16(500) + // - Second item (addDenyList operation): + // - a1: A map with 1 key-value pair + // - 6b61646444656e794c697374: Key "addDenyList" (in UTF-8) + // - a1: A map with 1 key-value pair + // - 667461726765744: Key "target" (in UTF-8) + // - The account address cbor + const expectedOperations = Buffer.from( + ` + 82 + a1 + 646d696e74 a1 + 66616d6f756e74 c4 + 82 + 21 + 1901f4 + a1 + 6b61646444656e794c697374 a1 + 66746172676574 ${accountCbor} + `.replace(/\s/g, ''), + 'hex' ); + expect(encoded.toString()).toEqual(expectedOperations.toString('hex')); + + const decoded = Cbor.decode(encoded, 'TokenOperation[]'); + expect(decoded).toEqual(operations); }); }); @@ -336,11 +305,10 @@ describe('Cbor', () => { const decoded = Cbor.decode(Cbor.fromHexString('9fa17f6175636e70616063757365ffbfffff')); expect(decoded).toEqual([{ [TokenOperationType.Unpause]: {} }]); }); - const accountAddress = AccountAddress.fromBase58('4BH5qnFPDfaD3MxnDzfhnu1jAHoBWXnq2i57T6G1eZn1kC194e'); - const tokenHolder = TokenHolder.fromAccountAddress(accountAddress); + const account = CborAccountAddress.fromBase58('4BH5qnFPDfaD3MxnDzfhnu1jAHoBWXnq2i57T6G1eZn1kC194e'); test('addAllowList operation encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { - [TokenOperationType.AddAllowList]: { target: tokenHolder }, + [TokenOperationType.AddAllowList]: { target: account }, }).operations; expect(encoded.toString()).toBe( '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' @@ -352,7 +320,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] indefinite account address) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -362,7 +330,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a101190397035f41a24040581f6c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15ff' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (oversize tag) decodes correctly', () => { const decoded = Cbor.decode( @@ -370,7 +338,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574da00009d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (more oversize tags) decodes correctly', () => { const decoded = Cbor.decode( @@ -378,7 +346,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574db0000000000009d73a201db0000000000009d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (oversize int) decodes correctly', () => { const decoded = Cbor.decode( @@ -386,7 +354,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a1011a00000397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (more oversize int) decodes correctly', () => { const decoded = Cbor.decode( @@ -394,7 +362,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a1011b0000000000000397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (oversize int key) decodes correctly', () => { const decoded = Cbor.decode( @@ -402,7 +370,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a21801d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (oversize int keys) decodes correctly', () => { const decoded = Cbor.decode( @@ -410,7 +378,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a21b0000000000000001d99d71a11a000000011903971900035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation (reordered keys) decodes correctly', () => { const decoded = Cbor.decode( @@ -418,7 +386,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a2035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b1501d99d71a101190397' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] big int for int) fails decoding', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -450,7 +418,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a101f9632e035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] single-precision float for int) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -460,7 +428,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a101fa4465c000035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] double-precision float for int) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -470,7 +438,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a101fb408cb80000000000035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] half-precision float for int tags) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -480,7 +448,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a201d99d71a1f93c00190397f942005820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] single-precision float for int tags) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -490,7 +458,7 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a2fa3f800000d99d71a1fa3f800000190397fa404000005820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); test('addAllowList operation ([reject] double-precision float for int tags) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -500,12 +468,12 @@ describe('Cbor', () => { '81a16c616464416c6c6f774c697374a166746172676574d99d73a2fb3ff0000000000000d99d71a1fb3ff0000000000000190397fb40080000000000005820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddAllowList]: { target: account } }]); }); - const tokenHolderNoCoinInfo = TokenHolder.fromAccountAddressNoCoinInfo(accountAddress); + const accountNoCoinInfo = CborAccountAddress.fromJSON({ address: account.address.toString() }); test('removeAllowList operation encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { - [TokenOperationType.RemoveAllowList]: { target: tokenHolderNoCoinInfo }, + [TokenOperationType.RemoveAllowList]: { target: accountNoCoinInfo }, }).operations; expect(encoded.toString()).toBe( '81a16f72656d6f7665416c6c6f774c697374a166746172676574d99d73a1035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' @@ -517,7 +485,7 @@ describe('Cbor', () => { '81a16f72656d6f7665416c6c6f774c697374a166746172676574d99d73a1035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.RemoveAllowList]: { target: tokenHolderNoCoinInfo } }]); + expect(decoded).toEqual([{ [TokenOperationType.RemoveAllowList]: { target: accountNoCoinInfo } }]); }); test('removeAllowList operation (indefinite lengths and oversized ints) decodes correctly', () => { const decoded = Cbor.decode( @@ -525,11 +493,11 @@ describe('Cbor', () => { '81a16f72656d6f7665416c6c6f774c697374a166746172676574d99d73a1035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.RemoveAllowList]: { target: tokenHolderNoCoinInfo } }]); + expect(decoded).toEqual([{ [TokenOperationType.RemoveAllowList]: { target: accountNoCoinInfo } }]); }); test('addDenyList operation encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { - [TokenOperationType.AddDenyList]: { target: tokenHolder }, + [TokenOperationType.AddDenyList]: { target: account }, }).operations; expect(encoded.toString()).toBe( '81a16b61646444656e794c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' @@ -541,7 +509,7 @@ describe('Cbor', () => { '81a16b61646444656e794c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddDenyList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddDenyList]: { target: account } }]); }); test('addDenyList operation ([reject] coininfo includes network) decodes correctly', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -551,7 +519,7 @@ describe('Cbor', () => { '81a16b61646444656e794c697374a166746172676574d99d73a201d99d71a2011903970200035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.AddDenyList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.AddDenyList]: { target: account } }]); }); test('addDenyList operation ([reject] coininfo with bad coin type) fails decoding', () => { // Note: this is rejected by the Haskell Token Module implementation, due to the @@ -566,7 +534,7 @@ describe('Cbor', () => { }); test('removeDenyList operation encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { - [TokenOperationType.RemoveDenyList]: { target: tokenHolder }, + [TokenOperationType.RemoveDenyList]: { target: account }, }).operations; expect(encoded.toString()).toBe( '81a16e72656d6f766544656e794c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' @@ -578,7 +546,7 @@ describe('Cbor', () => { '81a16e72656d6f766544656e794c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { target: account } }]); }); test('removeDenyList operation ([reject] RemoveDenyList) decodes correctly', () => { // Note: this is rejected by the Haskell token module implementation, due to @@ -589,7 +557,7 @@ describe('Cbor', () => { '81a16e52656d6f766544656e794c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ RemoveDenyList: { target: tokenHolder } }]); + expect(decoded).toEqual([{ RemoveDenyList: { target: account } }]); }); test('removeDenyList operation ([reject] remove-deny-list) decodes correctly', () => { // Note: this is rejected by the Haskell token module implementation, due to @@ -600,7 +568,7 @@ describe('Cbor', () => { '81a17072656d6f76652d64656e792d6c697374a166746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ 'remove-deny-list': { target: tokenHolder } }]); + expect(decoded).toEqual([{ 'remove-deny-list': { target: account } }]); }); test('removeDenyList operation ([reject] Target) decodes correctly', () => { // Note: this is rejected by the Haskell token module implementation, due to @@ -611,7 +579,7 @@ describe('Cbor', () => { '81a16e72656d6f766544656e794c697374a166546172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { Target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { Target: account } }]); }); test('removeDenyList operation ([reject] no target) decodes correctly', () => { // Note: this is rejected by the Haskell token module implementation, due to @@ -627,7 +595,7 @@ describe('Cbor', () => { '81a16e72656d6f766544656e794c697374a2646c6973740066746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { list: 0, target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { list: 0, target: account } }]); }); test('removeDenyList operation ([reject] duplicate target) decodes', () => { // Note: this is rejected by the Haskell Token Module implementation, due to @@ -637,11 +605,11 @@ describe('Cbor', () => { '81a16e72656d6f766544656e794c697374a266746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b1566746172676574d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' ) ); - expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { target: tokenHolder } }]); + expect(decoded).toEqual([{ [TokenOperationType.RemoveDenyList]: { target: account } }]); }); test('transfer operation encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { - [TokenOperationType.Transfer]: { recipient: tokenHolder, amount: TokenAmount.fromDecimal('1.00', 2) }, + [TokenOperationType.Transfer]: { recipient: account, amount: TokenAmount.fromDecimal('1.00', 2) }, }).operations; expect(encoded.toString()).toBe( '81a1687472616e73666572a266616d6f756e74c48221186469726563697069656e74d99d73a201d99d71a101190397035820a26c957377a2461b6d0b9f63e7c9504136181942145e16c926451bbce5502b15' @@ -656,7 +624,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.fromDecimal('1.00', 2), }, }, @@ -665,7 +633,7 @@ describe('Cbor', () => { test('transfer operation (max amount) encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt('18446744073709551615'), 0), }, }).operations; @@ -682,7 +650,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt('18446744073709551615'), 0), }, }, @@ -691,7 +659,7 @@ describe('Cbor', () => { test('transfer operation (max decimals) encodes correctly', () => { const encoded = createTokenUpdatePayload(TokenId.fromString('TEST'), { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt('18446744073709551615'), 255), }, }).operations; @@ -708,7 +676,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt('18446744073709551615'), 255), }, }, @@ -743,7 +711,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -756,7 +724,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -769,7 +737,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -782,7 +750,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -795,7 +763,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -808,7 +776,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -821,7 +789,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -846,7 +814,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -861,7 +829,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -876,7 +844,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -891,7 +859,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -907,7 +875,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -922,7 +890,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -956,7 +924,7 @@ describe('Cbor', () => { 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, ]), - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -993,7 +961,7 @@ describe('Cbor', () => { 0xfc, 0xfd, 0xfe, 0xff, ]), }), - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -1028,7 +996,7 @@ describe('Cbor', () => { 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff, 0x00, ]), - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -1049,7 +1017,7 @@ describe('Cbor', () => { { [TokenOperationType.Transfer]: { memo: Uint8Array.from([]), - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, @@ -1064,7 +1032,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), blahasdfasdf: 2, }, @@ -1080,7 +1048,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, }, }, ]); @@ -1094,7 +1062,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), memo: Uint8Array.from([0x01]), }, @@ -1110,7 +1078,7 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(200), 2), }, }, @@ -1123,13 +1091,13 @@ describe('Cbor', () => { expect(decoded).toEqual([ { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(100), 2), }, }, { [TokenOperationType.Transfer]: { - recipient: tokenHolder, + recipient: account, amount: TokenAmount.create(BigInt(500), 2), }, }, diff --git a/packages/sdk/test/ci/plt/CborAccountAddress.test.ts b/packages/sdk/test/ci/plt/CborAccountAddress.test.ts new file mode 100644 index 000000000..6e6b2ed9e --- /dev/null +++ b/packages/sdk/test/ci/plt/CborAccountAddress.test.ts @@ -0,0 +1,165 @@ +import { Buffer } from 'buffer/index.js'; +import { decode, encode } from 'cbor2'; + +import { CborAccountAddress } from '../../../src/plt/index.ts'; +import { AccountAddress } from '../../../src/types/index.ts'; + +describe('PLT CborAccountAddress', () => { + test('Account address cbor encoding', () => { + const address = CborAccountAddress.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); + const encoded = CborAccountAddress.toCBOR(address); + // This expected buffer represents the CBOR encoding of an TokenHolder with: + // - Tag 40307 (0x9d73) for Address + // - Two properties: + // - Key 01: Tagged CoinInfo (40305/0x9d71) with discriminant 919 (0x397) for Concordium + // - Key 03: 32-byte address filled with 0x15 + const expected = Buffer.from( + ` + d99d73 a2 + 01 + d99d71 + a1 + 01 19 0397 + 03 5820 1515151515151515151515151515151515151515151515151515151515151515 + `.replace(/\s/g, ''), + 'hex' + ); + expect(Buffer.from(encoded).toString('hex')).toEqual(expected.toString('hex')); + }); + + describe('fromCBOR preserves original structure', () => { + test('with tagged-coininfo', () => { + const expected = { + address: AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)), + coinInfo: 919, + }; + + // This expected buffer represents the CBOR encoding of an TokenHolder with: + // - Tag 40307 (0x9d73) for Address + // - Two properties: + // - Key 01: Tagged CoinInfo (40305/0x9d71) with discriminant 800 + // - Key 03: 32-byte address filled with 0x15 + const taggedAddressBytes = Buffer.from( + ` + d99d73 a2 + 01 + d99d71 + a1 + 01 19 0397 + 03 5820 1515151515151515151515151515151515151515151515151515151515151515 + `.replace(/\s/g, ''), + 'hex' + ); + const decoded = CborAccountAddress.fromCBOR(taggedAddressBytes); + + // Verify the decoded address matches the original + expect(decoded).toEqual(expected); + expect(decoded.coinInfo).toBe(919); + }); + + test('without tagged-coininfo', () => { + // Create a test address with known bytes + const expected = { + address: AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)), + coinInfo: undefined, + } as CborAccountAddress.Type; + + // Create CBOR bytes manually without the tagged-coininfo (only the address bytes) + // This buffer represents the simplified CBOR encoding of an TokenHolder with: + // - Tag 40307 (0x9d73) for Address + // - One property: + // - Key 03: 32-byte address filled with 0x15 + const taggedAddressBytes = Buffer.from( + ` + d99d73 a1 + 03 + 5820 1515151515151515151515151515151515151515151515151515151515151515 + `.replace(/\s/g, ''), + 'hex' + ); + + const decoded = CborAccountAddress.fromCBOR(taggedAddressBytes); + + // Verify the decoded address matches the original + expect(decoded).toEqual(expected); + }); + }); + + describe('fromJSON preserves original structure', () => { + test('with tagged-coininfo', () => { + const account = AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)); + const expected: CborAccountAddress.JSON = { + address: account.toString(), + coinInfo: 919, + } as unknown as CborAccountAddress.JSON; + + const parsed = CborAccountAddress.fromJSON(expected); + + // Verify the decoded address matches the original + expect(parsed.address).toEqual(account); + expect(parsed.coinInfo).toBe(expected.coinInfo); + }); + + test('without tagged-coininfo', () => { + const account = AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)); + const expected: CborAccountAddress.JSON = { + address: account.toString(), + }; + + const parsed = CborAccountAddress.fromJSON(expected); + + // Verify the decoded address matches the original + expect(parsed.address).toEqual(account); + expect(parsed.coinInfo).toBe(undefined); + }); + }); + + test('CBOR encoding/decoding with cbor2 library registration', () => { + // Register the TokenHolder encoder and decoder + CborAccountAddress.registerCBOREncoder(); + const cleanup = CborAccountAddress.registerCBORDecoder(); + + try { + // Create a test address + const originalAddress = CborAccountAddress.fromAccountAddress( + AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)) + ); + + // Encode directly with cbor2 library + const encoded = encode(originalAddress); + // This hex string represents the expected CBOR encoding of an TokenHolder with: + // - Tag 40307 (0x9d73) for Address + // - Two properties: + // - Key 01: Tagged CoinInfo (40305/0x9d71) with discriminant 919 (0x397) for Concordium + // - Key 03: 32-byte address filled with 0x15 + const taggedAddressHex = ` + d99d73 a2 + 01 + d99d71 + a1 + 01 19 0397 + 03 5820 1515151515151515151515151515151515151515151515151515151515151515 + `.replace(/\s/g, ''); + + // Check that the encoded value follows the expected structure + // The address should be tagged with 40307 (0x9d73) + expect(Buffer.from(encoded).toString('hex')).toEqual(taggedAddressHex); + + // Decode directly with cbor2 library (should use our registered decoder) + const decoded: CborAccountAddress.Type = decode(encoded); + + // Verify it's an TokenHolder instance + expect(CborAccountAddress.instanceOf(decoded)).toBeTruthy(); + + // Verify the address matches the original + expect(decoded).toEqual(originalAddress); + + // Test encode-decode roundtrip with cbor2 + const roundtrip: CborAccountAddress.Type = decode(encode(originalAddress)); + expect(roundtrip).toEqual(originalAddress); + } finally { + // Clean up the registered decoder + cleanup(); + } + }); +}); diff --git a/packages/sdk/test/ci/plt/CborContractAddress.test.ts b/packages/sdk/test/ci/plt/CborContractAddress.test.ts new file mode 100644 index 000000000..862811515 --- /dev/null +++ b/packages/sdk/test/ci/plt/CborContractAddress.test.ts @@ -0,0 +1,233 @@ +import { Tag, decode, encode } from 'cbor2'; + +import { MAX_U64 } from '../../../src/constants.ts'; +import { CborContractAddress } from '../../../src/pub/plt.ts'; +import { ContractAddress } from '../../../src/types/index.js'; + +/** + * Tests for CBOR contract address (tag 40919) encoding/decoding utilities. + */ +describe('PLT CborContractAddress', () => { + test('create basic (no explicit subindex) and toString/toJSON', () => { + const addr = CborContractAddress.create(42n); + expect(addr.index).toBe(42n); + expect(addr.subindex).toBe(undefined); + expect(addr.toString()).toBe('<42, 0>'); + expect(addr.toJSON()).toEqual({ index: 42n }); + }); + + test('create with explicit subindex 0 and toString/toJSON', () => { + const addr = CborContractAddress.create(42n, 0n); + expect(addr.index).toBe(42n); + expect(addr.subindex).toBe(undefined); + expect(addr.toString()).toBe('<42, 0>'); + expect(addr.toJSON()).toEqual({ index: 42n }); + }); + + test('create with subindex and toJSON includes subindex', () => { + const addr = CborContractAddress.create(42n, 7n); + expect(addr.index).toBe(42n); + expect(addr.subindex).toBe(7n); + expect(addr.toJSON()).toEqual({ index: 42n, subindex: 7n }); + expect(addr.toString()).toBe('<42, 7>'); + }); + + test('negative index rejected', () => { + expect(() => CborContractAddress.create(-1n)).toThrow(/negative/i); + expect(() => CborContractAddress.create(1n, -5n)).toThrow(/negative/i); + }); + + test('exceeds max value rejected', () => { + const over = MAX_U64 + 1n; + expect(() => CborContractAddress.create(over)).toThrow(/larger/); + expect(() => CborContractAddress.create(1n, over)).toThrow(/larger/); + }); + + test('decimal values rejected', () => { + expect(() => CborContractAddress.create(1.2)).toThrow(); + expect(() => CborContractAddress.create(1n, 3.2)).toThrow(); + }); + + test('from/to ContractAddress conversion', () => { + const ca = ContractAddress.create(10n, 5n); + const wrapped = CborContractAddress.fromContractAddress(ca); + expect(wrapped.index).toBe(10n); + expect(wrapped.subindex).toBe(5n); + const back = CborContractAddress.toContractAddress(wrapped); + expect(back).toEqual(ca); + }); + + describe('CBOR tagged value (array form) encode/decode', () => { + test('single byte encoding', () => { + const addr = CborContractAddress.create(5n, 9n); + const encoded = CborContractAddress.toCBOR(addr); + const hex = Buffer.from(encoded).toString('hex'); + + // Tag 40919 containing array with 2 values: uint(5) and uint(9) + const expected = `d99fd7 82 05 09`.replace(/\s/g, ''); + expect(hex).toEqual(expected); + + const rt = CborContractAddress.fromCBOR(encoded); + expect(rt.index).toEqual(5n); + expect(rt.subindex).toEqual(9n); + }); + + test('small values', () => { + const addr = CborContractAddress.create(123n, 25n); + const encoded = CborContractAddress.toCBOR(addr); + const hex = Buffer.from(encoded).toString('hex'); + + // Tag 40919 containing array with 2 values: uint(123) and uint(25) + const expected = `d99fd7 82 187b 1819`.replace(/\s/g, ''); + expect(hex).toEqual(expected); + + const rt = CborContractAddress.fromCBOR(encoded); + expect(rt.index).toEqual(123n); + expect(rt.subindex).toEqual(25n); + }); + + test('large values', () => { + const addr = CborContractAddress.create(MAX_U64, 25n); + const encoded = CborContractAddress.toCBOR(addr); + const hex = Buffer.from(encoded).toString('hex'); + + // Tag 40919 containing array with 2 values: uint(uint64_max) and uint(25) + const expected = `d99fd7 82 1bffffffffffffffff 1819`.replace(/\s/g, ''); + expect(hex).toEqual(expected); + + const rt = CborContractAddress.fromCBOR(encoded); + expect(rt.index).toEqual(MAX_U64); + expect(rt.subindex).toEqual(25n); + }); + }); + + describe('CBOR tagged value decode simple form (uint value)', () => { + test('single byte encoding', () => { + const addr = CborContractAddress.create(5n); + const encoded = CborContractAddress.toCBOR(addr); + const hex = Buffer.from(encoded).toString('hex'); + + // Tag 40919 containing uint(5) + const expected = `d99fd7 05`.replace(/\s/g, ''); + expect(hex).toEqual(expected); + + const decoded = CborContractAddress.fromCBOR(encoded); + expect(decoded.index).toEqual(5n); + expect(decoded.subindex).toEqual(undefined); + }); + + test('small values', () => { + const addr = CborContractAddress.create(123n); + const encoded = CborContractAddress.toCBOR(addr); + const hex = Buffer.from(encoded).toString('hex'); + + // Tag 40919 containing uint(123) + const expected = `d99fd7 187b`.replace(/\s/g, ''); + expect(hex).toEqual(expected); + + const decoded = CborContractAddress.fromCBOR(encoded); + expect(decoded.index).toEqual(123n); + expect(decoded.subindex).toEqual(undefined); + }); + + test('large values', () => { + const addr = CborContractAddress.create(MAX_U64); + const encoded = CborContractAddress.toCBOR(addr); + const hex = Buffer.from(encoded).toString('hex'); + + // Tag 40919 containing uint(uint64_max) + const expected = `d99fd7 1bffffffffffffffff`.replace(/\s/g, ''); + expect(hex).toEqual(expected); + + const decoded = CborContractAddress.fromCBOR(encoded); + expect(decoded.index).toEqual(MAX_U64); + expect(decoded.subindex).toEqual(undefined); + }); + }); + + test('CBOR roundtrip without explicit subindex', () => { + const addr = CborContractAddress.create(77n); + const encoded = CborContractAddress.toCBOR(addr); + const rt = CborContractAddress.fromCBOR(encoded); + expect(rt.index).toBe(77n); + expect(rt.subindex).toBe(undefined); + }); + + test('CBOR roundtrip without explicit subindex 0', () => { + const addr = CborContractAddress.create(77n, 0n); + const encoded = CborContractAddress.toCBOR(addr); + const rt = CborContractAddress.fromCBOR(encoded); + expect(rt.index).toBe(77n); + expect(rt.subindex).toBe(undefined); + }); + + test('register encoder & decoder with cbor2', () => { + CborContractAddress.registerCBOREncoder(); + const cleanup = CborContractAddress.registerCBORDecoder(); + try { + const addr = CborContractAddress.create(123n, 456n); + const encoded = encode(addr); // automatic via registered encoder + const decoded: CborContractAddress.Type = decode(encoded); // automatic via decoder + expect(decoded.index).toBe(123n); + expect(decoded.subindex).toBe(456n); + } finally { + cleanup(); + } + }); + + describe('fromCBOR preserves original structure', () => { + test('no subindex', () => { + // Tag 40919 containing uint(5) + const hex = `d99fd7 05`.replace(/\s/g, ''); + const decoded = CborContractAddress.fromCBOR(Buffer.from(hex, 'hex')); + + expect(decoded.index).toBe(5n); + expect(decoded.subindex).toBe(undefined); + }); + + test('explicit 0 subindex', () => { + // Tag 40919 containing array with 2 values: uint(5) and uint(0) + const hex = `d99fd7 82 05 00`.replace(/\s/g, ''); + const decoded = CborContractAddress.fromCBOR(Buffer.from(hex, 'hex')); + + expect(decoded.index).toBe(5n); + expect(decoded.subindex).toBe(0n); + }); + }); + + describe('fromJSON preserves original structure', () => { + test('no subindex', () => { + const json = { index: '5' } as unknown as CborContractAddress.JSON; + const parsed = CborContractAddress.fromJSON(json); + + expect(parsed.index).toBe(5n); + expect(parsed.subindex).toBe(undefined); + }); + + test('explicit 0 subindex', () => { + const json = { index: '5', subindex: '0' } as unknown as CborContractAddress.JSON; + const parsed = CborContractAddress.fromJSON(json); + + expect(parsed.index).toBe(5n); + expect(parsed.subindex).toBe(0n); + }); + }); + + test('decoder rejects wrong tag', () => { + // Manually craft CBOR with wrong tag: use tag 0 instead of 40919 + const malformed = encode(new Tag(123, [1, 2])); + expect(() => CborContractAddress.fromCBOR(malformed)).toThrow(/expected tag 40919/); + }); + + test('decoder rejects malformed array length', () => { + // Tag 40919 with array length 1 + // Build Tag manually by encoding [tag, contents] + const malformed = encode(new Tag(40919, [1])); + expect(() => CborContractAddress.fromCBOR(malformed)).toThrow(/expected uint value or tuple/); + }); + + test('decoder rejects non-numeric contents', () => { + const malformed = encode(new Tag(40919, ['a', 'b'])); + expect(() => CborContractAddress.fromCBOR(malformed)).toThrow(/expected uint value/); + }); +}); diff --git a/packages/sdk/test/ci/plt/CborMemo.test.ts b/packages/sdk/test/ci/plt/CborMemo.test.ts index 8902b887a..50f3b337a 100644 --- a/packages/sdk/test/ci/plt/CborMemo.test.ts +++ b/packages/sdk/test/ci/plt/CborMemo.test.ts @@ -4,7 +4,7 @@ import { Tag } from 'cbor2/tag'; import { CborMemo } from '../../../src/plt/index.js'; -describe('CborMemo', () => { +describe('PLT CborMemo', () => { it('should throw an error if content exceeds 256 bytes', () => { const content = 't'.repeat(257); expect(() => CborMemo.fromString(content)).toThrow( diff --git a/packages/sdk/test/ci/plt/Token.test.ts b/packages/sdk/test/ci/plt/Token.test.ts index d699bbc61..5b5ee1e80 100644 --- a/packages/sdk/test/ci/plt/Token.test.ts +++ b/packages/sdk/test/ci/plt/Token.test.ts @@ -1,5 +1,14 @@ -import { TokenModuleAccountState, TokenModuleState, TokenTransfer } from '../../../src/plt/module.js'; -import { Cbor, Token, TokenAmount, TokenHolder, TokenId, TokenMetadataUrl } from '../../../src/pub/plt.ts'; +import { + Cbor, + CborAccountAddress, + Token, + TokenAmount, + TokenId, + TokenMetadataUrl, + TokenModuleAccountState, + TokenModuleState, + TokenTransfer, +} from '../../../src/pub/plt.js'; import { AccountAddress, AccountInfo } from '../../../src/pub/types.js'; const ACCOUNT_1 = AccountAddress.fromBase58('4UC8o4m8AgTxt5VBFMdLwMCwwJQVJwjesNzW7RPXkACynrULmd'); @@ -17,7 +26,7 @@ jest.mock('../../../src/grpc/GRPCClient.js', () => { }; }); -describe('Token.scaleAmount', () => { +describe('PLT Token.scaleAmount', () => { it('should scale token amount correctly when decimals are compatible', () => { let token: Token.Type = { info: { @@ -68,7 +77,7 @@ describe('Token.scaleAmount', () => { }); }); -describe('Token.validateAmount', () => { +describe('PLT Token.validateAmount', () => { it('should not throw an error when decimals match', () => { const token = createMockToken(8); const amount = TokenAmount.create(BigInt(100), 8); @@ -84,7 +93,7 @@ describe('Token.validateAmount', () => { }); }); -describe('Token.validateTransfer', () => { +describe('PLT Token.validateTransfer', () => { beforeEach(() => { jest.clearAllMocks(); }); @@ -100,7 +109,7 @@ describe('Token.validateTransfer', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), allowList: false, denyList: false, }; @@ -118,7 +127,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; // Should validate successfully @@ -138,7 +147,7 @@ describe('Token.validateTransfer', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -154,7 +163,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; // Should throw InsufficientFundsError @@ -172,7 +181,7 @@ describe('Token.validateTransfer', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), denyList: true, }; @@ -195,7 +204,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; // Should throw NotAllowedError @@ -212,7 +221,7 @@ describe('Token.validateTransfer', () => { const moduleState: TokenModuleState = { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), denyList: true, paused: false, }; @@ -236,7 +245,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; await expect(Token.validateTransfer(token, sender, transfer)).resolves.toBe(true); @@ -252,7 +261,7 @@ describe('Token.validateTransfer', () => { const moduleState: TokenModuleState = { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), allowList: true, paused: false, }; @@ -276,7 +285,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; // Should throw NotAllowedError @@ -294,7 +303,7 @@ describe('Token.validateTransfer', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), allowList: true, }; @@ -321,7 +330,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; // Should throw NotAllowedError @@ -339,7 +348,7 @@ describe('Token.validateTransfer', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -359,11 +368,11 @@ describe('Token.validateTransfer', () => { const transfers: TokenTransfer[] = [ { amount: TokenAmount.create(BigInt(300), decimals), - recipient: TokenHolder.fromAccountAddress(recipient1), + recipient: CborAccountAddress.fromAccountAddress(recipient1), }, { amount: TokenAmount.create(BigInt(400), decimals), - recipient: TokenHolder.fromAccountAddress(recipient2), + recipient: CborAccountAddress.fromAccountAddress(recipient2), }, ]; @@ -382,7 +391,7 @@ describe('Token.validateTransfer', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: true, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -402,7 +411,7 @@ describe('Token.validateTransfer', () => { const transferAmount = TokenAmount.create(BigInt(500), decimals); const transfer: TokenTransfer = { amount: transferAmount, - recipient: TokenHolder.fromAccountAddress(recipient), + recipient: CborAccountAddress.fromAccountAddress(recipient), }; // Should throw UnsupportedOperationError @@ -410,7 +419,7 @@ describe('Token.validateTransfer', () => { }); }); -describe('Token.validateMint', () => { +describe('PLT Token.validateMint', () => { it('should throw NotMintableError when the token is not mintable', async () => { const sender = ACCOUNT_1; const tokenId = TokenId.fromString('3f1bfce9'); @@ -421,7 +430,7 @@ describe('Token.validateMint', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), mintable: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -431,7 +440,7 @@ describe('Token.validateMint', () => { }); }); -describe('Token.validateBurn', () => { +describe('PLT Token.validateBurn', () => { it('should throw NotBurnableError when the token is not burnable', async () => { const sender = ACCOUNT_1; const tokenId = TokenId.fromString('3f1bfce9'); @@ -442,7 +451,7 @@ describe('Token.validateBurn', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), burnable: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -452,7 +461,7 @@ describe('Token.validateBurn', () => { }); }); -describe('Token.validateAllowListUpdate', () => { +describe('PLT Token.validateAllowListUpdate', () => { it('should throw NoAllowListError when the token has no allow list', async () => { const sender = ACCOUNT_1; const tokenId = TokenId.fromString('3f1bfce9'); @@ -462,7 +471,7 @@ describe('Token.validateAllowListUpdate', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), allowList: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -471,7 +480,7 @@ describe('Token.validateAllowListUpdate', () => { }); }); -describe('Token.validateDenyListUpdate', () => { +describe('PLT Token.validateDenyListUpdate', () => { it('should throw NoDenyListError when the token has no deny list', async () => { const sender = ACCOUNT_1; const tokenId = TokenId.fromString('3f1bfce9'); @@ -481,7 +490,7 @@ describe('Token.validateDenyListUpdate', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), denyList: false, - governanceAccount: TokenHolder.fromAccountAddress(sender), + governanceAccount: CborAccountAddress.fromAccountAddress(sender), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -490,7 +499,7 @@ describe('Token.validateDenyListUpdate', () => { }); }); -describe('Token update supply operation', () => { +describe('PLT Token update supply operation', () => { it('should throw PausedError when trying to mint/burn tokens while token is paused', async () => { const governanceAddress = ACCOUNT_1; const tokenId = TokenId.fromString('3f1bfce9'); @@ -502,7 +511,7 @@ describe('Token update supply operation', () => { name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: true, - governanceAccount: TokenHolder.fromAccountAddress(governanceAddress), + governanceAccount: CborAccountAddress.fromAccountAddress(governanceAddress), }; const token = createMockToken(decimals, moduleState, tokenId); @@ -526,7 +535,7 @@ function createMockToken( name: 'Test Token', metadata: TokenMetadataUrl.fromString('https://example.com/metadata'), paused: false, - governanceAccount: TokenHolder.fromAccountAddress(ACCOUNT_1), + governanceAccount: CborAccountAddress.fromAccountAddress(ACCOUNT_1), }, tokenId: TokenId.Type = TokenId.fromString('3f1bfce9') ): Token.Type { diff --git a/packages/sdk/test/ci/plt/TokenHolder.test.ts b/packages/sdk/test/ci/plt/TokenHolder.test.ts deleted file mode 100644 index 56a18b89f..000000000 --- a/packages/sdk/test/ci/plt/TokenHolder.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Buffer } from 'buffer/index.js'; -import { decode, encode } from 'cbor2'; - -import { TokenHolder } from '../../../src/pub/plt.ts'; -import { AccountAddress } from '../../../src/types/index.ts'; - -describe('TokenHolder CBOR', () => { - test('Account address cbor encoding', () => { - const address = TokenHolder.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); - const encoded = TokenHolder.toCBOR(address); - // This expected buffer represents the CBOR encoding of an TokenHolder with: - // - Tag 40307 (0x9d73) for Address - // - Two properties: - // - Key 01: Tagged CoinInfo (40305/0x9d71) with discriminant 919 (0x397) for Concordium - // - Key 03: 32-byte address filled with 0x15 - const expected = Buffer.from( - ` - d99d73 a2 - 01 - d99d71 - a1 - 01 19 0397 - 03 5820 1515151515151515151515151515151515151515151515151515151515151515 - `.replace(/\s/g, ''), - 'hex' - ); - expect(Buffer.from(encoded).toString('hex')).toEqual(expected.toString('hex')); - }); - - test('Account address cbor decoding with tagged-coininfo', () => { - // Create a test address with known bytes - const originalAddress = TokenHolder.fromAccountAddress( - AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)) - ); - - const encoded = TokenHolder.toCBOR(originalAddress); - const decoded = TokenHolder.fromCBOR(encoded); - - // Verify the decoded address matches the original - expect(decoded).toEqual(originalAddress); - }); - - test('Account address cbor decoding without tagged-coininfo', () => { - // Create a test address with known bytes - const originalAddress = TokenHolder.fromAccountAddressNoCoinInfo( - AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)) - ); - - // Create CBOR bytes manually without the tagged-coininfo (only the address bytes) - // This buffer represents the simplified CBOR encoding of an TokenHolder with: - // - Tag 40307 (0x9d73) for Address - // - One property: - // - Key 03: 32-byte address filled with 0x15 - const taggedAddressBytes = Buffer.from( - ` - d99d73 a1 - 03 - 5820 1515151515151515151515151515151515151515151515151515151515151515 - `.replace(/\s/g, ''), - 'hex' - ); - - const decoded = TokenHolder.fromCBOR(taggedAddressBytes); - - // Verify the decoded address matches the original - expect(decoded).toEqual(originalAddress); - }); - - test('CBOR encoding/decoding with cbor2 library registration', () => { - // Register the TokenHolder encoder and decoder - TokenHolder.registerCBOREncoder(); - const cleanup = TokenHolder.registerCBORDecoder(); - - try { - // Create a test address - const originalAddress = TokenHolder.fromAccountAddress( - AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)) - ); - - // Encode directly with cbor2 library - const encoded = encode(originalAddress); - // This hex string represents the expected CBOR encoding of an TokenHolder with: - // - Tag 40307 (0x9d73) for Address - // - Two properties: - // - Key 01: Tagged CoinInfo (40305/0x9d71) with discriminant 919 (0x397) for Concordium - // - Key 03: 32-byte address filled with 0x15 - const taggedAddressHex = ` - d99d73 a2 - 01 - d99d71 - a1 - 01 19 0397 - 03 5820 1515151515151515151515151515151515151515151515151515151515151515 - `.replace(/\s/g, ''); - - // Check that the encoded value follows the expected structure - // The address should be tagged with 40307 (0x9d73) - expect(Buffer.from(encoded).toString('hex')).toEqual(taggedAddressHex); - - // Decode directly with cbor2 library (should use our registered decoder) - const decoded: TokenHolder.Type = decode(encoded); - - // Verify it's an TokenHolder instance - expect(TokenHolder.instanceOf(decoded)).toBeTruthy(); - - // Verify the address matches the original - expect(decoded).toEqual(originalAddress); - - // Test encode-decode roundtrip with cbor2 - const roundtrip: TokenHolder.Type = decode(encode(originalAddress)); - expect(roundtrip).toEqual(originalAddress); - } finally { - // Clean up the registered decoder - cleanup(); - } - }); -}); diff --git a/packages/sdk/test/ci/plt/TokenMetadataUrl.test.ts b/packages/sdk/test/ci/plt/TokenMetadataUrl.test.ts index e18296800..8a3b14a70 100644 --- a/packages/sdk/test/ci/plt/TokenMetadataUrl.test.ts +++ b/packages/sdk/test/ci/plt/TokenMetadataUrl.test.ts @@ -2,7 +2,7 @@ import { encode } from 'cbor2'; import { Cbor, TokenMetadataUrl } from '../../../src/pub/plt.js'; -describe('TokenMetadataUrl', () => { +describe('PLT TokenMetadataUrl', () => { it('should create a TokenMetadataUrl instance using the create function', () => { const url = 'https://example.com'; const checksum = new Uint8Array(32); diff --git a/packages/sdk/test/ci/plt/TokenModuleEvent.test.ts b/packages/sdk/test/ci/plt/TokenModuleEvent.test.ts new file mode 100644 index 000000000..ad4613e9b --- /dev/null +++ b/packages/sdk/test/ci/plt/TokenModuleEvent.test.ts @@ -0,0 +1,122 @@ +import { AccountAddress, EncodedTokenModuleEvent, TransactionEventTag } from '../../../src/index.ts'; +import { + Cbor, + CborAccountAddress, + TokenId, + TokenListUpdateEventDetails, + TokenPauseEventDetails, + parseTokenModuleEvent, +} from '../../../src/plt/index.ts'; + +describe('PLT TokenModuleEvent', () => { + const testEventParsing = (type: string, targetValue: number) => { + it(`parses ${type} events correctly`, () => { + const accountBytes = new Uint8Array(32).fill(targetValue); + const details: TokenListUpdateEventDetails = { + target: CborAccountAddress.fromAccountAddress(AccountAddress.fromBuffer(accountBytes)), + }; + const validEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + type, + tokenId: TokenId.fromString('PLT'), + details: Cbor.encode(details), + }; + expect(validEvent.details.toString()).toEqual( + `a166746172676574d99d73a201d99d71a101190397035820${Buffer.from(accountBytes).toString('hex')}` + ); + + const parsedEvent = parseTokenModuleEvent(validEvent)!; + expect(parsedEvent.type).toEqual(type); + expect((parsedEvent.details as TokenListUpdateEventDetails).target.address.decodedAddress).toEqual( + new Uint8Array(32).fill(targetValue) + ); + }); + }; + + testEventParsing('addAllowList', 0x15); + testEventParsing('addDenyList', 0x16); + testEventParsing('removeAllowList', 0x17); + testEventParsing('removeDenyList', 0x18); + + it('parses pause event', () => { + const details: TokenPauseEventDetails = {}; + const validEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'pause', + details: Cbor.encode(details), + }; + expect(validEvent.details.toString()).toEqual('a0'); + + const parsedEvent = parseTokenModuleEvent(validEvent)!; + expect(parsedEvent.type).toEqual('pause'); + expect(parsedEvent.details).toEqual({}); + }); + + it('parses unpause event', () => { + const details: TokenPauseEventDetails = {}; + const validEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'unpause', + details: Cbor.encode(details), + }; + expect(validEvent.details.toString()).toEqual('a0'); + + const parsedEvent = parseTokenModuleEvent(validEvent)!; + expect(parsedEvent.type).toEqual('unpause'); + expect(parsedEvent.details).toEqual({}); + }); + + it('handles unknown events', () => { + const unknownEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'unknown', + details: Cbor.encode({}), + }; + const parsed = parseTokenModuleEvent(unknownEvent); + expect(parsed.type).toEqual('unknown'); + expect(parsed.details).toEqual({}); + }); + + it('throws an error for invalid event details for list update events', () => { + const invalidDetailsEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'addAllowList', + details: Cbor.encode(null), + }; + expect(() => parseTokenModuleEvent(invalidDetailsEvent)).toThrowError(/null/); + }); + + it("throws an error if 'target' is missing or invalid for list update events", () => { + const missingTargetEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'addAllowList', + details: Cbor.encode({}), + }; + expect(() => parseTokenModuleEvent(missingTargetEvent)).toThrowError(/{}/); + }); + + it('throws an error for invalid event details for pause events', () => { + const invalidEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'pause', + details: Cbor.encode(null), + }; + expect(() => parseTokenModuleEvent(invalidEvent)).toThrowError(/null/); + }); + + it('throws an error for invalid event details for unpause events', () => { + const invalidEvent: EncodedTokenModuleEvent = { + tag: TransactionEventTag.TokenModuleEvent, + tokenId: TokenId.fromString('PLT'), + type: 'unpause', + details: Cbor.encode(null), + }; + expect(() => parseTokenModuleEvent(invalidEvent)).toThrowError(/null/); + }); +}); diff --git a/packages/sdk/test/ci/plt/TokenModuleRejectReason.test.ts b/packages/sdk/test/ci/plt/TokenModuleRejectReason.test.ts new file mode 100644 index 000000000..97ab0619d --- /dev/null +++ b/packages/sdk/test/ci/plt/TokenModuleRejectReason.test.ts @@ -0,0 +1,182 @@ +import { + AddressNotFoundDetails, + AddressNotFoundRejectReason, + Cbor, + CborAccountAddress, + DeserializationFailureDetails, + DeserializationFailureRejectReason, + EncodedTokenModuleRejectReason, + MintWouldOverflowDetails, + MintWouldOverflowRejectReason, + OperationNotPermittedDetails, + OperationNotPermittedRejectReason, + TokenAmount, + TokenBalanceInsufficientDetails, + TokenBalanceInsufficientRejectReason, + TokenId, + TokenRejectReasonType, + UnknownTokenRejectReason, + UnsupportedOperationDetails, + UnsupportedOperationRejectReason, + parseTokenModuleRejectReason, +} from '../../../src/pub/plt.ts'; +import { AccountAddress } from '../../../src/pub/types.ts'; + +function amt(value: number, decimals = 0) { + return TokenAmount.create(BigInt(value), decimals); +} + +function dummyTokenId() { + return TokenId.fromString('PLT'); +} + +describe('PLT TokenModuleRejectReason', () => { + it('parses addressNotFound correctly', () => { + const addrBytes = new Uint8Array(32).fill(0x11); + const details: AddressNotFoundDetails = { + index: 2, + address: CborAccountAddress.fromAccountAddress(AccountAddress.fromBuffer(addrBytes)), + }; + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.AddressNotFound, + details: Cbor.encode(details), + }; + const parsed = parseTokenModuleRejectReason(encoded) as AddressNotFoundRejectReason; + expect(parsed.type).toBe(TokenRejectReasonType.AddressNotFound); + expect(parsed.details.index).toBe(2); + }); + + it('parses tokenBalanceInsufficient correctly', () => { + const details: TokenBalanceInsufficientDetails = { + index: 0, + availableBalance: amt(100), + requiredBalance: amt(200), + }; + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.TokenBalanceInsufficient, + details: Cbor.encode(details), + }; + const parsed = parseTokenModuleRejectReason(encoded) as TokenBalanceInsufficientRejectReason; + expect(parsed.type).toBe(TokenRejectReasonType.TokenBalanceInsufficient); + expect(parsed.details.availableBalance.value).toBe(100n); + expect(parsed.details.requiredBalance.value).toBe(200n); + }); + + it('parses deserializationFailure correctly (with cause)', () => { + const details: DeserializationFailureDetails = { cause: 'bad format' }; + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.DeserializationFailure, + details: Cbor.encode(details), + }; + const parsed = parseTokenModuleRejectReason(encoded) as DeserializationFailureRejectReason; + expect(parsed.type).toBe(TokenRejectReasonType.DeserializationFailure); + expect(parsed.details.cause).toBe('bad format'); + }); + + it('parses unsupportedOperation correctly', () => { + const details: UnsupportedOperationDetails = { index: 3, operationType: 'freeze', reason: 'disabled' }; + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.UnsupportedOperation, + details: Cbor.encode(details), + }; + const parsed = parseTokenModuleRejectReason(encoded) as UnsupportedOperationRejectReason; + expect(parsed.type).toBe(TokenRejectReasonType.UnsupportedOperation); + expect(parsed.details.operationType).toBe('freeze'); + expect(parsed.details.reason).toBe('disabled'); + }); + + it('parses operationNotPermitted correctly (with address + reason)', () => { + const addrBytes = new Uint8Array(32).fill(0x22); + const details: OperationNotPermittedDetails = { + index: 5, + address: CborAccountAddress.fromAccountAddress(AccountAddress.fromBuffer(addrBytes)), + reason: 'paused', + }; + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.OperationNotPermitted, + details: Cbor.encode(details), + }; + const parsed = parseTokenModuleRejectReason(encoded) as OperationNotPermittedRejectReason; + expect(parsed.type).toBe(TokenRejectReasonType.OperationNotPermitted); + expect(parsed.details.index).toBe(5); + expect(parsed.details.reason).toBe('paused'); + }); + + it('parses mintWouldOverflow correctly', () => { + const details: MintWouldOverflowDetails = { + index: 7, + requestedAmount: amt(1000), + currentSupply: amt(9000), + maxRepresentableAmount: amt(9999), + }; + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.MintWouldOverflow, + details: Cbor.encode(details), + }; + const parsed = parseTokenModuleRejectReason(encoded) as MintWouldOverflowRejectReason; + expect(parsed.type).toBe(TokenRejectReasonType.MintWouldOverflow); + expect(parsed.details.currentSupply.value).toBe(9000n); + }); + + it('returns unknown variant for unrecognized reject reason type', () => { + const encoded: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: 'someNewFutureReason', + details: Cbor.encode({ extra: 1 }), + }; + const parsed = parseTokenModuleRejectReason(encoded) as UnknownTokenRejectReason; + expect(parsed.type).toBe('someNewFutureReason'); + expect(parsed.details).toEqual({ extra: 1 }); + }); + + it('throws for invalid details: addressNotFound missing address', () => { + const bad: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.AddressNotFound, + details: Cbor.encode({ index: 0 }), + }; + expect(() => parseTokenModuleRejectReason(bad)).toThrow(/address/); + }); + + it('throws for invalid details: tokenBalanceInsufficient wrong availableBalance type', () => { + const bad: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.TokenBalanceInsufficient, + details: Cbor.encode({ index: 0, availableBalance: 5, requiredBalance: amt(10) }), + }; + expect(() => parseTokenModuleRejectReason(bad)).toThrow(/availableBalance/); + }); + + it('throws for invalid details: unsupportedOperation missing operationType', () => { + const bad: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.UnsupportedOperation, + details: Cbor.encode({ index: 2 }), + }; + expect(() => parseTokenModuleRejectReason(bad)).toThrow(/operationType/); + }); + + it('throws for invalid details: operationNotPermitted wrong address type', () => { + const bad: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.OperationNotPermitted, + details: Cbor.encode({ index: 1, address: 'notAHolder' }), + }; + expect(() => parseTokenModuleRejectReason(bad)).toThrow(/address/); + }); + + it('throws for invalid details: mintWouldOverflow missing requestedAmount', () => { + const bad: EncodedTokenModuleRejectReason = { + tokenId: dummyTokenId(), + type: TokenRejectReasonType.MintWouldOverflow, + details: Cbor.encode({ index: 1, currentSupply: amt(1), maxRepresentableAmount: amt(2) }), + }; + expect(() => parseTokenModuleRejectReason(bad)).toThrow(/requestedAmount/); + }); +}); diff --git a/packages/sdk/test/ci/plt/module.test.ts b/packages/sdk/test/ci/plt/TokenOperation.test.ts similarity index 70% rename from packages/sdk/test/ci/plt/module.test.ts rename to packages/sdk/test/ci/plt/TokenOperation.test.ts index af827b165..6b50c490d 100644 --- a/packages/sdk/test/ci/plt/module.test.ts +++ b/packages/sdk/test/ci/plt/TokenOperation.test.ts @@ -1,146 +1,36 @@ -import { Cursor } from '../../../src/deserializationHelpers.js'; -import { TokenHolder } from '../../../src/plt/index.ts'; +import { Cursor } from '../../../src/deserializationHelpers.ts'; import { + Cbor, + CborAccountAddress, TokenAddAllowListOperation, TokenAddDenyListOperation, + TokenAmount, TokenBurnOperation, - TokenListUpdateEventDetails, + TokenId, TokenMintOperation, TokenOperationType, - TokenPauseEventDetails, TokenPauseOperation, TokenRemoveAllowListOperation, TokenRemoveDenyListOperation, TokenTransferOperation, TokenUnpauseOperation, + UnknownTokenOperation, createTokenUpdatePayload, - parseModuleEvent, -} from '../../../src/plt/module.js'; -import { Cbor, TokenAmount, TokenId } from '../../../src/pub/plt.js'; + parseTokenUpdatePayload, +} from '../../../src/pub/plt.ts'; import { AccountAddress, AccountTransactionType, - EncodedTokenModuleEvent, TokenUpdateHandler, - TransactionEventTag, + TokenUpdatePayload, serializeAccountTransactionPayload, -} from '../../../src/pub/types.js'; - -describe('PLT parseModuleEvent', () => { - const testEventParsing = (type: string, targetValue: number) => { - it(`parses ${type} events correctly`, () => { - const accountBytes = new Uint8Array(32).fill(targetValue); - const details: TokenListUpdateEventDetails = { - target: TokenHolder.fromAccountAddress(AccountAddress.fromBuffer(accountBytes)), - }; - const validEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - type, - tokenId: TokenId.fromString('PLT'), - details: Cbor.encode(details), - }; - expect(validEvent.details.toString()).toEqual( - `a166746172676574d99d73a201d99d71a101190397035820${Buffer.from(accountBytes).toString('hex')}` - ); - - const parsedEvent = parseModuleEvent(validEvent); - expect(parsedEvent.type).toEqual(type); - expect((parsedEvent.details as TokenListUpdateEventDetails).target.type).toEqual('account'); - expect((parsedEvent.details as TokenListUpdateEventDetails).target.address.decodedAddress).toEqual( - new Uint8Array(32).fill(targetValue) - ); - }); - }; - - testEventParsing('addAllowList', 0x15); - testEventParsing('addDenyList', 0x16); - testEventParsing('removeAllowList', 0x17); - testEventParsing('removeDenyList', 0x18); - - it('parses pause event', () => { - const details: TokenPauseEventDetails = {}; - const validEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'pause', - details: Cbor.encode(details), - }; - expect(validEvent.details.toString()).toEqual('a0'); - - const parsedEvent = parseModuleEvent(validEvent); - expect(parsedEvent.type).toEqual('pause'); - expect(parsedEvent.details).toEqual({}); - }); - - it('parses unpause event', () => { - const details: TokenPauseEventDetails = {}; - const validEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'unpause', - details: Cbor.encode(details), - }; - expect(validEvent.details.toString()).toEqual('a0'); - - const parsedEvent = parseModuleEvent(validEvent); - expect(parsedEvent.type).toEqual('unpause'); - expect(parsedEvent.details).toEqual({}); - }); - - it('throws an error for invalid event type', () => { - const invalidEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'invalidType', - details: Cbor.encode({}), - }; - expect(() => parseModuleEvent(invalidEvent)).toThrowError(/invalidType/); - }); - - it('throws an error for invalid event details for list update events', () => { - const invalidDetailsEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'addAllowList', - details: Cbor.encode(null), - }; - expect(() => parseModuleEvent(invalidDetailsEvent)).toThrowError(/null/); - }); - - it("throws an error if 'target' is missing or invalid for list update events", () => { - const missingTargetEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'addAllowList', - details: Cbor.encode({}), - }; - expect(() => parseModuleEvent(missingTargetEvent)).toThrowError(/{}/); - }); - - it('throws an error for invalid event details for pause events', () => { - const invalidEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'pause', - details: Cbor.encode(null), - }; - expect(() => parseModuleEvent(invalidEvent)).toThrowError(/null/); - }); +} from '../../../src/pub/types.ts'; - it('throws an error for invalid event details for unpause events', () => { - const invalidEvent: EncodedTokenModuleEvent = { - tag: TransactionEventTag.TokenModuleEvent, - tokenId: TokenId.fromString('PLT'), - type: 'unpause', - details: Cbor.encode(null), - }; - expect(() => parseModuleEvent(invalidEvent)).toThrowError(/null/); - }); -}); - -describe('PLT transactions', () => { +describe('PLT TokenOperation', () => { const token = TokenId.fromString('DKK'); - const testAccountAddress = TokenHolder.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); + const testAccountAddress = CborAccountAddress.fromAccountAddress( + AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15)) + ); // - d99d73: A tagged (40307) item with a map (a2) containing: // - a2: A map with 2 key-value pairs // - 01: Key 1. @@ -193,12 +83,8 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([transfer]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const expected = Buffer.concat([Buffer.from('1b03444b4b00000052', 'hex'), expectedOperations]); expect(ser.toString('hex')).toEqual(expected.toString('hex')); @@ -206,6 +92,9 @@ describe('PLT transactions', () => { const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([transfer]); }); it('(de)serializes mint operations correctly', () => { @@ -239,16 +128,15 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([mint]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([mint]); }); it('(de)serializes burn operations correctly', () => { @@ -282,16 +170,15 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([burn]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([burn]); }); it('(de)serializes addAllowList operations correctly', () => { @@ -319,16 +206,15 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([addAllowList]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([addAllowList]); }); it('(de)serializes removeAllowList operations correctly', () => { @@ -356,16 +242,15 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([removeAllowList]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([removeAllowList]); }); it('(de)serializes addDenyList operations correctly', () => { @@ -393,16 +278,15 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([addDenyList]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([addDenyList]); }); it('(de)serializes removeDenyList operations correctly', () => { @@ -430,24 +314,23 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([removeDenyList]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([removeDenyList]); }); it('(de)serializes unpause operations correctly', () => { - const removeDenyList: TokenUnpauseOperation = { + const uppause: TokenUnpauseOperation = { [TokenOperationType.Unpause]: {}, }; - const payload = createTokenUpdatePayload(token, removeDenyList); + const payload = createTokenUpdatePayload(token, uppause); // This is a CBOR encoded byte sequence representing the unpause operation: // - 81: An array of 1 item @@ -461,24 +344,23 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([removeDenyList]); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([uppause]); }); it('(de)serializes pause operations correctly', () => { - const removeDenyList: TokenPauseOperation = { + const pause: TokenPauseOperation = { [TokenOperationType.Pause]: {}, }; - const payload = createTokenUpdatePayload(token, removeDenyList); + const payload = createTokenUpdatePayload(token, pause); // This is a CBOR encoded byte sequence representing the pause operation: // - 81: An array of 1 item @@ -492,16 +374,49 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual([removeDenyList]); + const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); + const serPayload = ser.slice(1); + const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); + expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([pause]); + }); + + it('(de)serializes unknown operations correctly', () => { + const unknown: UnknownTokenOperation = { + unknownOperation: { test: 'something', test2: 123 }, + }; + + const payload: TokenUpdatePayload = { tokenId: token, operations: Cbor.encode([unknown]) }; + + // This is a CBOR encoded byte sequence representing the pause operation: + // - 81: An array of 1 item + // - a1: A map with 1 key-value pair + // - 70 756e6b6e6f776e4f7065726174696f6e a2: A string key "unknownOperation" with map with 2 keys as the value + // - 64 74657374 69 736f6d657468696e67: a string key "test" with a string value "something" + // - 65 7465737432 18 7b: a string key "test2" with an unsigned int value 123 + const expectedOperations = Buffer.from( + ` + 81 + a1 + 70 756e6b6e6f776e4f7065726174696f6e a2 + 64 74657374 69 736f6d657468696e67 + 65 7465737432 18 7b + `.replace(/\s/g, ''), + 'hex' + ); + expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual([unknown]); }); it('(de)serializes multiple governance operations correctly', () => { @@ -553,15 +468,14 @@ describe('PLT transactions', () => { `.replace(/\s/g, ''), 'hex' ); - expect(payload.operations.toString()).toEqual(expectedOperations.toString('hex')); - const decoded = Cbor.decode(payload.operations); - expect(decoded).toEqual(operations); - const ser = serializeAccountTransactionPayload({ payload, type: AccountTransactionType.TokenUpdate }); const serPayload = ser.slice(1); const des = new TokenUpdateHandler().deserialize(Cursor.fromBuffer(serPayload)); expect(des).toEqual(payload); + + const parsed = parseTokenUpdatePayload(des); + expect(parsed.operations).toEqual(operations); }); }); diff --git a/packages/sdk/test/ci/types/cbor.test.ts b/packages/sdk/test/ci/types/cbor.test.ts index aec293a49..9a6ac9bb0 100644 --- a/packages/sdk/test/ci/types/cbor.test.ts +++ b/packages/sdk/test/ci/types/cbor.test.ts @@ -1,8 +1,8 @@ -import { CborMemo, TokenHolder } from '../../../src/pub/plt.ts'; +import { CborAccountAddress, CborMemo } from '../../../src/pub/plt.ts'; import { AccountAddress, cborEncode } from '../../../src/pub/types.ts'; it('should encode types and type compositions correctly', () => { - const account = TokenHolder.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); + const account = CborAccountAddress.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); const accountCbor = cborEncode(account); // CBOR byte sequence is as follows: // - d99d73 a2: A tagged (40307) item containing a map with 2 key-value pairs @@ -42,7 +42,7 @@ it('should encode types and type compositions correctly', () => { }); it('should lexicographically sort object keys when encoding', () => { - const account = TokenHolder.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); + const account = CborAccountAddress.fromAccountAddress(AccountAddress.fromBuffer(new Uint8Array(32).fill(0x15))); // CBOR byte sequence is as follows: // - d99d73 a2: A tagged (40307) item containing a map with 2 key-value pairs // - 01 d99d71 a1: Key 1 => d99d71: A tagged (40305) item containing a map with 1 key-value pair: diff --git a/packages/sdk/test/client/CIS2Contract.test.ts b/packages/sdk/test/client/CIS2Contract.test.ts index f36a133cf..d42f12d32 100644 --- a/packages/sdk/test/client/CIS2Contract.test.ts +++ b/packages/sdk/test/client/CIS2Contract.test.ts @@ -101,8 +101,8 @@ test('dryRun.transfer', async () => { // Results in 1 transfer event expect( result.tag === 'success' && - result.events[0].tag === TransactionEventTag.Updated && - result.events[0].events.length + result.events[0]!.tag === TransactionEventTag.Updated && + result.events[0]!.events.length ).toBe(1); const resultMulti = await cis2.dryRun.transfer( @@ -128,8 +128,8 @@ test('dryRun.transfer', async () => { // Results in 2 transfer events expect( resultMulti.tag === 'success' && - resultMulti.events[0].tag === TransactionEventTag.Updated && - resultMulti.events[0].events.length + resultMulti.events[0]!.tag === TransactionEventTag.Updated && + resultMulti.events[0]!.events.length ).toBe(2); const resultContractReceiver = await cis2.dryRun.transfer( @@ -250,8 +250,8 @@ test('dryRun.updateOperator', async () => { // Results in 1 transfer event expect( result.tag === 'success' && - result.events[0].tag === TransactionEventTag.Updated && - result.events[0].events.length + result.events[0]!.tag === TransactionEventTag.Updated && + result.events[0]!.events.length ).toEqual(1); const resultMulti = await cis2.dryRun.updateOperator( @@ -273,8 +273,8 @@ test('dryRun.updateOperator', async () => { // Results in 2 transfer events expect( resultMulti.tag === 'success' && - resultMulti.events[0].tag === TransactionEventTag.Updated && - resultMulti.events[0].events.length + resultMulti.events[0]!.tag === TransactionEventTag.Updated && + resultMulti.events[0]!.events.length ).toEqual(2); }); diff --git a/packages/sdk/test/client/cis3Util.test.ts b/packages/sdk/test/client/cis3Util.test.ts index 3954f7307..dea02e8a4 100644 --- a/packages/sdk/test/client/cis3Util.test.ts +++ b/packages/sdk/test/client/cis3Util.test.ts @@ -10,7 +10,7 @@ const SPONSOREE = AccountAddress.fromBase58('4NgCvVSCuCyHkALqbAnSX3QEC7zrfoZbig7 async function getBlockItemSummary(): Promise { const nodeClient = getNodeClientV2(); const bi = await nodeClient.waitForTransactionFinalization(TRANSACTION_HASH); - return bi.summary; + return bi.summary!; } test('CIS3 nonce events are deserialized correctly', async () => { diff --git a/packages/sdk/test/client/clientV2.test.ts b/packages/sdk/test/client/clientV2.test.ts index ce97a9792..a6cbf33dc 100644 --- a/packages/sdk/test/client/clientV2.test.ts +++ b/packages/sdk/test/client/clientV2.test.ts @@ -9,7 +9,7 @@ import { BlockHash, buildBasicAccountSigner, calculateEnergyCost, - createCredentialDeploymentTransaction, + createCredentialDeploymentPayload, getAccountTransactionHandler, getCredentialDeploymentSignDigest, serializeAccountTransaction, @@ -187,7 +187,7 @@ test.each(clients)('Failed invoke contract', async (client) => { } expect(result.usedEnergy.value).toBe(340n); - expect(result.reason.tag).toBe(v1.RejectReasonTag.RejectedReceive); + expect(result.reason?.tag).toBe(v1.RejectReasonTag.RejectedReceive); }); test.each(clients)('Invoke contract on v0 contract', async (client) => { @@ -351,7 +351,7 @@ test.each(clients)('createAccount', async (client) => { }, ]; - const credentialDeploymentTransaction: v1.CredentialDeploymentTransaction = createCredentialDeploymentTransaction( + const credentialDeploymentTransaction: v1.CredentialDeploymentPayload = createCredentialDeploymentPayload( identityInput, cryptoParams, threshold, diff --git a/packages/sdk/test/client/events.test.ts b/packages/sdk/test/client/events.test.ts index 7fb8ad1f3..eca0713ad 100644 --- a/packages/sdk/test/client/events.test.ts +++ b/packages/sdk/test/client/events.test.ts @@ -11,7 +11,7 @@ test('accountCreated', async () => { const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - expect(events[0]).toEqual(expected.accountCreationEvent); + expect(events[0]!).toEqual(expected.accountCreationEvent); }); // EncryptedAmountsRemoved, AmountAddedByDecryption @@ -19,7 +19,7 @@ test('transferToPublic', async () => { const blockHash = BlockHash.fromHexString('e59ba7559e2de14e1bd4c05ddbfca808dd5b870cd89eec3942ae29f842906262'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'transferToPublic') { const transferToPublicEvent = [event.removed, event.added]; @@ -36,7 +36,7 @@ test('configureBaker: Add baker', async () => { const blockHash = BlockHash.fromHexString('04d24b3d44e4ec4681c279424bd276215809a6af64e57fd20cd907a08d998f09'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureBaker') { expect(event.events).toEqual(expected.configureBaker); @@ -50,10 +50,10 @@ test('configureBaker: Remove baker', async () => { const blockHash = BlockHash.fromHexString('2aa7c4a54ad403a9f9b48de2469e5f13a64c95f2cf7a8e72c0f9f7ae0718f642'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureBaker') { - expect(event.events[0]).toEqual(expected.bakerRemoved); + expect(event.events[0]!).toEqual(expected.bakerRemoved); } else { throw Error('Wrong event.'); } @@ -65,7 +65,7 @@ test('configureDelegation', async () => { const blockHash = BlockHash.fromHexString('9cf7f3ba97e027f08bc3dc779e6eb4aadaecee0899a532224846196f646921f3'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureDelegation') { expect(event.events).toEqual(expected.configureDelegation); @@ -79,7 +79,7 @@ test.only('contract update', async () => { const blockHash = BlockHash.fromHexString('a74a3914143eb596132c74685fac1314f6d5e8bb393e3372e83726f0c4654de2'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'update') { expect(event.events).toEqual(expected.updateEvent); @@ -93,7 +93,7 @@ test('transferToEncrypted', async () => { const blockHash = BlockHash.fromHexString('0254312274ccd192288ca49923c6571ae64d7d0ef57923a68d4c1b055e2ca757'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'transferToEncrypted') { expect(event.added).toEqual(expected.encryptedSelfAmountAddedEvent); @@ -108,7 +108,7 @@ test('UpdateEnqueued', async () => { const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - expect(events[0]).toEqual(expected.updateEnqueuedEvent); + expect(events[0]!).toEqual(expected.updateEnqueuedEvent); }); // ContractInitialized @@ -116,7 +116,7 @@ test('ContractInitialized', async () => { const blockHash = BlockHash.fromHexString('70dbb294060878220505e928d616dde2d90cf5eeee0a92d3fdc1268334ace89e'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'initContract') { expect(event.contractInitialized).toEqual(expected.contractInitializedEvent); @@ -130,7 +130,7 @@ test('ModuleDeployed', async () => { const blockHash = BlockHash.fromHexString('c7fd8efa319942d54336ccdfe8460a0591a2a4b3a6bac65fe552198d530105d1'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'deployModule') { expect(event.moduleDeployed).toEqual(expected.moduleDeployedEvent); @@ -144,10 +144,10 @@ test('DelegationRemoved', async () => { const blockHash = BlockHash.fromHexString('65ad6b6a4c9eaccb99a01e2661fcc588a411beb0ed91d39ac692359d5a666631'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[1]; + const event = events[1]!!; if (event.type === 'accountTransaction' && event.transactionType === 'configureDelegation') { - expect(event.events[0]).toEqual(expected.delegationRemovedEvent); + expect(event.events[0]!).toEqual(expected.delegationRemovedEvent); } else { throw Error('Wrong event.'); } @@ -158,7 +158,7 @@ test('TransferMemo', async () => { const blockHash = BlockHash.fromHexString('df96a12cc515bc863ed7021154494c8747e321565ff8b788066f0308c2963ece'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'transferWithMemo') { expect(event).toEqual(expected.transferWithMemoSummary); @@ -172,10 +172,10 @@ test('Upgraded', async () => { const blockHash = BlockHash.fromHexString('77ffdf2e8e4144a9a39b20ea7211a4aee0a23847778dcc1963c7a85f32b4f27d'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'update') { - expect(event.events[1]).toEqual(expected.upgradedEvent); + expect(event.events[1]!).toEqual(expected.upgradedEvent); } else { throw Error('Wrong event.'); } @@ -186,7 +186,7 @@ test('DataRegistered', async () => { const blockHash = BlockHash.fromHexString('ac4e60f4a014d823e3bf03859abdb2f9d2317b988dedc9c9621e3b7f5dcffb06'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'registerData') { expect(event.dataRegistered).toEqual(expected.dataRegisteredEvent); @@ -200,7 +200,7 @@ test('NewEncryptedAmountEvent', async () => { const blockHash = BlockHash.fromHexString('4eec1470e133340859dd9cd39187ad5f32c5b59ca3c7277d44f9b30e7a563388'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'encryptedAmountTransfer') { expect(event.added).toEqual(expected.newEncryptedAmountEvent); @@ -214,7 +214,7 @@ test('TransferWithScheduleEvent', async () => { const blockHash = BlockHash.fromHexString('7696ce3b5e3c5165572984abb250f8ac7c8f42cdc5ca3e1c1f1c387bb878fc94'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'transferWithSchedule') { expect(event.event).toEqual(expected.transferWithScheduleEvent); @@ -228,10 +228,10 @@ test('BakerKeysUpdated', async () => { const blockHash = BlockHash.fromHexString('ec886543ea454845ce09dcc064d7bc79f7da5a8c74c8f7cce9783681028a47de'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[1]; + const event = events[1]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureBaker') { - expect(event.events[0]).toEqual(expected.bakerKeysUpdatedEvent); + expect(event.events[0]!).toEqual(expected.bakerKeysUpdatedEvent); } else { throw Error('Wrong event:'); } @@ -242,10 +242,10 @@ test('BakerStakeIncreased', async () => { const blockHash = BlockHash.fromHexString('29c4caa0de1d9fc9da9513635e876aa0db0c6ab37fc30e7d2d7883af51659273'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[1]; + const event = events[1]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureBaker') { - expect(event.events[0]).toEqual(expected.bakerStakeIncreasedEvent); + expect(event.events[0]!).toEqual(expected.bakerStakeIncreasedEvent); } else { throw Error('Wrong event:'); } @@ -256,7 +256,7 @@ test('CredentialKeysUpdated', async () => { const blockHash = BlockHash.fromHexString('387a2f5812b16e4f6543e51007f20b514909de4d7ea39785b83bcd6f1cde9af4'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'updateCredentialKeys') { expect(event.keysUpdated).toEqual(expected.credentialKeysUpdatedEvent); @@ -270,7 +270,7 @@ test('CredentialsUpdated', async () => { const blockHash = BlockHash.fromHexString('6ce268765af0f59e147a0935980ae3014b9e90b9a43a2d9cf785f19641a9bf64'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'updateCredentials') { expect(event.credentialsUpdated).toEqual(expected.credentialsUpdatedEvent); @@ -284,10 +284,10 @@ test('BakerStakeDecreased', async () => { const blockHash = BlockHash.fromHexString('2103b8c6f1e0608790f241b1ca5d19df16f00abe54e5885d72e60985959826ae'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureBaker') { - expect(event.events[0]).toEqual(expected.bakerStakeDecreasedEvent); + expect(event.events[0]!).toEqual(expected.bakerStakeDecreasedEvent); } else { throw Error('Wrong event:'); } @@ -298,7 +298,7 @@ test('DelegationStakeDecreased', async () => { const blockHash = BlockHash.fromHexString('f0426a8937438551692bbd777ac61f309fa2adee2dc50c82d6bd6ff151f5ce0a'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'configureDelegation') { expect(event.events[2]).toEqual(expected.delegationStakeDecreasedEvent); diff --git a/packages/sdk/test/client/rejectReasons.test.ts b/packages/sdk/test/client/rejectReasons.test.ts index a7baa8ab4..880afc553 100644 --- a/packages/sdk/test/client/rejectReasons.test.ts +++ b/packages/sdk/test/client/rejectReasons.test.ts @@ -10,7 +10,7 @@ test('EncryptedAmountSelfTransfer', async () => { const blockHash = BlockHash.fromHexString('a68ef25ac9b38dfb76884dc797f0b1f924695218107caed3b3e370479d552c3a'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.encryptedAmountSelfTransferRejectReason); @@ -24,7 +24,7 @@ test('FinalizationRewardCommissionNotInRange', async () => { const blockHash = BlockHash.fromHexString('bb58a5dbcb77ec5d94d1039724e347a5a06b60bd098bb404c9967531e58ec870'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[1]; + const event = events[1]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.finalizationRewardCommissionNotInRangeRejectReason); @@ -38,7 +38,7 @@ test('DelegationTargetNotABaker', async () => { const blockHash = BlockHash.fromHexString('f885db7e2b27953f3f6f10b3c69bf7d9e77bc529768234e4191ecbc6fd4cc47d'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.delegationTargetNotABakerRejectReason); @@ -52,7 +52,7 @@ test('AlreadyABaker', async () => { const blockHash = BlockHash.fromHexString('20324be7fdb1dd2556e5492ac0b73df408bda7f237066cee3c3d71a4804327a4'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.alreadyABakerRejectReason); @@ -66,7 +66,7 @@ test('NonExistentCredentialID', async () => { const blockHash = BlockHash.fromHexString('be5bd3b147eeababdbf19a0d60b29e2aeddc7eb65e3ab901cbd4f071d5af211c'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.nonExistentCredentialIDRejectReason); @@ -80,7 +80,7 @@ test('ModuleNotWF', async () => { const blockHash = BlockHash.fromHexString('b100e5568b2db7cce2da671ac17d45911447d86340b40a469717c15fd4098dda'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.moduleNotWFRejectReason); @@ -94,7 +94,7 @@ test('AmountTooLarge', async () => { const blockHash = BlockHash.fromHexString('25658e0353cae71a48f25f9ed92682cc096d1463b801676b449cb89c7fa13a1f'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.amountTooLargeRejectReason); @@ -108,7 +108,7 @@ test('ModuleHashAlreadyExists', async () => { const blockHash = BlockHash.fromHexString('ec85ac5f3b7a39ac277aee9e96837c53be3bd3442068a0970ab3badd80fd88e5'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.moduleHashAlreadyExistsRejectReason); @@ -122,7 +122,7 @@ test('TransactionFeeCommissionNotInRange', async () => { const blockHash = BlockHash.fromHexString('102ef7df5a6d1502c6e2b864e182cbb10824d017e88bb90a4cb82e3c054e0bba'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.transactionFeeCommissionNotInRangeRejectReason); @@ -136,7 +136,7 @@ test('StakeOverMaximumThresholdForPool', async () => { const blockHash = BlockHash.fromHexString('5284633bd71b4f8840e9f2e86ced6a4615961248347669d7b5a5a7088422a9f0'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[1]; + const event = events[1]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.stakeOverMaximumThresholdForPoolRejectReason); @@ -150,7 +150,7 @@ test('BakerInCooldown', async () => { const blockHash = BlockHash.fromHexString('dd47761affcc6446306158cd51b8ab117b81ae5d33413af2b3c4c5f20275fb5f'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.bakerInCooldownRejectReason); @@ -164,7 +164,7 @@ test('InvalidInitMethod', async () => { const blockHash = BlockHash.fromHexString('2830618959b146313cfc596826e59390f6b8907d33a964ec0663c1d7e975fcfa'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidInitMethodRejectReason); @@ -178,7 +178,7 @@ test('InsufficientBalanceForDelegationStake', async () => { const blockHash = BlockHash.fromHexString('dce2ce0d5e893e273eb53726e35fb249e3151db2347c624e5d0c5ffce20c4950'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.insufficientBalanceForDelegationStakeRejectReason); @@ -192,7 +192,7 @@ test('InvalidAccountReference', async () => { const blockHash = BlockHash.fromHexString('a37e065c239787a4fca3241580dd37ce354ef97224adf1f34afbf92fdd310b69'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidAccountReferenceRejectReason); @@ -206,7 +206,7 @@ test('MissingBakerAddParameters', async () => { const blockHash = BlockHash.fromHexString('269d3730dd3813dbe5c8104be20bcfe02ee3fbd4a7a3da4fcca1271c38a6e405'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.missingBakerAddParametersRejectReason); @@ -220,7 +220,7 @@ test('PoolClosed', async () => { const blockHash = BlockHash.fromHexString('72c2d0d9634b82ade18616711eb1cb351456b913d1758c4d840759a408b75775'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.poolClosedRejectReason); @@ -234,7 +234,7 @@ test('ScheduledSelfTransfer', async () => { const blockHash = BlockHash.fromHexString('917ca9e15667a667cad97c7806ea27b78633d6821cc6f1fa29f8aecd238223c5'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.scheduledSelfTransferRejectReason); @@ -248,7 +248,7 @@ test('InvalidModuleReference', async () => { const blockHash = BlockHash.fromHexString('c6ebed14d387e8d0c3f8120f83d69948b39478d7205e468f4db9b089459ff8c4'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidModuleReferenceRejectReason); @@ -262,7 +262,7 @@ test('FirstScheduledReleaseExpired', async () => { const blockHash = BlockHash.fromHexString('8692bbfd18983543aace1a04596e27ec8f332243b01ed2b6fed28397bf66ff89'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.firstScheduledReleaseExpiredRejectReason); @@ -276,7 +276,7 @@ test('InvalidReceiveMethod', async () => { const blockHash = BlockHash.fromHexString('0b667b6886760c37a176097b390fd1d655e714f2bf19a507b3242d8ee919ed1a'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidReceiveMethodRejectReason); @@ -290,7 +290,7 @@ test('InsufficientBalanceForBakerStake', async () => { const blockHash = BlockHash.fromHexString('1803d84dfaa081e5da1c1dc96bbb65888a65904cba5abcbfc2aad963d2d39097'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.insufficientBalanceForBakerStakeRejectReason); @@ -304,7 +304,7 @@ test('RuntimeFailure', async () => { const blockHash = BlockHash.fromHexString('5072f24f681fc5ff9ae09f0b698f8aed20c02bd6990fc59bcb618252ad257355'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.runtimeFailureRejectReason); @@ -318,7 +318,7 @@ test('InvalidContractAddress', async () => { const blockHash = BlockHash.fromHexString('30247d68bcca12a0a611bfc412a9a8b28152f501ea957970f1351c528bd58edf'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidContractAddressRejectReason); @@ -332,7 +332,7 @@ test('OutOfEnergy', async () => { const blockHash = BlockHash.fromHexString('57c632333f9373fbc7ea4ce3306269981560fd87c5a6de23b4a7584604e2c6bc'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.outOfEnergyRejectReason); @@ -346,7 +346,7 @@ test('InvalidEncryptedAmountTransferProof', async () => { const blockHash = BlockHash.fromHexString('6a63a548e2d983cafe65f47a785e1e1dde1ba35f6fe16234602936f4fbecb4dd'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidEncryptedAmountTransferProofRejectReason); @@ -360,7 +360,7 @@ test('RejectedInit', async () => { const blockHash = BlockHash.fromHexString('b95031d150ae90175c203a63b23f8dafd5a8c57defaf5d287a6c534d4a4ad2d5'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.rejectedInitRejectReason); @@ -374,7 +374,7 @@ test('RejectedReceive', async () => { const blockHash = BlockHash.fromHexString('2141282b7a2ec57f3bcce59dc3b0649c80b872ae21a56c2ad300c4002145f988'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.rejectedReceiveRejectReason); @@ -388,7 +388,7 @@ test('StakeUnderMinimumThresholdForBaking', async () => { const blockHash = BlockHash.fromHexString('4d8a001488e2295911b55822c9fb48fae7deff1bb1e2a36aba54c5f61b8e3159'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.stakeUnderMinimumThresholdForBakingRejectReason); @@ -402,7 +402,7 @@ test('InvalidTransferToPublicProof', async () => { const blockHash = BlockHash.fromHexString('10f02dba8e75ef25d2eefde19d39624c62600f13a5d91b857283b718017a4471'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.invalidTransferToPublicProofRejectReason); @@ -416,7 +416,7 @@ test('SerializationFailure', async () => { const blockHash = BlockHash.fromHexString('d3e2e0a0a6674a56f9e057894fcba2244c21242705f9a95ba1052e6ab156eeb1'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.serializationFailureRejectReason); @@ -430,7 +430,7 @@ test('PoolWouldBecomeOverDelegated', async () => { const blockHash = BlockHash.fromHexString('c4ae2d1e29ed2dfed7e4a0e08fb419ae6b5cef65cba9ff0c6553ef6377b3e95c'); const eventStream = client.getBlockTransactionEvents(blockHash); const events = await streamToList(eventStream); - const event = events[0]; + const event = events[0]!; if (event.type === 'accountTransaction' && event.transactionType === 'failed') { expect(event.rejectReason).toEqual(expected.poolWouldBecomeOverDelegatedRejectReason); diff --git a/packages/sdk/test/client/resources/expectedJsons.ts b/packages/sdk/test/client/resources/expectedJsons.ts index 4a18430e7..5d6e5d1c6 100644 --- a/packages/sdk/test/client/resources/expectedJsons.ts +++ b/packages/sdk/test/client/resources/expectedJsons.ts @@ -1771,63 +1771,48 @@ export const chainParameters: ChainParametersV1 = { }, keys: [ { - schemeId: 'Ed25519', verifyKey: 'b8ddf4505a37eee2c046671f634b74cf3630f3958ad70a04f39dc843041965be', }, { - schemeId: 'Ed25519', verifyKey: '6f39b453a1f2d04be8e1d34822d4819af1bae7bd10bd0d1f05cbdbfc0907886f', }, { - schemeId: 'Ed25519', verifyKey: 'bf1dbf2070a9af3f469f829817c929ca98349bf383180abf53fa5d349c2ae72f', }, { - schemeId: 'Ed25519', verifyKey: '691dea8d3aeb620e08130d0cddb68156809e894f603763990a2b21af5b17d916', }, { - schemeId: 'Ed25519', verifyKey: 'd6e2ec35c642f52681921b313e4563fda16ab893b8597417778ffd57748a4f30', }, { - schemeId: 'Ed25519', verifyKey: '4fefb5ee8f8f46ecb86accbf44218e7699eb4937122a284d349db9f8e70a9794', }, { - schemeId: 'Ed25519', verifyKey: 'b43e41e008c520c421df2229ea2af816d907d2f085b82b3cadc156165d49ed2a', }, { - schemeId: 'Ed25519', verifyKey: '7386b8a50d01797f95e594112ca1734d2dc2984235c58c9cf5c18a07f7cef98c', }, { - schemeId: 'Ed25519', verifyKey: '56a75f4399a0671fd8a11d88b59c33be00ffb9328a31a41467ce98ddd932dcb1', }, { - schemeId: 'Ed25519', verifyKey: '9974b5868241dd1eee38edda8ad64cfb23722e280ef09286c8a1c7a3c3ba1f40', }, { - schemeId: 'Ed25519', verifyKey: 'fd363dfd04f319848c3d766bc617a5f88f0044f1813cc4a2140a6d28e9b62cce', }, { - schemeId: 'Ed25519', verifyKey: '528281d04d8d74dba67fac53460fa3e2ff2f171f16363d80f679877821b538d2', }, { - schemeId: 'Ed25519', verifyKey: '8ebab6f84c237b331d54a2f611ea8fceeba5b9e0ffff7a096a172f034257999f', }, { - schemeId: 'Ed25519', verifyKey: '616fe3f0441f87e2ba32194faf6c3ea658cac5d31353303a45ca3d1d4164a4f1', }, { - schemeId: 'Ed25519', verifyKey: 'e1f1c6971705da9c2a50be7967609c092fe295a88c71fbf18dd90cc6d81508f2', }, ],