From 2a29a766f7d8d36b0d9918dfb8b728fef5471faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bruus=20Zeppelin?= Date: Wed, 25 Oct 2023 11:45:22 +0200 Subject: [PATCH] Unit tests for wallet API compatibility --- .eslintrc.js => .eslintrc.cjs | 7 +- examples/piggybank/src/utils.ts | 2 +- .../src/wallet-api-types.ts | 120 +++- packages/browser-wallet-api/jest.config.ts | 16 + packages/browser-wallet-api/package.json | 8 + .../browser-wallet-api/src/compatibility.ts | 234 +++++--- packages/browser-wallet-api/src/wallet-api.ts | 4 +- .../test/compatibility.test.ts | 552 ++++++++++++++++++ .../browser-wallet-api/tsconfig.eslint.json | 4 + packages/browser-wallet-api/tsconfig.json | 1 + tsconfig.json | 2 +- yarn.lock | 35 ++ 12 files changed, 880 insertions(+), 105 deletions(-) rename .eslintrc.js => .eslintrc.cjs (87%) create mode 100644 packages/browser-wallet-api/jest.config.ts create mode 100644 packages/browser-wallet-api/test/compatibility.test.ts create mode 100644 packages/browser-wallet-api/tsconfig.eslint.json diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 87% rename from .eslintrc.js rename to .eslintrc.cjs index 0f3ab5446..c840360a5 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -7,7 +7,12 @@ module.exports = { ecmaFeatures: { jsx: true, }, - project: ['./tsconfig.json', './packages/browser-wallet/tsconfig.eslint.json', './examples/*/tsconfig.json'], + project: [ + './tsconfig.json', + './packages/browser-wallet/tsconfig.eslint.json', + './packages/browser-wallet-api/tsconfig.eslint.json', + './examples/*/tsconfig.json', + ], tsconfigRootDir: __dirname, }, env: { diff --git a/examples/piggybank/src/utils.ts b/examples/piggybank/src/utils.ts index 59dcb588c..139a73f98 100644 --- a/examples/piggybank/src/utils.ts +++ b/examples/piggybank/src/utils.ts @@ -55,7 +55,7 @@ export const smash = (account: string, index: bigint, subindex = 0n) => { address: ContractAddress.create(index, subindex), receiveName: ReceiveName.fromString(`${CONTRACT_NAME}.smash`), maxContractExecutionEnergy: Energy.create(30000), - } as UpdateContractPayload) + }) .then((txHash) => console.log(`https://testnet.ccdscan.io/?dcount=1&dentity=transaction&dhash=${txHash}`) ) diff --git a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts index 840a4b12b..31cf48c21 100644 --- a/packages/browser-wallet-api-helpers/src/wallet-api-types.ts +++ b/packages/browser-wallet-api-helpers/src/wallet-api-types.ts @@ -15,6 +15,13 @@ import type { Base58String, Base64String, ContractAddress, + UpdateCredentialsPayload, + RegisterDataPayload, + SimpleTransferPayload, + SimpleTransferWithMemoPayload, + DeployModulePayload, + ConfigureBakerPayload, + ConfigureDelegationPayload, } from '@concordium/web-sdk'; import type { RpcTransport } from '@protobuf-ts/runtime-rpc'; import { LaxNumberEnumValue, LaxStringEnumValue } from './util'; @@ -51,10 +58,13 @@ export interface CredentialProof { verificationMethod: string; } +export type SendTransactionUpdateContractPayload = Omit; +export type SendTransactionInitContractPayload = Omit; + export type SendTransactionPayload = | Exclude - | Omit - | Omit; + | SendTransactionUpdateContractPayload + | SendTransactionInitContractPayload; export type SmartContractParameters = | { [key: string]: SmartContractParameters } @@ -114,18 +124,108 @@ interface MainWalletApi { * @param accountAddress the address of the account that should sign the transaction * @param type the type of transaction that is to be signed and sent. * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the params/message fields, those should instead be provided in the subsequent argument instead. - * @param parameters parameters for the initContract and updateContract transactions in JSON-like format. - * @param schema schema used for the initContract and updateContract transactions to serialize the parameters. Should be base64 encoded. + * @param [parameters] parameters for the initContract and updateContract transactions in JSON-like format. + * @param [schema] schema used for the initContract and updateContract transactions to serialize the parameters. Should be base64 encoded. * @param [schemaVersion] version of the schema provided. Must be supplied for schemas that use version 0 or 1, as they don't have the version embedded. */ sendTransaction( accountAddress: AccountAddressSource, - type: LaxNumberEnumValue, - payload: SendTransactionPayload, - parameters: SmartContractParameters, - schema: SchemaSource, + type: LaxNumberEnumValue, + payload: SendTransactionInitContractPayload, + parameters?: SmartContractParameters, + schema?: SchemaSource, schemaVersion?: SchemaVersion ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the params/message fields, those should instead be provided in the subsequent argument instead. + * @param [parameters] parameters for the initContract and updateContract transactions in JSON-like format. + * @param [schema] schema used for the initContract and updateContract transactions to serialize the parameters. Should be base64 encoded. + * @param [schemaVersion] version of the schema provided. Must be supplied for schemas that use version 0 or 1, as they don't have the version embedded. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: SendTransactionUpdateContractPayload, + parameters?: SmartContractParameters, + schema?: SchemaSource, + schemaVersion?: SchemaVersion + ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the parameters, those should instead be provided in the subsequent argument instead. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: UpdateCredentialsPayload + ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the parameters, those should instead be provided in the subsequent argument instead. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: RegisterDataPayload + ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the parameters, those should instead be provided in the subsequent argument instead. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: SimpleTransferPayload + ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the parameters, those should instead be provided in the subsequent argument instead. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: SimpleTransferWithMemoPayload + ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the parameters, those should instead be provided in the subsequent argument instead. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: DeployModulePayload + ): Promise; + /** + * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. + * Note that if the user rejects signing the transaction, this will throw an error. + * @param accountAddress the address of the account that should sign the transaction + * @param type the type of transaction that is to be signed and sent. + * @param payload the payload of the transaction to be signed and sent. Note that for smart contract transactions, the payload should not contain the parameters, those should instead be provided in the subsequent argument instead. + */ + sendTransaction( + accountAddress: AccountAddressSource, + type: LaxNumberEnumValue, + payload: ConfigureBakerPayload + ): Promise; /** * Sends a transaction to the Concordium Wallet and awaits the users action. Note that a header is not sent, and will be constructed by the wallet itself. * Note that if the user rejects signing the transaction, this will throw an error. @@ -135,8 +235,8 @@ interface MainWalletApi { */ sendTransaction( accountAddress: AccountAddressSource, - type: LaxNumberEnumValue, - payload: SendTransactionPayload + type: LaxNumberEnumValue, + payload: ConfigureDelegationPayload ): Promise; /** * Sends a message to the Concordium Wallet and awaits the users action. If the user signs the message, this will resolve to the signature. diff --git a/packages/browser-wallet-api/jest.config.ts b/packages/browser-wallet-api/jest.config.ts new file mode 100644 index 000000000..fc84733ed --- /dev/null +++ b/packages/browser-wallet-api/jest.config.ts @@ -0,0 +1,16 @@ +import { pathsToModuleNameMapper } from 'ts-jest'; +import type { Config } from '@jest/types'; +import tsconfig from './tsconfig.json'; + +const esModules = [].join('|'); + +const config: Config.InitialOptions = { + preset: 'ts-jest/presets/js-with-ts-esm', + moduleNameMapper: { + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: '/' }), + }, + transformIgnorePatterns: [`node_modules/(?!${esModules})`], + modulePaths: ['/../../node_modules', '/node_modules'], +}; + +export default config; diff --git a/packages/browser-wallet-api/package.json b/packages/browser-wallet-api/package.json index f3f74af25..e1f5300fb 100644 --- a/packages/browser-wallet-api/package.json +++ b/packages/browser-wallet-api/package.json @@ -14,5 +14,13 @@ "@protobuf-ts/grpcweb-transport": "^2.8.2", "@protobuf-ts/runtime-rpc": "^2.8.2", "buffer": "^6.0.3" + }, + "devDependencies": { + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + }, + "scripts": { + "test": "jest", + "build": "tsc" } } diff --git a/packages/browser-wallet-api/src/compatibility.ts b/packages/browser-wallet-api/src/compatibility.ts index fe6e8b452..4ffe85abf 100644 --- a/packages/browser-wallet-api/src/compatibility.ts +++ b/packages/browser-wallet-api/src/compatibility.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Buffer } from 'buffer/'; import { SmartContractParameters, SchemaWithContext, @@ -7,10 +6,12 @@ import { AccountAddressSource, SchemaSource, SignMessageObject, + SendTransactionPayload, + SendTransactionInitContractPayload, + SendTransactionUpdateContractPayload, } from '@concordium/browser-wallet-api-helpers'; import { AccountAddress, - AccountTransactionPayload, AccountTransactionType, CcdAmount, ConfigureBakerPayload, @@ -23,14 +24,15 @@ import { IdStatement, InitContractPayload, ModuleReference, - Parameter, ReceiveName, + RegisterDataPayload, SchemaVersion, SimpleTransferPayload, - UpdateContractPayload, + SimpleTransferWithMemoPayload, + UpdateCredentialsPayload, } from '@concordium/web-sdk'; -type GtuAmount = { microGtuAmount: bigint }; +export type GtuAmount = { microGtuAmount: bigint }; /** * Used in versions released prior to coin being renamed to CCD. @@ -43,7 +45,7 @@ function sanitizeAccountAddress(accountAddress: AccountAddressSource): AccountAd return AccountAddress.instanceOf(accountAddress) ? accountAddress : AccountAddress.fromBase58(accountAddress); } -type SanitizedSignMessageInput = { +export type SanitizedSignMessageInput = { accountAddress: AccountAddress.Type; message: string | SignMessageObject; }; @@ -58,7 +60,7 @@ export function sanitizeSignMessageInput( }; } -type SanitizedRequestIdProofInput = { +export type SanitizedRequestIdProofInput = { accountAddress: AccountAddress.Type; statement: IdStatement; challenge: string; @@ -76,7 +78,7 @@ export function sanitizeRequestIdProofInput( }; } -type SanitizedAddCIS2TokensInput = { +export type SanitizedAddCIS2TokensInput = { accountAddress: AccountAddress.Type; tokenIds: string[]; contractAddress: ContractAddress.Type; @@ -85,149 +87,187 @@ type SanitizedAddCIS2TokensInput = { export function sanitizeAddCIS2TokensInput( _accountAddress: AccountAddressSource, tokenIds: string[], - dyn: ContractAddress.Type | bigint, + contractAddressSource: ContractAddress.Type | bigint, contractSubindex?: bigint ): SanitizedAddCIS2TokensInput { const accountAddress = sanitizeAccountAddress(_accountAddress); let contractAddress: ContractAddress.Type; - if (ContractAddress.instanceOf(dyn)) { - contractAddress = dyn; + if (ContractAddress.instanceOf(contractAddressSource)) { + contractAddress = contractAddressSource; } else { - contractAddress = ContractAddress.create(dyn, contractSubindex); + contractAddress = ContractAddress.create(contractAddressSource, contractSubindex); } return { accountAddress, tokenIds, contractAddress }; } -interface CcdAmountV0 { +export interface CcdAmountV0 { readonly microCcdAmount: bigint; } -interface ContractAddressV0 { +export interface ContractAddressV0 { index: bigint; subindex: bigint; } -interface AccountAddressV0 { +export interface AccountAddressV0 { readonly address: HexString; readonly decodedAddress: Uint8Array; } -interface ModuleReferenceV0 { +export interface ModuleReferenceV0 { readonly moduleRef: HexString; readonly decodedModuleRef: Uint8Array; } -interface InitContractPayloadV0 { +export interface InitContractPayloadV0 { + amount: GtuAmount; + moduleRef: ModuleReferenceV0; + contractName: string; + maxContractExecutionEnergy: bigint; +} + +export interface InitContractPayloadV1 { amount: CcdAmountV0; moduleRef: ModuleReferenceV0; contractName: string; - param?: Buffer; maxContractExecutionEnergy: bigint; } -interface InitContractPayloadV1 { +export interface InitContractPayloadV2 { amount: CcdAmountV0; moduleRef: ModuleReferenceV0; initName: string; - param?: Buffer; maxContractExecutionEnergy: bigint; } -type InitContractPayloadCompat = InitContractPayloadV0 | InitContractPayloadV1 | InitContractPayload; +export type InitContractPayloadCompat = + | InitContractPayloadV0 + | InitContractPayloadV1 + | InitContractPayloadV2 + | SendTransactionInitContractPayload; -interface UpdateContractPayloadV0 { +export interface UpdateContractPayloadV0 { + amount: GtuAmount; + contractAddress: ContractAddressV0; + receiveName: string; + maxContractExecutionEnergy: bigint; +} + +export interface UpdateContractPayloadV1 { amount: CcdAmountV0; contractAddress: ContractAddressV0; receiveName: string; - message?: Buffer; maxContractExecutionEnergy: bigint; } -interface UpdateContractPayloadV1 { +export interface UpdateContractPayloadV2 { amount: CcdAmountV0; address: ContractAddressV0; receiveName: string; - message?: Buffer; maxContractExecutionEnergy: bigint; } -type UpdateContractPayloadCompat = UpdateContractPayloadV0 | UpdateContractPayloadV1 | UpdateContractPayload; +export type UpdateContractPayloadCompat = + | UpdateContractPayloadV0 + | UpdateContractPayloadV1 + | UpdateContractPayloadV2 + | SendTransactionUpdateContractPayload; -interface DeployModulePayloadV0 { +export interface DeployModulePayloadV0 { version?: number; content: Uint8Array; } -type DeployModulePayloadCompat = DeployModulePayloadV0 | DeployModulePayload; +export type DeployModulePayloadCompat = DeployModulePayloadV0 | DeployModulePayload; + +type WithMemo = T & Pick; -interface SimpleTransferPayloadV0 { +export interface SimpleTransferPayloadV0 { + amount: GtuAmount; + toAddress: AccountAddressV0; +} +export type SimpleTransferWithMemoPayloadV0 = WithMemo; + +export interface SimpleTransferPayloadV1 { amount: CcdAmountV0; toAddress: AccountAddressV0; } +export type SimpleTransferWithMemoPayloadV1 = WithMemo; -type SimpleTransferPayloadCompat = SimpleTransferPayloadV0 | SimpleTransferPayload; +export type SimpleTransferPayloadCompat = SimpleTransferPayloadV0 | SimpleTransferPayloadV1 | SimpleTransferPayload; +export type SimpleTransferWithMemoPayloadCompat = + | SimpleTransferWithMemoPayloadV0 + | SimpleTransferWithMemoPayloadV1 + | SimpleTransferWithMemoPayload; -interface ConfigureBakerPayloadV0 extends Omit { +export interface ConfigureBakerPayloadV0 extends Omit { stake?: CcdAmountV0; } -type ConfigureBakerPayloadCompat = ConfigureBakerPayloadV0 | ConfigureBakerPayload; +export type ConfigureBakerPayloadCompat = ConfigureBakerPayloadV0 | ConfigureBakerPayload; -interface ConfigureDelegationPayloadV0 extends Omit { +export interface ConfigureDelegationPayloadV0 extends Omit { stake?: CcdAmountV0; } -type ConfigureDelegationPayloadCompat = ConfigureDelegationPayloadV0 | ConfigureDelegationPayload; +export type ConfigureDelegationPayloadCompat = ConfigureDelegationPayloadV0 | ConfigureDelegationPayload; -type SanitizedSendTransactionInput = { - accountAddress: AccountAddress.Type; - type: AccountTransactionType; - payload: AccountTransactionPayload; - parameters?: SmartContractParameters; - schema?: SchemaWithContext; - schemaVersion?: SchemaVersion; -}; +export type RegisterDataPayloadCompat = RegisterDataPayload; +export type UpdateCredentialsPayloadCompat = UpdateCredentialsPayload; -function sanitizePayload(type: AccountTransactionType, payload: AccountTransactionPayload): AccountTransactionPayload { +export type SendTransactionPayloadCompat = + | InitContractPayloadCompat + | UpdateContractPayloadCompat + | DeployModulePayloadCompat + | SimpleTransferPayloadCompat + | ConfigureBakerPayloadCompat + | ConfigureDelegationPayloadCompat + | RegisterDataPayloadCompat + | UpdateCredentialsPayloadCompat; + +function sanitizePayload(type: AccountTransactionType, payload: SendTransactionPayloadCompat): SendTransactionPayload { switch (type) { case AccountTransactionType.InitContract: { const p = payload as InitContractPayloadCompat; - const amount = CcdAmount.fromMicroCcd(p.amount.microCcdAmount); + const amount = CcdAmount.fromMicroCcd( + isGtuAmount(p.amount) ? p.amount.microGtuAmount : p.amount.microCcdAmount + ); const moduleRef = ModuleReference.fromHexString(p.moduleRef.moduleRef); - const initName = - typeof (p as InitContractPayload).initName !== 'string' - ? (p as InitContractPayload).initName - : ContractName.fromString( - (p as InitContractPayloadV0).contractName ?? (p as InitContractPayloadV1).initName - ); + + let initName: ContractName.Type; + if (typeof (p as InitContractPayloadV0 | InitContractPayloadV1).contractName === 'string') { + initName = ContractName.fromString((p as InitContractPayloadV0).contractName); + } else if (typeof (p as InitContractPayloadV2).initName === 'string') { + initName = ContractName.fromString((p as InitContractPayloadV2).initName); + } else if ( + typeof (p as InitContractPayload).initName === 'object' && + (p as InitContractPayload).initName !== null + ) { + initName = (p as InitContractPayload).initName; + } else { + throw new Error(`Unexpected payload for type ${type}: ${p}`); + } + const maxContractExecutionEnergy = typeof p.maxContractExecutionEnergy !== 'bigint' ? p.maxContractExecutionEnergy : Energy.create(p.maxContractExecutionEnergy); - let param: Parameter.Type | undefined; - if (p.param === undefined) { - param = undefined; - } else if (p.param instanceof Uint8Array) { - param = Parameter.fromBuffer(p.param); - } else { - param = p.param; - } - return { amount, moduleRef, - param, initName, maxContractExecutionEnergy, - } as InitContractPayload; + } as SendTransactionInitContractPayload; } case AccountTransactionType.Update: { const p = payload as UpdateContractPayloadCompat; - const amount = CcdAmount.fromMicroCcd(p.amount.microCcdAmount); + const amount = CcdAmount.fromMicroCcd( + isGtuAmount(p.amount) ? p.amount.microGtuAmount : p.amount.microCcdAmount + ); const maxContractExecutionEnergy = typeof p.maxContractExecutionEnergy !== 'bigint' ? p.maxContractExecutionEnergy @@ -236,26 +276,16 @@ function sanitizePayload(type: AccountTransactionType, payload: AccountTransacti typeof p.receiveName === 'string' ? ReceiveName.fromString(p.receiveName) : p.receiveName; const { index, subindex } = - (p as UpdateContractPayloadV1 | UpdateContractPayload).address ?? + (p as Exclude).address ?? (p as UpdateContractPayloadV0).contractAddress; const address = ContractAddress.create(index, subindex); - let message: Parameter.Type | undefined; - if (p.message === undefined) { - message = undefined; - } else if (p.message instanceof Uint8Array) { - message = Parameter.fromBuffer(p.message); - } else { - message = p.message; - } - return { amount, address, - message, receiveName, maxContractExecutionEnergy, - } as UpdateContractPayload; + } as SendTransactionUpdateContractPayload; } case AccountTransactionType.DeployModule: { const p = payload as DeployModulePayloadCompat; @@ -268,47 +298,71 @@ function sanitizePayload(type: AccountTransactionType, payload: AccountTransacti } case AccountTransactionType.Transfer: case AccountTransactionType.TransferWithMemo: { - const p = payload as SimpleTransferPayloadCompat; + // The "memo" part of transfers have not changes, so these are treated the same. + const p = payload as SimpleTransferPayloadCompat | SimpleTransferWithMemoPayloadCompat; - const amount = CcdAmount.fromMicroCcd(p.amount.microCcdAmount); + const amount = CcdAmount.fromMicroCcd( + isGtuAmount(p.amount) ? p.amount.microGtuAmount : p.amount.microCcdAmount + ); const toAddress = AccountAddress.fromBuffer(p.toAddress.decodedAddress); - return { ...p, amount, toAddress }; + return { ...p, amount, toAddress } as SimpleTransferPayload | SimpleTransferWithMemoPayload; } case AccountTransactionType.ConfigureBaker: case AccountTransactionType.ConfigureDelegation: { const p = payload as ConfigureBakerPayloadCompat | ConfigureDelegationPayloadCompat; const stake = p.stake !== undefined ? CcdAmount.fromMicroCcd(p.stake.microCcdAmount) : undefined; - return { ...p, stake }; + return { ...p, stake } as ConfigureBakerPayload | ConfigureDelegationPayload; } + case AccountTransactionType.RegisterData: + case AccountTransactionType.UpdateCredentials: + // No changes across any API versions. + return payload as RegisterDataPayload | UpdateCredentialsPayload; default: - return payload; + // This should never happen, but is here for backwards compatibility. + return payload as SendTransactionPayload; } } +export type SanitizedSendTransactionInput = { + accountAddress: AccountAddress.Type; + type: AccountTransactionType; + payload: SendTransactionPayload; + parameters?: SmartContractParameters; + schema?: SchemaWithContext; + schemaVersion?: SchemaVersion; +}; + /** * Compatibility layer for `WalletApi.sendTransaction` */ export function sanitizeSendTransactionInput( - _accountAddress: AccountAddressSource, + accountAddress: AccountAddressSource, type: AccountTransactionType, - _payload: AccountTransactionPayload, + payload: SendTransactionPayloadCompat, parameters?: SmartContractParameters, - _schema?: SchemaSource, + schema?: SchemaSource, schemaVersion?: SchemaVersion ): SanitizedSendTransactionInput { - const accountAddress = sanitizeAccountAddress(_accountAddress); - const payload = sanitizePayload(type, _payload); + const sanitizedAccountAddress = sanitizeAccountAddress(accountAddress); + const sanitizedPayload = sanitizePayload(type, payload); - let schema: SchemaWithContext | undefined; - if (typeof _schema === 'string' || _schema instanceof String) { - schema = { + let sanitizedSchema: SchemaWithContext | undefined; + if (typeof schema === 'string' || schema instanceof String) { + sanitizedSchema = { type: SchemaType.Module, - value: _schema.toString(), + value: schema.toString(), }; } else { - schema = _schema; + sanitizedSchema = schema; } - return { accountAddress, type, payload, parameters, schema, schemaVersion }; + return { + accountAddress: sanitizedAccountAddress, + type, + payload: sanitizedPayload, + parameters, + schema: sanitizedSchema, + schemaVersion, + }; } diff --git a/packages/browser-wallet-api/src/wallet-api.ts b/packages/browser-wallet-api/src/wallet-api.ts index 47599d5d0..d9302771c 100644 --- a/packages/browser-wallet-api/src/wallet-api.ts +++ b/packages/browser-wallet-api/src/wallet-api.ts @@ -6,7 +6,6 @@ import { } from '@concordium/browser-wallet-message-hub'; import { AccountAddress, - AccountTransactionPayload, AccountTransactionSignature, AccountTransactionType, HexString, @@ -24,6 +23,7 @@ import { CredentialProof, AccountAddressSource, SchemaSource, + SendTransactionPayload, } from '@concordium/browser-wallet-api-helpers'; import EventEmitter from 'events'; import { IdProofOutput, IdStatement } from '@concordium/web-sdk/id'; @@ -126,7 +126,7 @@ class WalletApi extends EventEmitter implements IWalletApi { public async sendTransaction( accountAddress: AccountAddressSource, type: AccountTransactionType, - payload: AccountTransactionPayload, + payload: SendTransactionPayload, parameters?: SmartContractParameters, schema?: SchemaSource, schemaVersion?: SchemaVersion diff --git a/packages/browser-wallet-api/test/compatibility.test.ts b/packages/browser-wallet-api/test/compatibility.test.ts new file mode 100644 index 000000000..7d8722b75 --- /dev/null +++ b/packages/browser-wallet-api/test/compatibility.test.ts @@ -0,0 +1,552 @@ +import { + AccountAddress, + AccountTransactionType, + AttributeKey, + BakerKeysWithProofs, + CcdAmount, + ConfigureBakerPayload, + ConfigureDelegationPayload, + ContractAddress, + ContractName, + DataBlob, + DelegationTargetBaker, + DelegationTargetType, + DeployModulePayload, + Energy, + EntrypointName, + IdStatementBuilder, + ModuleReference, + OpenStatus, + ReceiveName, + RegisterDataPayload, + SchemaVersion, + SimpleTransferPayload, + SimpleTransferWithMemoPayload, + UpdateCredentialsPayload, +} from '@concordium/web-sdk'; +import { + SendTransactionInitContractPayload, + SendTransactionUpdateContractPayload, + SignMessageObject, + SmartContractParameters, +} from '@concordium/browser-wallet-api-helpers'; +import { + sanitizeSignMessageInput, + sanitizeAddCIS2TokensInput, + sanitizeRequestIdProofInput, + sanitizeSendTransactionInput, + SanitizedSignMessageInput, + SanitizedAddCIS2TokensInput, + SanitizedRequestIdProofInput, + InitContractPayloadV0, + SanitizedSendTransactionInput, + InitContractPayloadV1, + InitContractPayloadV2, + UpdateContractPayloadV0, + UpdateContractPayloadV1, + DeployModulePayloadV0, + SimpleTransferPayloadV0, + SimpleTransferPayloadV1, + SimpleTransferWithMemoPayloadV0, + SimpleTransferWithMemoPayloadV1, + ConfigureBakerPayloadV0, + ConfigureDelegationPayloadV0, +} from '../src/compatibility'; + +const accountAddress = '4UC8o4m8AgTxt5VBFMdLwMCwwJQVJwjesNzW7RPXkACynrULmd'; + +describe(sanitizeSignMessageInput, () => { + test('Returns expected format', () => { + const message = 'Test message'; + + const expected: SanitizedSignMessageInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + message, + }; + + let result = sanitizeSignMessageInput(AccountAddress.fromBase58(accountAddress), message); + expect(result).toEqual(expected); + + result = sanitizeSignMessageInput(accountAddress, message); + expect(result).toEqual(expected); + + const wrappedMessage: SignMessageObject = { + data: '010102', + schema: '', + }; + result = sanitizeSignMessageInput(accountAddress, wrappedMessage); + expect(result).toEqual({ ...expected, message: wrappedMessage }); + }); +}); + +describe(sanitizeAddCIS2TokensInput, () => { + test('Returns expected format', () => { + const tokenIds = ['01', '02']; + const contractIndex = 10n; + + const expected: SanitizedAddCIS2TokensInput = { + tokenIds, + accountAddress: AccountAddress.fromBase58(accountAddress), + contractAddress: ContractAddress.create(contractIndex), + }; + + let result = sanitizeAddCIS2TokensInput(accountAddress, tokenIds, contractIndex, 0n); + expect(result).toEqual(expected); + + result = sanitizeAddCIS2TokensInput( + AccountAddress.fromBase58(accountAddress), + tokenIds, + ContractAddress.create(contractIndex) + ); + expect(result).toEqual(expected); + }); +}); + +describe(sanitizeRequestIdProofInput, () => { + test('Returns expected format', () => { + const statement = new IdStatementBuilder().addMinimumAge(18).getStatement(); + const challenge = '000102'; + + const expected: SanitizedRequestIdProofInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + statement, + challenge, + }; + + let result = sanitizeRequestIdProofInput(accountAddress, statement, challenge); + expect(result).toEqual(expected); + + result = sanitizeRequestIdProofInput(AccountAddress.fromBase58(accountAddress), statement, challenge); + expect(result).toEqual(expected); + }); +}); + +describe(sanitizeSendTransactionInput, () => { + const maxContractExecutionEnergy = 30000n; + const contractName = 'SomeContract'; + const entrypointName = 'someReceive'; + const contractIndex = 10n; + const contractSubindex = 0n; + const amount = 100n; + + test('Returns expected format (barring transaction payload)', () => { + const payload: SendTransactionUpdateContractPayload = { + amount: CcdAmount.fromMicroCcd(0), + maxContractExecutionEnergy: Energy.create(maxContractExecutionEnergy), + address: ContractAddress.create(contractIndex, contractSubindex), + receiveName: ReceiveName.create( + ContractName.fromString(contractName), + EntrypointName.fromString(entrypointName) + ), + }; + const type = AccountTransactionType.Update; + const schemaVersion: SchemaVersion = SchemaVersion.V2; + const parameters: SmartContractParameters = { obj: 'test' }; + const schema = 'VGhpcyBpcyBiYXNlNjQK'; + + let expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, payload); + expect(result).toEqual(expected); + + expected = { ...expected, schemaVersion, parameters, schema: { type: 'module', value: schema } }; + + result = sanitizeSendTransactionInput( + AccountAddress.fromBase58(accountAddress), + type, + payload, + parameters, + schema, + schemaVersion + ); + expect(result).toEqual(expected); + }); + + test('Transforms "InitContract" transaction input as expected', () => { + const type = AccountTransactionType.InitContract; + const moduleRef = ModuleReference.fromHexString( + '23513bcb5dbc81216fa4e12d3165a818e2b8699a1c9ef5c699f46ca3b1024ebf' + ); + + const expectedPayload: SendTransactionInitContractPayload = { + moduleRef, + maxContractExecutionEnergy: Energy.create(maxContractExecutionEnergy), + amount: CcdAmount.fromMicroCcd(amount), + initName: ContractName.fromString(contractName), + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: InitContractPayloadV0 = { + amount: { microGtuAmount: amount }, + moduleRef, + contractName, + maxContractExecutionEnergy, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: InitContractPayloadV1 = { + amount: { microCcdAmount: amount }, + moduleRef, + contractName, + maxContractExecutionEnergy, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + + const v2: InitContractPayloadV2 = { + amount: { microCcdAmount: amount }, + initName: contractName, + moduleRef, + maxContractExecutionEnergy, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v2); + expect(result).toEqual(expected); + + const v3: SendTransactionInitContractPayload = { + amount: CcdAmount.fromMicroCcd(amount), + initName: ContractName.fromString(contractName), + moduleRef, + maxContractExecutionEnergy: Energy.create(maxContractExecutionEnergy), + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v3); + expect(result).toEqual(expected); + }); + + test('Transforms "UpdateContract" transaction input as expected', () => { + const type = AccountTransactionType.Update; + const receiveName = `${contractName}.${entrypointName}`; + + const expectedPayload: SendTransactionUpdateContractPayload = { + maxContractExecutionEnergy: Energy.create(maxContractExecutionEnergy), + amount: CcdAmount.fromMicroCcd(amount), + address: ContractAddress.create(contractIndex, contractSubindex), + receiveName: ReceiveName.create( + ContractName.fromString(contractName), + EntrypointName.fromString(entrypointName) + ), + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: UpdateContractPayloadV0 = { + amount: { microGtuAmount: amount }, + receiveName, + contractAddress: { index: contractIndex, subindex: contractSubindex }, + maxContractExecutionEnergy, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: UpdateContractPayloadV1 = { + amount: { microCcdAmount: amount }, + receiveName, + contractAddress: { index: contractIndex, subindex: contractSubindex }, + maxContractExecutionEnergy, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + + const v2: SendTransactionUpdateContractPayload = { + amount: CcdAmount.fromMicroCcd(amount), + receiveName: ReceiveName.create( + ContractName.fromString(contractName), + EntrypointName.fromString(entrypointName) + ), + address: ContractAddress.create(contractIndex, contractSubindex), + maxContractExecutionEnergy: Energy.create(maxContractExecutionEnergy), + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v2); + expect(result).toEqual(expected); + }); + + test('Transforms "DeployModule" transaction input as expected', () => { + const type = AccountTransactionType.DeployModule; + const version = 0; + const source = Buffer.from('Serialize!'); + + const expectedPayload: DeployModulePayload = { + version, + source, + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: DeployModulePayloadV0 = { + version, + content: source, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: DeployModulePayload = { + version, + source, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + }); + + test('Transforms "Transfer" transaction input as expected', () => { + const type = AccountTransactionType.Transfer; + + const expectedPayload: SimpleTransferPayload = { + toAddress: AccountAddress.fromBase58(accountAddress), + amount: CcdAmount.fromMicroCcd(amount), + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: SimpleTransferPayloadV0 = { + toAddress: { ...AccountAddress.fromBase58(accountAddress) }, + amount: { microGtuAmount: amount }, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: SimpleTransferPayloadV1 = { + toAddress: { ...AccountAddress.fromBase58(accountAddress) }, + amount: { microCcdAmount: amount }, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + + const v2: SimpleTransferPayload = { + toAddress: AccountAddress.fromBase58(accountAddress), + amount: CcdAmount.fromMicroCcd(amount), + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v2); + expect(result).toEqual(expected); + }); + + test('Transforms "TransferWithMemo" transaction input as expected', () => { + const type = AccountTransactionType.TransferWithMemo; + const memo = new DataBlob(Buffer.from('Some memo message')); + + const expectedPayload: SimpleTransferWithMemoPayload = { + toAddress: AccountAddress.fromBase58(accountAddress), + amount: CcdAmount.fromMicroCcd(amount), + memo, + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: SimpleTransferWithMemoPayloadV0 = { + toAddress: { ...AccountAddress.fromBase58(accountAddress) }, + amount: { microGtuAmount: amount }, + memo, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: SimpleTransferWithMemoPayloadV1 = { + toAddress: { ...AccountAddress.fromBase58(accountAddress) }, + amount: { microCcdAmount: amount }, + memo, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + + const v2: SimpleTransferWithMemoPayload = { + toAddress: AccountAddress.fromBase58(accountAddress), + amount: CcdAmount.fromMicroCcd(amount), + memo, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v2); + expect(result).toEqual(expected); + }); + + test('Transforms "ConfigureBaker" transaction input as expected', () => { + const type = AccountTransactionType.ConfigureBaker; + const keys: BakerKeysWithProofs = { + proofSig: '01', + proofElection: '02', + proofAggregation: '03', + electionVerifyKey: '04', + signatureVerifyKey: '05', + aggregationVerifyKey: '06', + }; + const metadataUrl = 'http://metadata.url'; + const restakeEarnings = true; + const openForDelegation: OpenStatus = OpenStatus.OpenForAll; + const bakingRewardCommission = 10; + const transactionFeeCommission = 20; + const finalizationRewardCommission = 30; + + const expectedPayload: ConfigureBakerPayload = { + keys, + stake: CcdAmount.fromMicroCcd(amount), + metadataUrl, + restakeEarnings, + openForDelegation, + bakingRewardCommission, + transactionFeeCommission, + finalizationRewardCommission, + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: ConfigureBakerPayloadV0 = { + keys, + metadataUrl, + restakeEarnings, + openForDelegation, + bakingRewardCommission, + transactionFeeCommission, + finalizationRewardCommission, + stake: { microCcdAmount: amount }, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: ConfigureBakerPayload = { + keys, + metadataUrl, + restakeEarnings, + openForDelegation, + bakingRewardCommission, + transactionFeeCommission, + finalizationRewardCommission, + stake: CcdAmount.fromMicroCcd(amount), + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + }); + + test('Transforms "ConfigureDelegation" transaction input as expected', () => { + const type = AccountTransactionType.ConfigureDelegation; + const restakeEarnings = true; + const delegationTarget: DelegationTargetBaker = { bakerId: 12n, delegateType: DelegationTargetType.Baker }; + + const expectedPayload: ConfigureDelegationPayload = { + stake: CcdAmount.fromMicroCcd(amount), + restakeEarnings, + delegationTarget, + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload: expectedPayload, + }; + + const v0: ConfigureDelegationPayloadV0 = { + restakeEarnings, + stake: { microCcdAmount: amount }, + delegationTarget, + }; + + let result = sanitizeSendTransactionInput(accountAddress, type, v0); + expect(result).toEqual(expected); + + const v1: ConfigureDelegationPayload = { + restakeEarnings, + stake: CcdAmount.fromMicroCcd(amount), + delegationTarget, + }; + + result = sanitizeSendTransactionInput(accountAddress, type, v1); + expect(result).toEqual(expected); + }); + + test('Transforms "RegisterData" transaction input as expected', () => { + const type = AccountTransactionType.RegisterData; + + const payload: RegisterDataPayload = { + data: new DataBlob(Buffer.from('This is data!')), + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload, + }; + const result = sanitizeSendTransactionInput(accountAddress, type, payload); + expect(result).toEqual(expected); + expect(result.payload).toBe(payload); + }); + + test('Transforms "UpdateCredentials" transaction input as expected', () => { + const type = AccountTransactionType.UpdateCredentials; + + const payload: UpdateCredentialsPayload = { + newCredentials: [ + { + index: 1, + cdi: { + credId: '010203', + policy: { + validTo: new Date().toString(), + createdAt: new Date(0).toString(), + revealedAttributes: { dob: 'dob' } as Record, + }, + proofs: '01030204', + ipIdentity: 1, + revocationThreshold: 2, + credentialPublicKeys: { + threshold: 1, + keys: { 0: { schemeId: 'ed25519', verifyKey: '030201' } }, + }, + commitments: { + cmmPrf: 'cmmPrf', + cmmAttributes: { dob: 'cmmDob' } as Record, + cmmCredCounter: 'cmmCredCounter', + cmmMaxAccounts: 'cmmMaxAccounts', + cmmIdCredSecSharingCoeff: ['cmmIdCredSecSharingCoeff'], + }, + arData: { arData: { encIdCredPubShare: 'encIdCredPubShare' } }, + }, + }, + ], + threshold: 1, + removeCredentialIds: ['010302'], + currentNumberOfCredentials: 1n, + }; + const expected: SanitizedSendTransactionInput = { + accountAddress: AccountAddress.fromBase58(accountAddress), + type, + payload, + }; + const result = sanitizeSendTransactionInput(accountAddress, type, payload); + expect(result).toEqual(expected); + expect(result.payload).toBe(payload); + }); +}); diff --git a/packages/browser-wallet-api/tsconfig.eslint.json b/packages/browser-wallet-api/tsconfig.eslint.json new file mode 100644 index 000000000..f38dd7079 --- /dev/null +++ b/packages/browser-wallet-api/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "test", "jest.config.ts"] +} diff --git a/packages/browser-wallet-api/tsconfig.json b/packages/browser-wallet-api/tsconfig.json index 6cbfed3a6..ba70a37e9 100644 --- a/packages/browser-wallet-api/tsconfig.json +++ b/packages/browser-wallet-api/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "baseUrl": ".", + "noEmit": true, "paths": { "@concordium/browser-wallet-message-hub": ["../browser-wallet-message-hub/src"], "@concordium/browser-wallet-api-helpers": ["../browser-wallet-api-helpers/src"] diff --git a/tsconfig.json b/tsconfig.json index ca5300ccf..8c8f5a1ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,5 @@ "@concordium/browser-wallet-api-helpers": ["./packages/browser-wallet-api-helpers/src"] } }, - "include": [".eslintrc.js"] + "include": [".eslintrc.cjs"] } diff --git a/yarn.lock b/yarn.lock index 49875811a..8b3ce5c5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2200,6 +2200,8 @@ __metadata: "@protobuf-ts/grpcweb-transport": ^2.8.2 "@protobuf-ts/runtime-rpc": ^2.8.2 buffer: ^6.0.3 + jest: ^29.7.0 + ts-jest: ^29.1.1 languageName: unknown linkType: soft @@ -22348,6 +22350,39 @@ __metadata: languageName: node linkType: hard +"ts-jest@npm:^29.1.1": + version: 29.1.1 + resolution: "ts-jest@npm:29.1.1" + dependencies: + bs-logger: 0.x + fast-json-stable-stringify: 2.x + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: 4.x + make-error: 1.x + semver: ^7.5.3 + yargs-parser: ^21.0.1 + peerDependencies: + "@babel/core": ">=7.0.0-beta.0 <8" + "@jest/types": ^29.0.0 + babel-jest: ^29.0.0 + jest: ^29.0.0 + typescript: ">=4.3 <6" + peerDependenciesMeta: + "@babel/core": + optional: true + "@jest/types": + optional: true + babel-jest: + optional: true + esbuild: + optional: true + bin: + ts-jest: cli.js + checksum: a8c9e284ed4f819526749f6e4dc6421ec666f20ab44d31b0f02b4ed979975f7580b18aea4813172d43e39b29464a71899f8893dd29b06b4a351a3af8ba47b402 + languageName: node + linkType: hard + "ts-node@npm:^10.7.0, ts-node@npm:^10.8.0": version: 10.8.1 resolution: "ts-node@npm:10.8.1"