Skip to content

Commit

Permalink
Merge pull request #1825 from aeternity/feature/fix-AeSdkMethods
Browse files Browse the repository at this point in the history
fix: `onAccount` option in AeSdkMethods
  • Loading branch information
davidyuk authored May 22, 2023
2 parents 0dcf36c + 290758b commit 0dcb239
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 57 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
27 changes: 22 additions & 5 deletions src/AeSdkBase.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<Node['getNodeInfo']>> & { name: string };

Expand All @@ -23,7 +25,7 @@ export default class AeSdkBase extends AeSdkMethods {
* @param options.nodes - Array of nodes
*/
constructor(
{ nodes = [], ...options }: ConstructorParameters<typeof AeSdkMethods>[0] & {
{ nodes = [], ...options }: AeSdkMethodsOptions & {
nodes?: Array<{ name: string; instance: Node }>;
} = {},
) {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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)),
};
}
}
63 changes: 23 additions & 40 deletions src/AeSdkMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value extends object>(valueCb: () => Value): Value {
export function getValueOrErrorProxy<Value extends object | undefined>(
valueCb: () => Value,
): NonNullable<Value> {
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<Value>;
}

const { InvalidTxError: _2, ...chainMethodsOther } = chainMethods;
Expand All @@ -51,9 +55,8 @@ type GetMethodsOptions <Methods extends { [key: string]: Function }> =
? Args[Decrement<Args['length']>] : never
};
type MethodsOptions = GetMethodsOptions<typeof methods>;
interface AeSdkMethodsOptions
export interface AeSdkMethodsOptions
extends Partial<UnionToIntersection<MethodsOptions[keyof MethodsOptions]>> {
nodes?: Array<{ name: string; instance: Node }>;
}

/**
Expand All @@ -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,
};
}

Expand All @@ -107,16 +101,7 @@ class AeSdkMethods {
async initializeContract<Methods extends ContractMethodsBase>(
options?: Omit<Parameters<typeof Contract.initialize>[0], 'onNode'> & { onNode?: Node },
): Promise<Contract<Methods>> {
const { onNode, onCompiler, ...otherOptions } = this._getOptions();
if (onCompiler == null || onNode == null) {
throw new ArgumentError('onCompiler, onNode', 'provided', null);
}
return Contract.initialize<Methods>({
...otherOptions,
onNode,
onCompiler,
...options,
});
return Contract.initialize<Methods>(this._getOptions(options as AeSdkMethodsOptions));
}
}

Expand Down Expand Up @@ -150,15 +135,13 @@ Object.assign(AeSdkMethods.prototype, mapObject<Function, Function>(
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<typeof AeSdkMethods>[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;
39 changes: 39 additions & 0 deletions test/integration/AeSdkMethods.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
14 changes: 6 additions & 8 deletions test/integration/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Expand Down
7 changes: 3 additions & 4 deletions test/integration/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down

0 comments on commit 0dcb239

Please sign in to comment.