Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add deferred call manager #323

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deferred-call-manager/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PRIVATE_KEY=
6 changes: 6 additions & 0 deletions deferred-call-manager/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
extends: ['@massalabs'],
rules: {
'no-console': 'off',
},
};
18 changes: 18 additions & 0 deletions deferred-call-manager/.github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: massa sc ci tests
on: [push]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'

- name: Install
run: npm ci

- name: Test
run: npm run test
3 changes: 3 additions & 0 deletions deferred-call-manager/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
build
.env
42 changes: 42 additions & 0 deletions deferred-call-manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Deferred Call Manager Contract

This contract manages deferred function calls, allowing functions to be scheduled for execution at a later time. It provides mechanisms to add, execute, and remove recursive task using deferred calls. The contract schedule next task execution and store the execution infos like slot and callId. Every task schedule its next execution.

## Key Functionalities

- Start a recursive task to execute every given periods.
- Executing the next deferred call and schedule the next one.
- Stop the task recursion by canceling the next planned call.

This contract is useful for scenarios where a smart contract needs to be recursively call at fixed time interval.
The execution will last until stopped or contract coins are insufficient to next execution storage or deferred call slot booking fee.

## Setup

```shell
npm i
```

Create `.env` and set `PRIVATE_KEY`:

```shell
cp .env.example .env
```

## Deploy

```shell
npm run deploy
```

## Listen to Events

```shell
npm run listen
```

## Get Execution History

```shell
npm run history
```
24 changes: 24 additions & 0 deletions deferred-call-manager/as-pect.asconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"targets": {
"coverage": {
"lib": ["./node_modules/@as-covers/assembly/index.ts"],
"transform": ["@as-covers/transform", "@as-pect/transform"]
},
"noCoverage": {
"transform": ["@as-pect/transform"]
}
},
"options": {
"exportMemory": true,
"outFile": "output.wasm",
"textFile": "output.wat",
"bindings": "raw",
"exportStart": "_start",
"exportRuntime": true,
"use": ["RTRACE=1"],
"debug": true,
"exportTable": true
},
"extends": "./asconfig.json",
"entries": ["./node_modules/@as-pect/assembly/assembly/index.ts"]
}
28 changes: 28 additions & 0 deletions deferred-call-manager/as-pect.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import createMockedABI from '@massalabs/massa-as-sdk/vm-mock';

export default {
/**
* A set of globs passed to the glob package that qualify typescript files for testing.
*/
entries: ['assembly/__tests__/**/*.spec.ts'],
/**
* A set of globs passed to the glob package that quality files to be added to each test.
*/
include: ['assembly/__tests__/**/*.include.ts'],
/**
* A set of regexp that will disclude source files from testing.
*/
disclude: [/node_modules/],
/**
* Add your required AssemblyScript imports here.
*/
async instantiate(memory, createImports, instantiate, binary) {
return createMockedABI(memory, createImports, instantiate, binary);
},
/** Enable code coverage. */
// coverage: ["assembly/**/*.ts"],
/**
* Specify if the binary wasm file should be written to the file system.
*/
outputBinary: false,
};
13 changes: 13 additions & 0 deletions deferred-call-manager/asconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"targets": {
"release": {
"sourceMap": true,
"optimizeLevel": 3,
"shrinkLevel": 3,
"converge": true,
"noAssert": false,
"exportRuntime": true,
"bindings": false
}
}
}
1 change: 1 addition & 0 deletions deferred-call-manager/assembly/__tests__/as-pect.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/// <reference types="@as-pect/assembly/types/as-pect" />
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
describe('TODO', () => {});
30 changes: 30 additions & 0 deletions deferred-call-manager/assembly/contracts/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Context, Storage } from '@massalabs/massa-as-sdk';
import { Args, stringToBytes, u64ToBytes } from '@massalabs/as-types';
import {
cancelCall,
NEXT_CALL_ID_KEY,
registerCall,
TASK_COUNT_KEY,
} from '../internals';

// Export task function
export { processTask } from '../internals';

export function constructor(binArgs: StaticArray<u8>): void {
assert(Context.isDeployingContract());

const period = new Args(binArgs).nextU64().expect('Unable to decode period');

Storage.set(TASK_COUNT_KEY, u64ToBytes(0));
registerCall(period);
}

