Skip to content

Commit 0dcb239

Browse files
authored
Merge pull request #1825 from aeternity/feature/fix-AeSdkMethods
fix: `onAccount` option in AeSdkMethods
2 parents 0dcf36c + 290758b commit 0dcb239

File tree

6 files changed

+94
-57
lines changed

6 files changed

+94
-57
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module.exports = {
2323
],
2424
ignorePatterns: [
2525
'dist', 'es', 'src/apis', 'docs/api', 'test/environment/ledger/browser', 'types-legacy',
26+
'docs/examples', 'site',
2627
],
2728
rules: {
2829
'rulesdir/tsdoc-syntax': 'error',

src/AeSdkBase.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import Node from './Node';
22
import AccountBase from './account/Base';
3-
import { CompilerError, DuplicateNodeError, NodeNotFoundError } from './utils/errors';
3+
import {
4+
CompilerError, DuplicateNodeError, NodeNotFoundError, NotImplementedError, TypeError,
5+
} from './utils/errors';
46
import { Encoded } from './utils/encoder';
57
import CompilerBase from './contract/compiler/Base';
6-
import AeSdkMethods, { OnAccount, getValueOrErrorProxy } from './AeSdkMethods';
8+
import AeSdkMethods, { OnAccount, getValueOrErrorProxy, AeSdkMethodsOptions } from './AeSdkMethods';
79

810
type NodeInfo = Awaited<ReturnType<Node['getNodeInfo']>> & { name: string };
911

@@ -23,7 +25,7 @@ export default class AeSdkBase extends AeSdkMethods {
2325
* @param options.nodes - Array of nodes
2426
*/
2527
constructor(
26-
{ nodes = [], ...options }: ConstructorParameters<typeof AeSdkMethods>[0] & {
28+
{ nodes = [], ...options }: AeSdkMethodsOptions & {
2729
nodes?: Array<{ name: string; instance: Node }>;
2830
} = {},
2931
) {
@@ -127,6 +129,19 @@ export default class AeSdkBase extends AeSdkMethods {
127129
return [];
128130
}
129131

132+
/**
133+
* Resolves an account
134+
* @param account - ak-address, instance of AccountBase, or keypair
135+
*/
136+
_resolveAccount(account: OnAccount = this._options.onAccount): AccountBase {
137+
if (typeof account === 'string') throw new NotImplementedError('Address in AccountResolver');
138+
if (typeof account === 'object') return account;
139+
throw new TypeError(
140+
'Account should be an address (ak-prefixed string), '
141+
+ `or instance of AccountBase, got ${String(account)} instead`,
142+
);
143+
}
144+
130145
get address(): Encoded.AccountAddress {
131146
return this._resolveAccount().address;
132147
}
@@ -153,15 +168,17 @@ export default class AeSdkBase extends AeSdkMethods {
153168
return this._resolveAccount(onAccount).signMessage(message, options);
154169
}
155170

156-
override _getOptions(): {
171+
override _getOptions(callOptions: AeSdkMethodsOptions = {}): {
157172
onNode: Node;
158173
onAccount: AccountBase;
159174
onCompiler: CompilerBase;
160175
} {
161176
return {
162-
...super._getOptions(),
177+
...this._options,
163178
onNode: getValueOrErrorProxy(() => this.api),
164179
onCompiler: getValueOrErrorProxy(() => this.compilerApi),
180+
...callOptions,
181+
onAccount: getValueOrErrorProxy(() => this._resolveAccount(callOptions.onAccount)),
165182
};
166183
}
167184
}

src/AeSdkMethods.ts

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,29 @@ import Node from './Node';
1111
import { TxParamsAsync } from './tx/builder/schema.generated';
1212
import AccountBase from './account/Base';
1313
import { Encoded } from './utils/encoder';
14-
import { ArgumentError, NotImplementedError, TypeError } from './utils/errors';
14+
import { NotImplementedError } from './utils/errors';
15+
import CompilerBase from './contract/compiler/Base';
1516

1617
export type OnAccount = Encoded.AccountAddress | AccountBase | undefined;
1718

18-
export function getValueOrErrorProxy<Value extends object>(valueCb: () => Value): Value {
19+
export function getValueOrErrorProxy<Value extends object | undefined>(
20+
valueCb: () => Value,
21+
): NonNullable<Value> {
1922
return new Proxy({}, {
2023
...Object.fromEntries([
2124
'apply', 'construct', 'defineProperty', 'deleteProperty', 'getOwnPropertyDescriptor',
2225
'getPrototypeOf', 'isExtensible', 'ownKeys', 'preventExtensions', 'set', 'setPrototypeOf',
2326
].map((name) => [name, () => { throw new NotImplementedError(`${name} proxy request`); }])),
2427
get(t: {}, property: string | symbol, receiver: any) {
25-
const target = valueCb();
28+
const target = valueCb() as object; // to get a native exception in case it missed
2629
const value = Reflect.get(target, property, receiver);
2730
return typeof value === 'function' ? value.bind(target) : value;
2831
},
2932
has(t: {}, property: string | symbol) {
30-
return Reflect.has(valueCb(), property);
33+
const target = valueCb() as object; // to get a native exception in case it missed
34+
return Reflect.has(target, property);
3135
},
32-
}) as Value;
36+
}) as NonNullable<Value>;
3337
}
3438

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

5962
/**
@@ -79,24 +82,15 @@ class AeSdkMethods {
7982
Object.assign(this._options, options);
8083
}
8184

82-
/**
83-
* Resolves an account
84-
* @param account - ak-address, instance of AccountBase, or keypair
85-
*/
86-
// eslint-disable-next-line class-methods-use-this
87-
_resolveAccount(account?: OnAccount): AccountBase {
88-
if (typeof account === 'string') throw new NotImplementedError('Address in AccountResolver');
89-
if (typeof account === 'object') return account;
90-
throw new TypeError(
91-
'Account should be an address (ak-prefixed string), '
92-
+ `or instance of AccountBase, got ${String(account)} instead`,
93-
);
94-
}
95-
96-
_getOptions(): AeSdkMethodsOptions & { onAccount: AccountBase } {
85+
_getOptions(
86+
callOptions: AeSdkMethodsOptions = {},
87+
): AeSdkMethodsOptions & { onAccount: AccountBase; onCompiler: CompilerBase; onNode: Node } {
9788
return {
9889
...this._options,
99-
onAccount: getValueOrErrorProxy(() => this._resolveAccount()),
90+
onAccount: getValueOrErrorProxy(() => this._options.onAccount),
91+
onNode: getValueOrErrorProxy(() => this._options.onNode),
92+
onCompiler: getValueOrErrorProxy(() => this._options.onCompiler),
93+
...callOptions,
10094
};
10195
}
10296

@@ -107,16 +101,7 @@ class AeSdkMethods {
107101
async initializeContract<Methods extends ContractMethodsBase>(
108102
options?: Omit<Parameters<typeof Contract.initialize>[0], 'onNode'> & { onNode?: Node },
109103
): Promise<Contract<Methods>> {
110-
const { onNode, onCompiler, ...otherOptions } = this._getOptions();
111-
if (onCompiler == null || onNode == null) {
112-
throw new ArgumentError('onCompiler, onNode', 'provided', null);
113-
}
114-
return Contract.initialize<Methods>({
115-
...otherOptions,
116-
onNode,
117-
onCompiler,
118-
...options,
119-
});
104+
return Contract.initialize<Methods>(this._getOptions(options as AeSdkMethodsOptions));
120105
}
121106
}
122107

@@ -150,15 +135,13 @@ Object.assign(AeSdkMethods.prototype, mapObject<Function, Function>(
150135
function methodWrapper(this: AeSdkMethods, ...args: any[]) {
151136
args.length = handler.length;
152137
const options = args[args.length - 1];
153-
args[args.length - 1] = {
154-
...this._getOptions(),
155-
...options,
156-
...options?.onAccount != null && { onAccount: this._resolveAccount(options.onAccount) },
157-
};
138+
args[args.length - 1] = this._getOptions(options);
158139
return handler(...args);
159140
},
160141
],
161142
));
162143

163-
export default AeSdkMethods as new (options?: ConstructorParameters<typeof AeSdkMethods>[0]) =>
164-
AeSdkMethods & AeSdkMethodsTransformed;
144+
type AeSdkMethodsTyped = AeSdkMethods & AeSdkMethodsTransformed;
145+
// eslint-disable-next-line @typescript-eslint/no-redeclare
146+
const AeSdkMethodsTyped = AeSdkMethods as new (options?: AeSdkMethodsOptions) => AeSdkMethodsTyped;
147+
export default AeSdkMethodsTyped;

test/integration/AeSdkMethods.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, before } from 'mocha';
2+
import { expect } from 'chai';
3+
import { getSdk, url, compilerUrl } from '.';
4+
import { assertNotNull } from '../utils';
5+
import {
6+
AeSdkMethods, Node, CompilerHttp, AccountBase,
7+
} from '../../src';
8+
9+
describe('AeSdkMethods', () => {
10+
let accounts: AccountBase[];
11+
let aeSdkMethods: AeSdkMethods;
12+
13+
before(async () => {
14+
accounts = Object.values((await getSdk(2)).accounts);
15+
aeSdkMethods = new AeSdkMethods({
16+
onAccount: accounts[0],
17+
onNode: new Node(url),
18+
onCompiler: new CompilerHttp(compilerUrl),
19+
});
20+
});
21+
22+
it('spend coins', async () => {
23+
const { tx } = await aeSdkMethods.spend(1, accounts[1].address);
24+
assertNotNull(tx);
25+
expect(tx.senderId).to.equal(accounts[0].address);
26+
expect(tx.recipientId).to.equal(accounts[1].address);
27+
});
28+
29+
it('created contract remains connected to sdk', async () => {
30+
const contract = await aeSdkMethods.initializeContract({
31+
sourceCode: ''
32+
+ 'contract Identity =\n'
33+
+ ' entrypoint getArg(x : int) = x',
34+
});
35+
expect(contract.$options.onAccount?.address).to.be.eql(accounts[0].address);
36+
[, aeSdkMethods._options.onAccount] = accounts;
37+
expect(contract.$options.onAccount?.address).to.be.eql(accounts[1].address);
38+
});
39+
});

test/integration/accounts.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,17 @@ describe('Accounts', () => {
149149
tx.senderId.should.be.equal(onAccount);
150150
});
151151

152-
it('Fail on invalid account', () => {
153-
expect(() => {
154-
aeSdk.spend(1, aeSdk.address, { onAccount: 1 as any });
155-
}).to.throw(
152+
it('Fail on invalid account', async () => {
153+
await expect(aeSdk.spend(1, aeSdk.address, { onAccount: 1 as any })).to.be.rejectedWith(
156154
TypeError,
157155
'Account should be an address (ak-prefixed string), or instance of AccountBase, got 1 instead',
158156
);
159157
});
160158

161-
it('Fail on non exist account', () => {
162-
expect(() => {
163-
aeSdk.spend(1, aeSdk.address, { onAccount: 'ak_q2HatMwDnwCBpdNtN9oXf5gpD9pGSgFxaa8i2Evcam6gjiggk' });
164-
}).to.throw(
159+
it('Fail on non exist account', async () => {
160+
await expect(
161+
aeSdk.spend(1, aeSdk.address, { onAccount: 'ak_q2HatMwDnwCBpdNtN9oXf5gpD9pGSgFxaa8i2Evcam6gjiggk' }),
162+
).to.be.rejectedWith(
165163
UnavailableAccountError,
166164
'Account for ak_q2HatMwDnwCBpdNtN9oXf5gpD9pGSgFxaa8i2Evcam6gjiggk not available',
167165
);

test/integration/rpc.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,10 @@ describe('Aepp<->Wallet', function aeppWallet() {
223223
Object.keys(subscriptionResponse.address.connected).length.should.be.equal(1);
224224
});
225225

226-
it('Try to use `onAccount` for not existent account', () => {
226+
it('Try to use `onAccount` for not existent account', async () => {
227227
const { publicKey } = generateKeyPair();
228-
expect(() => {
229-
aepp.spend(100, publicKey, { onAccount: publicKey });
230-
}).to.throw(UnAuthorizedAccountError, `You do not have access to account ${publicKey}`);
228+
await expect(aepp.spend(100, publicKey, { onAccount: publicKey }))
229+
.to.be.rejectedWith(UnAuthorizedAccountError, `You do not have access to account ${publicKey}`);
231230
});
232231

233232
it('aepp accepts key pairs in onAccount', async () => {

0 commit comments

Comments
 (0)