From 290758b1884dc36209194bc6bd760c6063f9edd3 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Wed, 17 May 2023 11:29:15 +0600 Subject: [PATCH] fix: `onAccount` option in AeSdkMethods --- .eslintrc.js | 1 + src/AeSdkBase.ts | 27 +++++++++++--- src/AeSdkMethods.ts | 63 ++++++++++++-------------------- test/integration/AeSdkMethods.ts | 39 ++++++++++++++++++++ test/integration/accounts.ts | 14 +++---- test/integration/rpc.ts | 7 ++-- 6 files changed, 94 insertions(+), 57 deletions(-) create mode 100644 test/integration/AeSdkMethods.ts diff --git a/.eslintrc.js b/.eslintrc.js index 5b9286c39a..1106acb49f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { ], ignorePatterns: [ 'dist', 'es', 'src/apis', 'docs/api', 'test/environment/ledger/browser', 'types-legacy', + 'docs/examples', 'site', ], rules: { 'rulesdir/tsdoc-syntax': 'error', diff --git a/src/AeSdkBase.ts b/src/AeSdkBase.ts index f2c21b4a45..7d954ba413 100644 --- a/src/AeSdkBase.ts +++ b/src/AeSdkBase.ts @@ -1,9 +1,11 @@ import Node from './Node'; import AccountBase from './account/Base'; -import { CompilerError, DuplicateNodeError, NodeNotFoundError } from './utils/errors'; +import { + CompilerError, DuplicateNodeError, NodeNotFoundError, NotImplementedError, TypeError, +} from './utils/errors'; import { Encoded } from './utils/encoder'; import CompilerBase from './contract/compiler/Base'; -import AeSdkMethods, { OnAccount, getValueOrErrorProxy } from './AeSdkMethods'; +import AeSdkMethods, { OnAccount, getValueOrErrorProxy, AeSdkMethodsOptions } from './AeSdkMethods'; type NodeInfo = Awaited> & { name: string }; @@ -23,7 +25,7 @@ export default class AeSdkBase extends AeSdkMethods { * @param options.nodes - Array of nodes */ constructor( - { nodes = [], ...options }: ConstructorParameters[0] & { + { nodes = [], ...options }: AeSdkMethodsOptions & { nodes?: Array<{ name: string; instance: Node }>; } = {}, ) { @@ -127,6 +129,19 @@ export default class AeSdkBase extends AeSdkMethods { return []; } + /** + * Resolves an account + * @param account - ak-address, instance of AccountBase, or keypair + */ + _resolveAccount(account: OnAccount = this._options.onAccount): AccountBase { + if (typeof account === 'string') throw new NotImplementedError('Address in AccountResolver'); + if (typeof account === 'object') return account; + throw new TypeError( + 'Account should be an address (ak-prefixed string), ' + + `or instance of AccountBase, got ${String(account)} instead`, + ); + } + get address(): Encoded.AccountAddress { return this._resolveAccount().address; } @@ -153,15 +168,17 @@ export default class AeSdkBase extends AeSdkMethods { return this._resolveAccount(onAccount).signMessage(message, options); } - override _getOptions(): { + override _getOptions(callOptions: AeSdkMethodsOptions = {}): { onNode: Node; onAccount: AccountBase; onCompiler: CompilerBase; } { return { - ...super._getOptions(), + ...this._options, onNode: getValueOrErrorProxy(() => this.api), onCompiler: getValueOrErrorProxy(() => this.compilerApi), + ...callOptions, + onAccount: getValueOrErrorProxy(() => this._resolveAccount(callOptions.onAccount)), }; } } diff --git a/src/AeSdkMethods.ts b/src/AeSdkMethods.ts index 4f065b4745..70e1b062bd 100644 --- a/src/AeSdkMethods.ts +++ b/src/AeSdkMethods.ts @@ -11,25 +11,29 @@ import Node from './Node'; import { TxParamsAsync } from './tx/builder/schema.generated'; import AccountBase from './account/Base'; import { Encoded } from './utils/encoder'; -import { ArgumentError, NotImplementedError, TypeError } from './utils/errors'; +import { NotImplementedError } from './utils/errors'; +import CompilerBase from './contract/compiler/Base'; export type OnAccount = Encoded.AccountAddress | AccountBase | undefined; -export function getValueOrErrorProxy(valueCb: () => Value): Value { +export function getValueOrErrorProxy( + valueCb: () => Value, +): NonNullable { return new Proxy({}, { ...Object.fromEntries([ 'apply', 'construct', 'defineProperty', 'deleteProperty', 'getOwnPropertyDescriptor', 'getPrototypeOf', 'isExtensible', 'ownKeys', 'preventExtensions', 'set', 'setPrototypeOf', ].map((name) => [name, () => { throw new NotImplementedError(`${name} proxy request`); }])), get(t: {}, property: string | symbol, receiver: any) { - const target = valueCb(); + const target = valueCb() as object; // to get a native exception in case it missed const value = Reflect.get(target, property, receiver); return typeof value === 'function' ? value.bind(target) : value; }, has(t: {}, property: string | symbol) { - return Reflect.has(valueCb(), property); + const target = valueCb() as object; // to get a native exception in case it missed + return Reflect.has(target, property); }, - }) as Value; + }) as NonNullable; } const { InvalidTxError: _2, ...chainMethodsOther } = chainMethods; @@ -51,9 +55,8 @@ type GetMethodsOptions = ? Args[Decrement] : never }; type MethodsOptions = GetMethodsOptions; -interface AeSdkMethodsOptions +export interface AeSdkMethodsOptions extends Partial> { - nodes?: Array<{ name: string; instance: Node }>; } /** @@ -79,24 +82,15 @@ class AeSdkMethods { Object.assign(this._options, options); } - /** - * Resolves an account - * @param account - ak-address, instance of AccountBase, or keypair - */ - // eslint-disable-next-line class-methods-use-this - _resolveAccount(account?: OnAccount): AccountBase { - if (typeof account === 'string') throw new NotImplementedError('Address in AccountResolver'); - if (typeof account === 'object') return account; - throw new TypeError( - 'Account should be an address (ak-prefixed string), ' - + `or instance of AccountBase, got ${String(account)} instead`, - ); - } - - _getOptions(): AeSdkMethodsOptions & { onAccount: AccountBase } { + _getOptions( + callOptions: AeSdkMethodsOptions = {}, + ): AeSdkMethodsOptions & { onAccount: AccountBase; onCompiler: CompilerBase; onNode: Node } { return { ...this._options, - onAccount: getValueOrErrorProxy(() => this._resolveAccount()), + onAccount: getValueOrErrorProxy(() => this._options.onAccount), + onNode: getValueOrErrorProxy(() => this._options.onNode), + onCompiler: getValueOrErrorProxy(() => this._options.onCompiler), + ...callOptions, }; } @@ -107,16 +101,7 @@ class AeSdkMethods { async initializeContract( options?: Omit[0], 'onNode'> & { onNode?: Node }, ): Promise> { - const { onNode, onCompiler, ...otherOptions } = this._getOptions(); - if (onCompiler == null || onNode == null) { - throw new ArgumentError('onCompiler, onNode', 'provided', null); - } - return Contract.initialize({ - ...otherOptions, - onNode, - onCompiler, - ...options, - }); + return Contract.initialize(this._getOptions(options as AeSdkMethodsOptions)); } } @@ -150,15 +135,13 @@ Object.assign(AeSdkMethods.prototype, mapObject( function methodWrapper(this: AeSdkMethods, ...args: any[]) { args.length = handler.length; const options = args[args.length - 1]; - args[args.length - 1] = { - ...this._getOptions(), - ...options, - ...options?.onAccount != null && { onAccount: this._resolveAccount(options.onAccount) }, - }; + args[args.length - 1] = this._getOptions(options); return handler(...args); }, ], )); -export default AeSdkMethods as new (options?: ConstructorParameters[0]) => -AeSdkMethods & AeSdkMethodsTransformed; +type AeSdkMethodsTyped = AeSdkMethods & AeSdkMethodsTransformed; +// eslint-disable-next-line @typescript-eslint/no-redeclare +const AeSdkMethodsTyped = AeSdkMethods as new (options?: AeSdkMethodsOptions) => AeSdkMethodsTyped; +export default AeSdkMethodsTyped; diff --git a/test/integration/AeSdkMethods.ts b/test/integration/AeSdkMethods.ts new file mode 100644 index 0000000000..efeb49a2b8 --- /dev/null +++ b/test/integration/AeSdkMethods.ts @@ -0,0 +1,39 @@ +import { describe, it, before } from 'mocha'; +import { expect } from 'chai'; +import { getSdk, url, compilerUrl } from '.'; +import { assertNotNull } from '../utils'; +import { + AeSdkMethods, Node, CompilerHttp, AccountBase, +} from '../../src'; + +describe('AeSdkMethods', () => { + let accounts: AccountBase[]; + let aeSdkMethods: AeSdkMethods; + + before(async () => { + accounts = Object.values((await getSdk(2)).accounts); + aeSdkMethods = new AeSdkMethods({ + onAccount: accounts[0], + onNode: new Node(url), + onCompiler: new CompilerHttp(compilerUrl), + }); + }); + + it('spend coins', async () => { + const { tx } = await aeSdkMethods.spend(1, accounts[1].address); + assertNotNull(tx); + expect(tx.senderId).to.equal(accounts[0].address); + expect(tx.recipientId).to.equal(accounts[1].address); + }); + + it('created contract remains connected to sdk', async () => { + const contract = await aeSdkMethods.initializeContract({ + sourceCode: '' + + 'contract Identity =\n' + + ' entrypoint getArg(x : int) = x', + }); + expect(contract.$options.onAccount?.address).to.be.eql(accounts[0].address); + [, aeSdkMethods._options.onAccount] = accounts; + expect(contract.$options.onAccount?.address).to.be.eql(accounts[1].address); + }); +}); diff --git a/test/integration/accounts.ts b/test/integration/accounts.ts index 899d365b9f..c36221f22e 100644 --- a/test/integration/accounts.ts +++ b/test/integration/accounts.ts @@ -149,19 +149,17 @@ describe('Accounts', () => { tx.senderId.should.be.equal(onAccount); }); - it('Fail on invalid account', () => { - expect(() => { - aeSdk.spend(1, aeSdk.address, { onAccount: 1 as any }); - }).to.throw( + it('Fail on invalid account', async () => { + await expect(aeSdk.spend(1, aeSdk.address, { onAccount: 1 as any })).to.be.rejectedWith( TypeError, 'Account should be an address (ak-prefixed string), or instance of AccountBase, got 1 instead', ); }); - it('Fail on non exist account', () => { - expect(() => { - aeSdk.spend(1, aeSdk.address, { onAccount: 'ak_q2HatMwDnwCBpdNtN9oXf5gpD9pGSgFxaa8i2Evcam6gjiggk' }); - }).to.throw( + it('Fail on non exist account', async () => { + await expect( + aeSdk.spend(1, aeSdk.address, { onAccount: 'ak_q2HatMwDnwCBpdNtN9oXf5gpD9pGSgFxaa8i2Evcam6gjiggk' }), + ).to.be.rejectedWith( UnavailableAccountError, 'Account for ak_q2HatMwDnwCBpdNtN9oXf5gpD9pGSgFxaa8i2Evcam6gjiggk not available', ); diff --git a/test/integration/rpc.ts b/test/integration/rpc.ts index 393a1a8bbe..29870d2a4d 100644 --- a/test/integration/rpc.ts +++ b/test/integration/rpc.ts @@ -223,11 +223,10 @@ describe('Aepp<->Wallet', function aeppWallet() { Object.keys(subscriptionResponse.address.connected).length.should.be.equal(1); }); - it('Try to use `onAccount` for not existent account', () => { + it('Try to use `onAccount` for not existent account', async () => { const { publicKey } = generateKeyPair(); - expect(() => { - aepp.spend(100, publicKey, { onAccount: publicKey }); - }).to.throw(UnAuthorizedAccountError, `You do not have access to account ${publicKey}`); + await expect(aepp.spend(100, publicKey, { onAccount: publicKey })) + .to.be.rejectedWith(UnAuthorizedAccountError, `You do not have access to account ${publicKey}`); }); it('aepp accepts key pairs in onAccount', async () => {