export function getNextCallId(_: StaticArray<u8>): StaticArray<u8> {
assert(Storage.has(NEXT_CALL_ID_KEY), 'No deferred call planned');
return stringToBytes(Storage.get(NEXT_CALL_ID_KEY));
}

export function stop(_: StaticArray<u8>): void {
assert(Storage.has(NEXT_CALL_ID_KEY), 'No deferred call to stop');
cancelCall(Storage.get(NEXT_CALL_ID_KEY));
}
97 changes: 97 additions & 0 deletions deferred-call-manager/assembly/internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {
Args,
bytesToU64,
stringToBytes,
u64ToBytes,
} from '@massalabs/as-types';
import {
balance,
Context,
deferredCallCancel,
deferredCallExists,
deferredCallQuote,
deferredCallRegister,
findCheapestSlot,
generateEvent,
Storage,
} from '@massalabs/massa-as-sdk';
import { History } from './serializable/history';

export const NEXT_CALL_ID_KEY = 'callId';
export const HISTORY_KEY = stringToBytes('hist');
export const TASK_COUNT_KEY = stringToBytes('idx');

export function registerCall(period: u64): void {
const initBal = balance();
generateEvent('Current contract balance: ' + initBal.toString());

const maxGas = 20_000_000;
const params_size = 0;
const bookingPeriod = Context.currentPeriod() + period;
const slot = findCheapestSlot(bookingPeriod, bookingPeriod, maxGas, params_size);

const cost = deferredCallQuote(slot, maxGas, params_size);
const callId = deferredCallRegister(
Context.callee().toString(),
'processTask',
slot,
maxGas,
new Args().add(period).serialize(),
// No need to provide coins as processTask is internal function
0,
);

const bookingCost = initBal - balance();

Storage.set(NEXT_CALL_ID_KEY, callId);
generateEvent(
`Deferred call registered. id: ${callId}. Booked slot period: ${bookingPeriod.toString()}.\
Booking cost: ${bookingCost.toString()}, quote: ${cost.toString()}`,
);
}

function getTaskIndex(): u64 {
return bytesToU64(Storage.get(TASK_COUNT_KEY));
}

function getHistoryKey(taskIndex: u64): StaticArray<u8> {
return HISTORY_KEY.concat(u64ToBytes(taskIndex));
}

export function processTask(binArgs: StaticArray<u8>): void {
assert(
Context.callee() === Context.caller(),
'The caller must be the contract itself',
);

const taskIndex = getTaskIndex();
const callId = Storage.get(NEXT_CALL_ID_KEY);

generateEvent(`Processing task ${taskIndex}. Call id : ${callId}`);

// Save execution history
const key = getHistoryKey(taskIndex);
Storage.set(
key,
new History(
Context.currentPeriod(),
Context.currentThread(),
callId,
).serialize(),
);

// Increment task index
Storage.set(TASK_COUNT_KEY, u64ToBytes(taskIndex + 1));

const period = new Args(binArgs).nextU64().expect('Unable to decode period');
registerCall(period);
}

export function cancelCall(callId: string): void {
if (deferredCallExists(callId)) {
deferredCallCancel(callId);
generateEvent('Deferred call canceled. id : ' + callId);
} else {
generateEvent('Deferred call does not exist. id: ' + callId);
}
}
27 changes: 27 additions & 0 deletions deferred-call-manager/assembly/serializable/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Args, Result, Serializable } from '@massalabs/as-types';

export class History implements Serializable {
constructor(
public period: u64 = 0,
public thread: u8 = 0,
public callId: string = '',
) {}

serialize(): StaticArray<u8> {
return new Args()
.add(this.period)
.add(this.thread)
.add(this.callId)
.serialize();
}

deserialize(data: StaticArray<u8>, offset: i32): Result<i32> {
const args = new Args(data, offset);

this.period = args.nextU64().expect("Can't deserialize period.");
this.thread = args.nextU8().expect("Can't deserialize thread.");
this.callId = args.nextString().expect("Can't deserialize callId.");

return new Result(args.offset);
}
}
14 changes: 14 additions & 0 deletions deferred-call-manager/assembly/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"**/*.ts"
],
"typedocOptions": {
"exclude": "assembly/**/__tests__",
"excludePrivate": true,
"excludeProtected": true,
"excludeExternals": true,
"includeVersion": true,
"skipErrorChecking": true
}
}
Loading