Skip to content

Commit

Permalink
Merge pull request #295 from near/contract-fn-binding
Browse files Browse the repository at this point in the history
improve usability of contract functions
  • Loading branch information
chadoh authored May 13, 2020
2 parents a555453 + f980f3d commit 0e9e2c7
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 34 deletions.
24 changes: 16 additions & 8 deletions lib/contract.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 18 additions & 9 deletions src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { Account } from './account';
import { getTransactionLastResult } from './providers';
import { PositionalArgsError, ArgumentTypeError } from './utils/errors';

// Makes `function.name` return given name
function nameFunction(name, body) {
return {
[name](...args) {
return body(...args);
}
}[name];
}

/**
* Defines a smart contract on NEAR including the mutable and non-mutable methods
*/
Expand All @@ -18,26 +27,26 @@ export class Contract {
Object.defineProperty(this, methodName, {
writable: false,
enumerable: true,
value: async function(args: any) {
if (arguments.length > 1) {
value: nameFunction(methodName, async (args: object = {}, ...ignored) => {
if (ignored.length || Object.prototype.toString.call(args) !== '[object Object]') {
throw new PositionalArgsError();
}
return this.account.viewFunction(this.contractId, methodName, args || {});
}
return this.account.viewFunction(this.contractId, methodName, args);
})
});
});
changeMethods.forEach((methodName) => {
Object.defineProperty(this, methodName, {
writable: false,
enumerable: true,
value: async function(args: any, gas?: BN, amount?: BN) {
if (arguments.length > 3) {
value: nameFunction(methodName, async (args: object = {}, gas?: BN, amount?: BN, ...ignored) => {
if (ignored.length || Object.prototype.toString.call(args) !== '[object Object]') {
throw new PositionalArgsError();
}
validateBNLike({ gas, amount });
const rawResult = await this.account.functionCall(this.contractId, methodName, args || {}, gas, amount);
const rawResult = await this.account.functionCall(this.contractId, methodName, args, gas, amount);
return getTransactionLastResult(rawResult);
}
})
});
});
}
Expand All @@ -46,7 +55,7 @@ export class Contract {
/**
* Validation on arguments being a big number from bn.js
* Throws if an argument is not in BN format or otherwise invalid
* @param argMap
* @param argMap
*/
function validateBNLike(argMap: { [name: string]: any }) {
const bnLike = 'number, decimal string or BN';
Expand Down
17 changes: 0 additions & 17 deletions test/account.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,6 @@ describe('with deploy contract', () => {
expect(await contract.getValue()).toEqual(setCallValue);
});

test('view call gives error message when accidentally using positional arguments', async() => {
await expect(contract.hello('trex')).rejects.toThrow(/Contract method calls expect named arguments wrapped in object.+/);
await expect(contract.hello({ a: 1 }, 'trex')).rejects.toThrow(/Contract method calls expect named arguments wrapped in object.+/);
});

test('change call gives error message when accidentally using positional arguments', async() => {
await expect(contract.setValue('whatever')).rejects.toThrow(/Contract method calls expect named arguments wrapped in object.+/);
});

test('change call gives error message for invalid gas argument', async() => {
await expect(contract.setValue({ a: 1}, 'whatever')).rejects.toThrow(/Expected number, decimal string or BN for 'gas' argument, but got.+/);
});

test('change call gives error message for invalid amount argument', async() => {
await expect(contract.setValue({ a: 1}, 1000, 'whatever')).rejects.toThrow(/Expected number, decimal string or BN for 'amount' argument, but got.+/);
});

test('can get logs from method result', async () => {
await contract.generateLogs();
if (startFromVersion('0.4.11')) {
Expand Down
64 changes: 64 additions & 0 deletions test/contract.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const { Contract } = require('../lib/contract');
const { PositionalArgsError } = require('../lib/utils/errors');

const account = {
viewFunction() {
return this;
},
functionCall() {
return this;
}
};

const contract = new Contract(account, null, {
viewMethods: ['viewMethod'],
changeMethods: ['changeMethod'],
});

['viewMethod', 'changeMethod'].forEach(method => {
describe(method, () => {
test('returns what you expect for .name', () => {
expect(contract[method].name).toBe(method);
});

test('maintains correct reference to `this` when passed around an application', async () => {
function callFuncInNewContext(fn) {
return fn();
}
expect(await callFuncInNewContext(contract[method]));
});

test('throws PositionalArgsError if first argument is not an object', () => {
return Promise.all([
1,
'lol',
[],
new Date(),
null,
new Set(),
].map(async badArgs => {
try {
await contract[method](badArgs);
throw new Error(`Calling \`contract.${method}(${badArgs})\` worked. It shouldn't have worked.`);
} catch (e) {
if (!(e instanceof PositionalArgsError)) throw e;
}
}));
});

test('throws PositionalArgsError if given too many arguments', () => {
return expect(contract[method]({}, 1, 0, 'oops')).rejects.toBeInstanceOf(PositionalArgsError);
});
});
});

describe('changeMethod', () => {
test('throws error message for invalid gas argument', () => {
return expect(contract.changeMethod({ a: 1}, 'whatever')).rejects.toThrow(/Expected number, decimal string or BN for 'gas' argument, but got.+/);
});

test('gives error message for invalid amount argument', () => {
return expect(contract.changeMethod({ a: 1}, 1000, 'whatever')).rejects.toThrow(/Expected number, decimal string or BN for 'amount' argument, but got.+/);
});

});

0 comments on commit 0e9e2c7

Please sign in to comment.