Skip to content

Commit

Permalink
Merge pull request #17 from near/cross-js-contract-call
Browse files Browse the repository at this point in the history
cross-contract-call example
  • Loading branch information
ailisp committed May 16, 2022
2 parents 79314ea + 39e83c8 commit ba8b04e
Show file tree
Hide file tree
Showing 11 changed files with 2,382 additions and 3 deletions.
46 changes: 46 additions & 0 deletions examples/cross-contract-call/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Enclaved cross-contract-call in JavaScript

This example demonstrates how you can make a cross-contract call (contract call that is made from inside of the contract). Here we have 2 smart contracts: `status_message` from `../status-message` example and `on-call` contract in `src`. When somebody is trying to set a person on-call, smart-contract will check if the person is `AVAILABLE`. A person's status is in control of the person itself.

A good place to start is the integration test stored in `__tests__/` folder.

## Build the contract

First ensure JSVM contract is built and deployed locally, follow [Local Installation](https://github.com/near/near-sdk-js#local-installation). Then run:
```
yarn
yarn build
```

The result contract bytecode file will be in `build/on-call.base64`. An intermediate JavaScript file can be found in `build/on-call.js`. You'll only need the `base64` file to deploy contract to chain. The intermediate JavaScript file is for the curious user and near-sdk-js developers to understand what code generation happened under the hood.

## Test the contract with workspaces-js
```
yarn test
```

## Deploy the contract

Suppose JSVM contract was deployed to `jsvm.test.near`. Now you want to deploy the status-message contract to `status-message.test.near` and on-call contract to `on-call.test.near`. Create `status-message.test.near`, `on-call.test.near`, `alice.test.near` and `bob.test.near` locally. Then deploy the contracts following this pattern:

```
export NEAR_ENV=local
near call jsvm.test.near deploy_js_contract --accountId <accoundId> --args $(cat <contract-name>.base64) --base64 --deposit 0.1
```

## Initialize the contract

Now we need to initialize `status-message` and `on-call` contracts after deployment is ready, call the `init` method which will execute `new OnCall()` or `new StatusMessage()` respectfully.

Go back to the root dir of near-sdk-js, where we have a helper `encode-call.js` and use this pattern to init contracts:

```
near call jsvm.test.near call_js_contract --accountId <accountId> --base64 --args $(node encode_call.js <accountId> init '')
```

## Call the contract
Under the root dir of near-sdk-js, call the `set_status` and `set_person_on_call` using this pattern:

```
near call jsvm.test.near call_js_contract --accountId <accountID> --base64 --args $(node encode_call.js <contract-account-id> <function-name> '[<parameter>]') --deposit 0.1
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Worker } from 'near-workspaces';
import {readFile} from 'fs/promises'
import test from 'ava';

// TODO: make this function part of the npm package when it is available
function encodeCall(contract, method, args) {
return Buffer.concat([Buffer.from(contract), Buffer.from([0]), Buffer.from(method), Buffer.from([0]), Buffer.from(JSON.stringify(args))])
}

test.beforeEach(async t => {
// Init the worker and start a Sandbox server
const worker = await Worker.init();

// Prepare sandbox for tests, create accounts, deploy contracts, etx.
const root = worker.rootAccount;

// Deploy the jsvm contract.
const jsvm = await root.createAndDeploy(
root.getSubAccount('jsvm').accountId,
'build/jsvm.wasm',
);

// Deploy status-message JS contract
const statusMessageContract = await root.createSubAccount('status-message');
let statusContractBase64 = (await readFile('res/status-message.base64')).toString();
await statusMessageContract.call(jsvm, 'deploy_js_contract', Buffer.from(statusContractBase64, 'base64'), {attachedDeposit: '400000000000000000000000'});
await statusMessageContract.call(jsvm, 'call_js_contract', encodeCall(statusMessageContract.accountId, 'init', []), {attachedDeposit: '400000000000000000000000'});

// Deploy on-call contrat
const onCallContract = await root.createSubAccount('on-call');
let cross_cc_contract_base64 = (await readFile('build/on-call.base64')).toString();
await onCallContract.call(jsvm, 'deploy_js_contract', Buffer.from(cross_cc_contract_base64, 'base64'), {attachedDeposit: '400000000000000000000000'});
await onCallContract.call(jsvm, 'call_js_contract', encodeCall(onCallContract.accountId, 'init', []), {attachedDeposit: '400000000000000000000000'});

// Create test accounts
const ali = await root.createSubAccount('ali');
const bob = await root.createSubAccount('bob');

// Save state for test runs, it is unique for each test
t.context.worker = worker;
t.context.accounts = {
root,
jsvm,
statusMessageContract,
onCallContract,
ali,
bob,
};
});

test.afterEach(async t => {
await t.context.worker.tearDown().catch(error => {
console.log('Failed tear down the worker:', error);
});
});

test('Nobody is on-call in the beginning', async t => {
const { jsvm, onCallContract } = t.context.accounts;
const result = await jsvm.view('view_js_contract', encodeCall(onCallContract.accountId, 'person_on_call', []));
t.is(result, 'undefined');
});

test('Person can be set on-call if AVAILABLE', async t => {
const { ali, bob, jsvm, statusMessageContract, onCallContract } = t.context.accounts;

// Ali set her status as AVAILABLE
await ali.call(jsvm, 'call_js_contract', encodeCall(statusMessageContract.accountId, 'set_status', ['AVAILABLE']), {attachedDeposit: '100000000000000000000000'});
// Bob sets Ali on-call
await bob.call(jsvm, 'call_js_contract', encodeCall(onCallContract.accountId, 'set_person_on_call', [ali.accountId]), {attachedDeposit: '100000000000000000000000'});

// Check that Ali is on-call
t.is(
await jsvm.view('view_js_contract', encodeCall(onCallContract.accountId, 'person_on_call', [])),
ali.accountId
);
});

test('Person can NOT be set on-call if UNAVAILABLE', async t => {
const { ali, bob, jsvm, statusMessageContract, onCallContract } = t.context.accounts;

// Ali set her status as AVAILABLE
await ali.call(jsvm, 'call_js_contract', encodeCall(statusMessageContract.accountId, 'set_status', ['UNAVAILABLE']), {attachedDeposit: '100000000000000000000000'});
// Bob tries to sets Ali on-call
await bob.call(jsvm, 'call_js_contract', encodeCall(onCallContract.accountId, 'set_person_on_call', [ali.accountId]), {attachedDeposit: '100000000000000000000000'});

// Check that Ali is NOT on-call
t.not(
await jsvm.view('view_js_contract', encodeCall(onCallContract.accountId, 'person_on_call', [])),
ali.accountId
);
});
8 changes: 8 additions & 0 deletions examples/cross-contract-call/ava.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require('util').inspect.defaultOptions.depth = 5; // Increase AVA's printing depth

module.exports = {
timeout: '300000',
files: ['**/*.ava.js'],
failWithoutAssertions: false,
extensions: ['js'],
};
6 changes: 6 additions & 0 deletions examples/cross-contract-call/babel.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"plugins": [
"../../sdk/near-bindgen-exporter",
["@babel/plugin-proposal-decorators", {"version": "legacy"}]
]
}
8 changes: 8 additions & 0 deletions examples/cross-contract-call/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail

mkdir -p build
rollup -c
cd build
cp ../../../jsvm.wasm .
../../../builder.sh on-call.js
28 changes: 28 additions & 0 deletions examples/cross-contract-call/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "on-call",
"version": "1.0.0",
"description": "Cross contract call example with near-sdk-js",
"main": "index.js",
"type": "module",
"scripts": {
"build": "./build.sh",
"test": "ava"
},
"author": "Near Inc <[email protected]>",
"license": "Apache-2.0",
"dependencies": {
"lodash": "^4.17.21",
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@babel/core": "^7.17.5",
"@babel/plugin-proposal-decorators": "^7.17.2",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.1.1",
"ava": "^4.2.0",
"near-workspaces": "^2.0.0",
"rollup": "^2.61.1",
"rollup-plugin-sourcemaps": "^0.6.3"
}
}
1 change: 1 addition & 0 deletions examples/cross-contract-call/res/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
JS Contract from `../../status-message` example.
20 changes: 20 additions & 0 deletions examples/cross-contract-call/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import sourcemaps from 'rollup-plugin-sourcemaps';
import babel from '@rollup/plugin-babel';

// import commonjs from '@rollup/plugin-commonjs';

export default {
input: 'src/index.js',
output: {
sourcemap: true,
file: 'build/on-call.js',
format: 'es'
},
plugins: [
nodeResolve(),
sourcemaps(),
// commonjs(),
babel({babelHelpers: "bundled"})
]
};
28 changes: 28 additions & 0 deletions examples/cross-contract-call/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {NearContract, NearBindgen, call, view, near} from '../../../sdk'

@NearBindgen
class OnCall extends NearContract {
constructor() {
super()
this.personOnCall = "undefined"
}

@call
set_person_on_call(accountId) {
env.log(`Trying to set ${accountId} on-call`)
const status = near.jsvmCall('status-message.test.near', 'get_status', [accountId])
env.log(`${accountId} status is ${status}`)
if (status === 'AVAILABLE') {
this.personOnCall = accountId
env.log(`${accountId} set on-call`)
} else {
env.log(`${accountId} can not be set on-call`)
}
}

@view
person_on_call() {
env.log(`Returning person on-call: ${this.personOnCall}`)
return this.personOnCall
}
}
Loading

0 comments on commit ba8b04e

Please sign in to comment.