From fd95f7aa181afe7aa0c770e68b891798f3c2e07d Mon Sep 17 00:00:00 2001 From: Bruno Motta Date: Mon, 22 May 2023 12:58:06 -0300 Subject: [PATCH 1/4] Add keyExport types --- package.json | 4 ++-- src/plugins/popupPlugin/models.ts | 7 +++++++ src/webWidget/models.ts | 9 +++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a4d8808..007a94f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oreid-js", - "version": "4.5.2", + "version": "4.6.0", "description": "Add authentication and signing to any blockchain app", "author": "AIKON", "license": "MIT", @@ -80,4 +80,4 @@ "webpack": "^5.74.0", "webpack-cli": "^4.10.0" } -} +} \ No newline at end of file diff --git a/src/plugins/popupPlugin/models.ts b/src/plugins/popupPlugin/models.ts index 642f19c..9452aa5 100644 --- a/src/plugins/popupPlugin/models.ts +++ b/src/plugins/popupPlugin/models.ts @@ -5,6 +5,8 @@ import { WebWidgetAuthResult, WebWidgetBuyParams, WebWidgetBuyResult, + WebWidgetKeyExportParams, + WebWidgetKeyExportResult, WebWidgetLogoutParams, WebWidgetNewChainAccountParams, WebWidgetNewChainAccountResult, @@ -25,6 +27,8 @@ export type PopupPluginNewChainAccountParams = Partial +export type PopupPluginKeyExportParams = WebWidgetKeyExportParams + export type PopupPluginSignParams = { transaction: Transaction } @@ -43,10 +47,13 @@ export type PopupPluginRecoverAccountResults = WebWidgetRecoverAccountResult export type PopupPluginSignResults = WebWidgetSignResult +export type PopupPluginKeyExportSuccessResults = WebWidgetKeyExportResult + export interface PopupPlugin { auth: (args: PopupPluginAuthParams) => Promise sign: (args: PopupPluginSignParams) => Promise newChainAccount: (args: PopupPluginNewChainAccountParams) => Promise recoverAccount: (args: PopupPluginRecoverAccountParams) => Promise buy: (args: PopupPluginBuyParams) => Promise + keyExport: (args: PopupPluginKeyExportParams) => Promise } diff --git a/src/webWidget/models.ts b/src/webWidget/models.ts index 9d5584f..ec72d76 100644 --- a/src/webWidget/models.ts +++ b/src/webWidget/models.ts @@ -47,6 +47,7 @@ export enum WebWidgetAction { RecoverAccount = 'recoverAccount', Sign = 'sign', Buy = 'buy', + KeyExport = 'keyExport', } /** params for Logout action */ @@ -146,6 +147,8 @@ export type WebWidgetSignParams = { } } +export type WebWidgetKeyExportParams = {} + /** Result for Auth action */ export type WebWidgetAuthResult = { /** User's OreID account (aka wallet account name) */ @@ -189,9 +192,15 @@ export type WebWidgetSignResult = { transactionId?: string } +/** Result for Key Export action */ +export type WebWidgetKeyExportResult = { + // no result - just success or error +} + export type WebWidgetActionResult = | WebWidgetAuthResult | WebWidgetLogoutResult | WebWidgetNewChainAccountResult | WebWidgetRecoverAccountResult | WebWidgetSignResult + | WebWidgetKeyExportResult From 71fe2b4e3b08e37004cabafd48e8114cf186f727 Mon Sep 17 00:00:00 2001 From: Gaetano Mondelli Date: Wed, 28 Jun 2023 17:04:07 +0100 Subject: [PATCH 2/4] feat: added validate transaction and transaction payer --- package.json | 2 +- src/api/endpoints/validateTransaction.ts | 100 +++++++++++++++++++++++ src/api/models.ts | 2 + src/transaction/models.ts | 19 +++++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/api/endpoints/validateTransaction.ts diff --git a/package.json b/package.json index 007a94f..ed9207e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oreid-js", - "version": "4.6.0", + "version": "4.7.2", "description": "Add authentication and signing to any blockchain app", "author": "AIKON", "license": "MIT", diff --git a/src/api/endpoints/validateTransaction.ts b/src/api/endpoints/validateTransaction.ts new file mode 100644 index 0000000..7d7c3bf --- /dev/null +++ b/src/api/endpoints/validateTransaction.ts @@ -0,0 +1,100 @@ +import OreIdContext from '../../core/IOreidContext' +import { ApiEndpoint, ChainNetwork, RequestType, ValidateTransactionFees, ValidateTransactionResources} from '../../models' +import { assertHasApiKeyOrAccessToken, assertParamsHaveRequiredValues } from '../helpers' +import { ApiResultWithErrorCode } from '../models' + +export type ValidateTransactionParams = { + chainNetwork: ChainNetwork + encodedTransaction?: string + transactionChainAccount?: string + transactionOptionsStringified?: string + transactionRecordId?: string +} + +export type ApiValidateTransactionBodyParams = { + chain_network: ChainNetwork + encoded_transaction?: string + transaction_chain_account?: string + transaction_options_stringified?: string + transaction_record_id?: string +} + +export type ValidateTransactionPayerParams = { + chainNetwork: ChainNetwork + encodedTransaction?: string + payerChainAccount: string + transactionChainAccount?: string + transactionOptionsStringified?: string + transactionRecordId?: string +} + +export type ApiValidateTransactionPayerBodyParams = { + chain_network: ChainNetwork + encoded_transaction?: string + payer_chain_account: string + transaction_chain_account?: string + transaction_options_stringified?: string + transaction_record_id?: string +} + +export type ValidateTransactionResult = { + isValid: boolean + canChange: boolean + validFrom: string + validTo: string + errorMessage: string + fees: ValidateTransactionFees + resources: ValidateTransactionResources + actions: string[] +} + +export type ApiCustodialSignStringResult = { + signature: string +} & ApiResultWithErrorCode + + +export async function callApiValidateTransaction( + oreIdContext: OreIdContext, + params: ValidateTransactionParams, +): Promise { + const apiName = ApiEndpoint.ValidateTransaction + + const { chainNetwork, encodedTransaction, transactionChainAccount, transactionOptionsStringified, transactionRecordId } = params + const body: ApiValidateTransactionBodyParams = { + chain_network: chainNetwork, + encoded_transaction: encodedTransaction, + transaction_chain_account: transactionChainAccount, + transaction_options_stringified: transactionOptionsStringified, + transaction_record_id: transactionRecordId + } + + assertHasApiKeyOrAccessToken(oreIdContext, apiName) + assertParamsHaveRequiredValues(params, ['chainNetwork'], apiName) + + const results = await oreIdContext.callOreIdApi(RequestType.Post, ApiEndpoint.ValidateTransaction, body, null) + return results +} + +export async function callApiValidatePayerTransaction( + oreIdContext: OreIdContext, + params: ValidateTransactionPayerParams, +): Promise { + const apiName = ApiEndpoint.ValidatePayerTransaction + + const { chainNetwork, encodedTransaction, payerChainAccount, transactionChainAccount, transactionOptionsStringified, transactionRecordId } = params + + const body: ApiValidateTransactionPayerBodyParams = { + chain_network: chainNetwork, + encoded_transaction: encodedTransaction, + payer_chain_account: payerChainAccount, + transaction_chain_account: transactionChainAccount, + transaction_options_stringified: transactionOptionsStringified, + transaction_record_id: transactionRecordId + } + + assertHasApiKeyOrAccessToken(oreIdContext, apiName) + assertParamsHaveRequiredValues(params, ['chainNetwork', 'payerChainAccount'], apiName) + + const results = await oreIdContext.callOreIdApi(RequestType.Post, ApiEndpoint.ValidatePayerTransaction, body, null) + return results +} diff --git a/src/api/models.ts b/src/api/models.ts index 43353c4..d5f785e 100644 --- a/src/api/models.ts +++ b/src/api/models.ts @@ -21,6 +21,8 @@ export enum ApiEndpoint { PasswordLessSendCode = 'account/login-passwordless-send-code', PasswordLessVerifyCode = 'account/login-passwordless-verify-code', TransactionSign = 'transaction/sign', + ValidatePayerTransaction = 'transaction/validate-payer', + ValidateTransaction = 'transaction/validate', UpdateDelayWalletSetup = 'account/update-delay-wallet-setup', } diff --git a/src/transaction/models.ts b/src/transaction/models.ts index 528f840..eec3538 100644 --- a/src/transaction/models.ts +++ b/src/transaction/models.ts @@ -35,5 +35,24 @@ export type TransactionSignOptions = { state?: string } +export type FeesByPriority = { + priority: any + fee: string + lowFeeErrorMessage: string +} + +export type ValidateTransactionFees = { + chainSupportsFees: boolean + feesByPriority: FeesByPriority[] + resourceEstimationType: string +} + +export type ValidateTransactionResources = { + chainSupportsResources: boolean + resourcesRequired: string + resourceEstimationType: string + lowResourceErrorMessages: string[] +} + export interface CreateTransactionData extends Omit {} From ec7f12ed1cbfb635c1a34835e8440c8d4abd4a8c Mon Sep 17 00:00:00 2001 From: Tray Lewin Date: Wed, 28 Jun 2023 11:11:33 -0700 Subject: [PATCH 3/4] bump version 4.8.0 --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 8fd6d48..00cf9a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "oreid-js", - "version": "4.7.2", "version": "4.8.0", "description": "Add authentication and signing to any blockchain app", "author": "AIKON", From 71ba04a6e7f886e343860a68094ecaddeeb67d2f Mon Sep 17 00:00:00 2001 From: Gaetano Mondelli Date: Wed, 5 Jul 2023 15:27:36 +0100 Subject: [PATCH 4/4] feat: added validate transaction and transaction payer --- src/api/endpoints/index.ts | 1 + src/api/endpoints/validateTransaction.ts | 9 +- src/core/oreId.spec.ts | 115 ++++++++++++++++++++--- src/core/oreId.ts | 13 ++- src/transaction/transaction.ts | 62 ++++++++++-- 5 files changed, 169 insertions(+), 31 deletions(-) diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts index f34113b..293c796 100644 --- a/src/api/endpoints/index.ts +++ b/src/api/endpoints/index.ts @@ -13,3 +13,4 @@ export * from './newUserWithToken' export * from './passwordLessSendCode' export * from './passwordLessVerifyCode' export * from './signTransaction' +export * from './validateTransaction' diff --git a/src/api/endpoints/validateTransaction.ts b/src/api/endpoints/validateTransaction.ts index 7d7c3bf..9cf1694 100644 --- a/src/api/endpoints/validateTransaction.ts +++ b/src/api/endpoints/validateTransaction.ts @@ -46,17 +46,12 @@ export type ValidateTransactionResult = { fees: ValidateTransactionFees resources: ValidateTransactionResources actions: string[] -} - -export type ApiCustodialSignStringResult = { - signature: string } & ApiResultWithErrorCode - export async function callApiValidateTransaction( oreIdContext: OreIdContext, params: ValidateTransactionParams, -): Promise { +): Promise { const apiName = ApiEndpoint.ValidateTransaction const { chainNetwork, encodedTransaction, transactionChainAccount, transactionOptionsStringified, transactionRecordId } = params @@ -78,7 +73,7 @@ export async function callApiValidateTransaction( export async function callApiValidatePayerTransaction( oreIdContext: OreIdContext, params: ValidateTransactionPayerParams, -): Promise { +): Promise { const apiName = ApiEndpoint.ValidatePayerTransaction const { chainNetwork, encodedTransaction, payerChainAccount, transactionChainAccount, transactionOptionsStringified, transactionRecordId } = params diff --git a/src/core/oreId.spec.ts b/src/core/oreId.spec.ts index 9edb827..eb015c6 100644 --- a/src/core/oreId.spec.ts +++ b/src/core/oreId.spec.ts @@ -1,14 +1,50 @@ import { OreIdOptions } from '../core/IOreIdOptions' import OreId from './oreId' -import Transaction from '../transaction/transaction' -import { callApiCustodialNewAccount, callApiCustodialMigrateAccount } from '../api' +import { callApiCustodialNewAccount, callApiCustodialMigrateAccount, callApiValidateTransaction } from '../api' +import { ChainNetwork, TransactionData, UserChainAccount, UserData } from '../models' + +const payerErrorMessage = 'a low resource error message' +const validationErrorMessage = 'a error message' + jest.mock('../api', () => ({ ...jest.requireActual('../api'), callApiCustodialMigrateAccount: jest.fn(), callApiCustodialNewAccount: jest.fn(), + callApiValidateTransaction: jest.fn().mockImplementation(() => Promise.resolve( + { + isValid: false, errorMessage: validationErrorMessage, + fees: { + chainSupportsFees: true, + feesByPriority: [ + { + priority: "low", + fee: "0", + } + ] + } + }, + )), + callApiValidatePayerTransaction: jest.fn().mockImplementation(() => Promise.resolve( + { + resources: { + chainSupportsResources: true, + resourcesRequired: true, + resourceEstimationType: "exact", + lowResourceErrorMessages: [payerErrorMessage], + }, + fees: { + chainSupportsFees: true, + feesByPriority: [ + { + priority: "low", + fee: "0", + lowFeeErrorMessage: "balance available is 0" + } + ], + }} + )), })) -jest.mock('../transaction/transaction') // use factories as this is good to ensure that the values are these, and that the tests do not change the values const getOptions = (): OreIdOptions => ({ @@ -94,18 +130,69 @@ describe('custodial Custodial Account', () => { }) }) -describe('Transaction', () => { - test('createTransaction', async () => { - //@ts-ignore - jest.spyOn(oreId._auth.user, 'hasData', 'get').mockReturnValue(true) - const transactionReturn = { param: 'return' } - ;(Transaction as jest.Mock).mockReturnValue(transactionReturn) +describe('Create new Transaction with createTransaction', () => { + const userChainAccount: UserChainAccount = { + chainNetwork: ChainNetwork.AlgoBeta, + chainAccount: 'chainAccount', + defaultPermission: { name: 'active' }, + permissions: [{ name: 'active' }], + } + + const userData: UserData = { + chainAccounts: [userChainAccount], + email: 'email', + name: 'name', + username: 'username', + picture: 'picture', + accountName: 'accountName', + } + + const transactionData : TransactionData = { + account: 'accountName', + chainAccount: 'chainAccount', + chainNetwork: ChainNetwork.AlgoBeta, + transaction: {"actions":[{"from":"HRFT6WNEDH5LAN4JTUQIVYFHPZB7JUMPHGIYZZZOMOKBHHUV4HGEFF3JFA","to":"TM4HSPWPRUHEHBVVAYGX3YQTQG5KSEZ4OMAN6NPGELNPYOB7SYEA2PODTQ","amount":1000000,"note":"Hello World"}]} + } + + beforeEach(() => { + jest.spyOn(oreId.auth.user, 'getData').mockResolvedValue(userData) + jest.spyOn(oreId.auth.user, 'hasData', 'get').mockReturnValue(true) + jest.spyOn(oreId.auth.user, 'accountName', 'get').mockReturnValue('accountName') + }) + + test('createTransaction should not throw but fill error fields on validation failure when doesNotThrow is true', async () => { + const result = await oreId.createTransaction(transactionData, true) + + expect(result.validationData).toBeDefined(); + expect(result.validationData.isValid).toBeFalsy(); + expect(result.validationError).toBe('a error message'); + expect(result.payerErrors).toContain('balance available is 0'); + expect(result.payerErrors).toContain('a low resource error message'); + }) + + test('createTransaction should throw an error on data validation failure when doesNotThrow is set to false', async () => { + try{ + await oreId.createTransaction(transactionData, false) + } + catch(error){ + // doesNotThrow is false by default + expect(error.message).toBe(`Validation error: ${validationErrorMessage}`); + } + }) - expect(Transaction).not.toBeCalled() - const transactionData = { param: 'my-params' } - const result = await oreId.createTransaction(transactionData as any) + test('createTransaction should throw an error on a payer validation failure when doesNotThrow is set to false', async () => { + (callApiValidateTransaction as jest.Mock).mockImplementationOnce(() => Promise.resolve( + { + isValid: true + } + )); - expect(result).toEqual(transactionReturn) - expect(Transaction).toBeCalledWith({ oreIdContext: oreId, user: expect.any(Object), data: transactionData }) + try{ + // doesNotThrow is false by default + await oreId.createTransaction(transactionData) + } + catch(error){ + expect(error.message).toBe(`Fee or Resource error: ${payerErrorMessage}`); + } }) }) diff --git a/src/core/oreId.ts b/src/core/oreId.ts index a5fa672..f3615dc 100644 --- a/src/core/oreId.ts +++ b/src/core/oreId.ts @@ -225,11 +225,20 @@ export default class OreId implements IOreidContext { } /** Create a new Transaction object - used for composing and signing transactions */ - async createTransaction(data: TransactionData) { + async createTransaction(data: TransactionData, dontThrowOnErrors: boolean = false) { if (!this._auth.user.hasData) { await this._auth.user.getData() } - return new Transaction({ oreIdContext: this, user: this.auth.user, data }) + const transaction = new Transaction({ oreIdContext: this, user: this.auth.user, data }) + await transaction.validate(); + + if (!dontThrowOnErrors && transaction.hasErrors) { + if (!transaction.validationData.isValid) { + throw new Error(`Validation error: ${transaction.validationData.errorMessage}`) + } + throw new Error(`Fee or Resource error: ${transaction.payerErrors[0]}`) + } + return transaction } /** Call the setBusyCallback() callback provided in optiont diff --git a/src/transaction/transaction.ts b/src/transaction/transaction.ts index f8698c7..e0e71d4 100644 --- a/src/transaction/transaction.ts +++ b/src/transaction/transaction.ts @@ -6,13 +6,18 @@ import { SignatureProviderSignResult, SignWithOreIdResult, TransactionData, + ValidateTransactionFees, + ValidateTransactionResources, } from '../models' import TransitHelper from '../transit/TransitHelper' import { assertHasApiKey, callApiCanAutosignTransaction, callApiCustodialSignTransaction, + callApiValidateTransaction, + callApiValidatePayerTransaction, callApiSignTransaction, + ValidateTransactionResult, } from '../api' import { getOreIdSignUrl } from '../core/urlGenerators' import Helpers from '../utils/helpers' @@ -20,7 +25,7 @@ import UalHelper from '../ual/UalHelper' import { User } from '../user/user' export default class Transaction { - constructor(args: { oreIdContext: OreIdContext; user: User; data: TransactionData }) { + constructor(args: { oreIdContext: OreIdContext; user: User; data: TransactionData }) { this._oreIdContext = args.oreIdContext this._user = args.user this.assertValidTransactionAndSetData(args.data) @@ -38,12 +43,48 @@ export default class Transaction { private _user: User + private _validationData: ValidateTransactionResult; + + private _hasErrors: boolean; + + private _payerFees: ValidateTransactionFees; + + private _payerResources: ValidateTransactionResources; + + private _payerErrors: string[]; + + private _validationError: string; + get data() { return this._data } + get validationData() { + return this._validationData + } + + get hasErrors() { + return this._hasErrors + } + + get payerFees() { + return this._payerFees + } + + get payerResources() { + return this._payerResources + } + + get payerErrors() { + return this._payerErrors + } + + get validationError() { + return this._validationError + } + /** ensure all required parameters are provided */ - assertValidTransactionAndSetData(createTransactionData: CreateTransactionData) { + async assertValidTransactionAndSetData(createTransactionData: CreateTransactionData) { const { chainNetwork, transaction, signedTransaction } = createTransactionData || {} const missingFields: string[] = [] const validationIssues: string[] = [] @@ -60,8 +101,6 @@ export default class Transaction { validationIssues.push('Transaction Data error - Expecting a user.accountName - is the user logged-in in?') if (transaction && signedTransaction) validationIssues.push('Only provide one: transaction OR signedTransaction') - // TODO: call this.validate() - // transaction OR signedTransaction - check for valid JSON object if (!Helpers.isNullOrEmpty(missingFields)) { @@ -124,11 +163,18 @@ export default class Transaction { // TODO: check user.chainAccounts that /** validates that transaction is well-formed for the blockcahin - * Returns array of errors + * Sets validation properties on the transaction object if data or payer validation fails */ - async validate(): Promise { - // TODO: call API validateTransaction on OREID Service - transaction/validate api endpoint - throw new Error('Not Implemented') + async validate() { + const payerChainAccount = this._data.chainAccount; + const validationResult = await callApiValidateTransaction(this._oreIdContext, this._data) + const {fees, resources} = await callApiValidatePayerTransaction(this._oreIdContext, {payerChainAccount, ...this._data}) + this._validationData = validationResult + this._validationError = validationResult.errorMessage + this._payerFees = fees + this._payerResources = resources + this._payerErrors = [...resources.lowResourceErrorMessages, ...fees.feesByPriority.map(f => f.lowFeeErrorMessage)] + this._hasErrors = !validationResult.isValid || this.payerErrors.length > 0 } // TODO: add depricated