Skip to content
This repository has been archived by the owner on Jul 16, 2024. It is now read-only.

Tzalex/45 create executorservicehandler #48

Merged
merged 45 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
acb8bd8
Create blueprint for executorServiceHandler (WIP)
Tzal3x Oct 19, 2023
fe59569
Implement basic flow
Tzal3x Oct 19, 2023
00fe331
Merge worker to mainPool if txb fails
Tzal3x Oct 19, 2023
9d0aa61
Test multiple transactions (SYNC)
Tzal3x Oct 20, 2023
3aac8b6
Checkpoint (WIP)
Tzal3x Oct 20, 2023
36cac25
Implement async txb execution
Tzal3x Oct 20, 2023
7ca5d76
Add one more console log
Tzal3x Oct 20, 2023
4ce2f03
Fix lint error for non-null assertion
Tzal3x Oct 20, 2023
44585ce
Create SetupTestsHelper.setupAdmin
Tzal3x Oct 23, 2023
b9bd17f
Refactor for cleaner code & create nerge_all_coins.sh
Tzal3x Oct 23, 2023
30600fe
Increase MINIMUM_COIN_BALANCE
Tzal3x Oct 24, 2023
69a9f56
Refactor and parse ADMIN_ADDRESS from environment
Tzal3x Oct 24, 2023
a77b69d
Parse ids from json
Tzal3x Oct 24, 2023
83b6dbe
Execute setupAdmin beforeEach servicehandler test
Tzal3x Oct 24, 2023
b9c083b
Tailor pulbish.sh to our needs
Tzal3x Oct 24, 2023
e63088f
Generate only one .env file - at test/
Tzal3x Oct 24, 2023
91ff2c0
Refactor: Prettify helpers
Tzal3x Oct 24, 2023
47654db
Create initial_setup.sh
Tzal3x Oct 24, 2023
9a44aeb
Transfer some sui to the test user when creating him
Tzal3x Oct 24, 2023
3155ef0
Add setup step: reassure that admin has at least 2 coins
Tzal3x Oct 24, 2023
8ce069c
Suppress output of merge_all_coins
Tzal3x Oct 24, 2023
9c259fa
Add prolog message to initial_setup.sh
Tzal3x Oct 24, 2023
8bc0cb1
Move test user creation inside if block
Tzal3x Oct 24, 2023
b949f14
Assure that admin has a specific number of objects owned
Tzal3x Oct 24, 2023
4159b82
Checkpoint
Tzal3x Oct 24, 2023
d4f5441
Add docstring for merge_all_coins
Tzal3x Oct 24, 2023
6357386
Increase new coin balance from split
Tzal3x Oct 24, 2023
848db24
Update docs
Tzal3x Oct 24, 2023
be736b5
Set gasPayment to avoid gas smashing while re-transfering object to s…
Tzal3x Oct 24, 2023
bf9ba18
Fix test fail due to few coins
Tzal3x Oct 24, 2023
89cfa7f
Remove console logs for successful split coin
Tzal3x Oct 25, 2023
9b7fb31
Make tests pass by serializing them
Tzal3x Oct 25, 2023
6b9904f
Format code with "npm run-script format"
Tzal3x Oct 25, 2023
adef833
Refactor - create sleep function
Tzal3x Oct 25, 2023
86ee35d
Remove transfer NFT back to admin
Tzal3x Oct 25, 2023
8e23906
Fix lint errors
Tzal3x Oct 25, 2023
4e3c799
Fix lint warnings
Tzal3x Oct 25, 2023
6788697
Merge pull request #52 from MystenLabs/50-refactor-tests-so-they-can-…
teohaik Oct 25, 2023
ed3c306
Clarify log messages
Tzal3x Oct 25, 2023
0e6dd33
Add GET_WORKER_TIMEOUT_MS to .env
Tzal3x Oct 25, 2023
94d2e83
Remove remnant code from old test
Tzal3x Oct 25, 2023
ae923b5
Update node.js.yml
Tzal3x Oct 25, 2023
d414117
Update node.js.yml
Tzal3x Oct 25, 2023
ca0e264
Merge branch 'main' into tzalex/45-create-executorservicehandler
Tzal3x Oct 25, 2023
20d51c0
Increase tests timeout
Tzal3x Oct 25, 2023
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: 0 additions & 1 deletion .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
NFT_APP_ADMIN_CAP: ${{ vars.NFT_APP_ADMIN_CAP }}
SUI_NODE: ${{ vars.SUI_NODE }}
GET_WORKER_TIMEOUT_MS: ${{ vars.GET_WORKER_TIMEOUT_MS }}
run: echo "SUI_NODE=$SUI_NODE | NFT_APP_PACKAGE_ID=$NFT_APP_PACKAGE_ID | NFT_APP_ADMIN_CAP=$NFT_APP_ADMIN_CAP | TEST_USER_ADDRESS=$TEST_USER_ADDRESS | TEST_NFT_OBJECT_ID=$TEST_NFT_OBJECT_ID | TEST_NON_EXISTING_OBJECT_ID=$TEST_NON_EXISTING_OBJECT_ID | TEST_NOT_OWNED_BY_ADMIN_OBJECT_ID=$TEST_NOT_OWNED_BY_ADMIN_OBJECT_ID "
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
Expand Down
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
This is an initial design of a coin management system. The outcome should be a library with a good docs that will allow any builder to have an address issuing many concurrent transactions without locking its coins.

## Develop

Install dependencies with `npm install`

### Test
Usually we use testnet for testing. Switch to testnet with: `sui client switch --env testnet`

First and foremost, you need to define a `.env`. The file template is in [.env.example](https://github.com/MystenLabs/coin_management_system/blob/main/test/.env.example). The variables defined there are the following
To setup the tests environment use `./test/initial_setup.sh`

The script will create a .env file in the test folder.
When the script is complete you only need to add a `ADMIN_SECRET_KEY` and a `TEST_USER_SECRET` to the `.env`.

Usually we use the testnet network for testing. Switch to testnet with: `sui client switch --env testnet`

At the end of the setup your .env should look like the template [.env.example](https://github.com/MystenLabs/coin_management_system/blob/main/test/.env.example).
i.e.

```[.env]
# The Admin should also be the publisher of the nft_app smart contract
Expand All @@ -18,24 +26,17 @@ ADMIN_SECRET_KEY=
TEST_USER_ADDRESS=
TEST_USER_SECRET=

# This object id points to a test nft object owned by the admin account. Used for testing.
TEST_NFT_OBJECT_ID=
# This object id is arbitrary and should not exist. Used for testing.
TEST_NON_EXISTING_OBJECT_ID=
# This object id should exist in the network but not be owned by the admin account. Used for testing.
TEST_NOT_OWNED_BY_ADMIN_OBJECT_ID=

# Used for testing. Get this by publishing the move_examples/nft_app/
NFT_APP_PACKAGE_ID=
NFT_APP_ADMIN_CAP=

# Example: "https://fullnode.testnet.sui.io:443"
SUI_NODE=

# NOTE: The Admin should also be the publisher of the nft_app smart contract
GET_WORKER_TIMEOUT_MS=1000
```

Tip: You can see your addresses' secret keys by running `cat ~/.sui/sui_config/sui.keystore`
_Tip: You can see your addresses' secret keys by running `cat ~/.sui/sui_config/sui.keystore`_

We use the [jest](https://jestjs.io/) framework for testing. Having installed the project's packages with `npm install`, you can run the tests by either:

Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/test/**/*.test.ts'],
testTimeout: 30000, // 10000 ms = 10 seconds
};
57 changes: 21 additions & 36 deletions move_examples/setup/publish.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
#!/bin/bash

# check dependencies are available.
for i in jq sui; do
if ! command -V ${i} 2>/dev/null; then
echo "${i} is not installed"
exit 1
fi
done

# default network is localnet
NETWORK=http://localhost:9000
FAUCET=https://localhost:9000/gas

# If otherwise specified chose testnet or devnet
if [ $# -ne 0 ]; then
Expand All @@ -19,14 +10,27 @@ if [ $# -ne 0 ]; then
fi
if [ $1 = "testnet" ]; then
NETWORK="https://fullnode.testnet.sui.io:443"
FAUCET="https://faucet.testnet.sui.io/gas"
fi
if [ $1 = "devnet" ]; then
NETWORK="https://fullnode.devnet.sui.io:443"
FAUCET="https://faucet.devnet.sui.io/gas"
fi
fi

read -p "You are about to publish a new package at $NETWORK Continue? (y/n): " response
response=$(echo "$response" | tr '[:upper:]' '[:lower:]') # tolower
if ! [[ "$response" =~ ^(yes|y)$ ]]; then
echo "Exiting package publishing."
exit 1
fi

# check dependencies are available.
for i in jq sui; do
if ! command -V ${i} 2>/dev/null; then
echo "${i} is not installed"
exit 1
fi
done

publish_res=$(sui client publish --gas-budget 200000000 --json ../nft_app --skip-dependency-verification)

echo ${publish_res} >.publish.res.json
Expand All @@ -37,35 +41,16 @@ if [[ "$publish_res" =~ "error" ]]; then
exit 1
fi
echo "Contract Deployment finished!"

echo "Setting up environmental variables..."

DIGEST=$(echo "${publish_res}" | jq -r '.digest')
PACKAGE_ID=$(echo "${publish_res}" | jq -r '.effects.created[] | select(.owner == "Immutable").reference.objectId')
newObjs=$(echo "$publish_res" | jq -r '.objectChanges[] | select(.type == "created")')
ADMIN_CAP_ID=$(echo "$newObjs" | jq -r 'select (.objectType | contains("::genesis::AdminCap")).objectId')
PUBLISHER_ID=$(echo "$newObjs" | jq -r 'select (.objectType | contains("::Publisher")).objectId')
UPGRADE_CAP_ID=$(echo "$newObjs" | jq -r 'select (.objectType | contains("::package::UpgradeCap")).objectId')

ADMIN_ADDRESS=$(echo "$publish_res" | jq -r '.transaction.data.sender')
PACKAGE_ID=$(echo "${publish_res}" | jq -r '.effects.created[] | select(.owner == "Immutable").reference.objectId')

cat >.env <<-API_ENV
SUI_NETWORK=$NETWORK
DIGEST=$DIGEST
UPGRADE_CAP_ID=$UPGRADE_CAP_ID
PACKAGE_ID=$PACKAGE_ID
ADMIN_CAP_ID=$ADMIN_CAP_ID
PUBLISHER_ID=$PUBLISHER_ID
ADMIN_PHRASE=$ADMIN_PHRASE
cat >../../test/.env <<-API_ENV
NFT_APP_PACKAGE_ID=$PACKAGE_ID
NFT_APP_ADMIN_CAP=$ADMIN_CAP_ID
SUI_NODE=$NETWORK
ADMIN_ADDRESS=$ADMIN_ADDRESS
NON_CUSTODIAN_ADDRESS=$NON_CUSTODIAN_ADDRESS
API_ENV

# echo "Waiting for Fullnode to sync..."
# sleep 5

# echo "Installing dependencies..."

# npm install

# echo "Setting up Display..."
# npm start
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"start": "node src/index.ts",
"format": "prettier --write '**/*.{ts,tsx,js,jsx,json,css,md}'",
"lint": "eslint --ext .ts --fix --max-warnings 0 src",
"test": "jest test/**/*.test.ts"
"test": "jest test/**/*.test.ts --runInBand"
},
"keywords": [
"coin",
Expand Down
138 changes: 138 additions & 0 deletions src/executorServiceHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { CoinStruct, SuiClient } from '@mysten/sui.js/client';
import { Keypair } from '@mysten/sui.js/src/cryptography';
import { SuiObjectRef } from '@mysten/sui.js/src/types/objects';
import { TransactionBlock } from '@mysten/sui.js/transactions';

import { Pool } from './pool';

type WorkerPool = {
status: 'available' | 'busy';
pool: Pool;
};

// SplitStrategy defines the predicates used to split the pool's objects and coins
// to get new worker pools.
type SplitStrategy = {
objPred: (obj: SuiObjectRef | undefined) => boolean | null;
coinPred: (coin: CoinStruct | undefined) => boolean | null;
};

export class ExecutorServiceHandler {
private _mainPool: Pool;
private _workers: WorkerPool[] = [];
private constructor(mainPool: Pool) {
this._mainPool = mainPool;
}

public static initialize(keypair: Keypair, client: SuiClient) {
return Pool.full({ keypair: keypair, client }).then((pool) => {
return new ExecutorServiceHandler(pool);
});
}

public async execute(
txb: TransactionBlock,
client: SuiClient,
splitStrategy: SplitStrategy,
teohaik marked this conversation as resolved.
Show resolved Hide resolved
retries = 3,
) {
let res;
do {
res = await this.executeFlow(txb, client, splitStrategy);
if (res) {
return res;
} else {
console.log(
`Failed to execute the txb - [remaining retries: ${retries}]`,
);
}
} while (retries-- > 0);
throw new Error('Internal server error - could not execute the transaction block');
}

private async executeFlow(
txb: TransactionBlock,
client: SuiClient,
splitStrategy: SplitStrategy,
) {
const worker: WorkerPool | undefined = this.getAWorker();
const noWorkerAvailable = worker === undefined;
if (noWorkerAvailable) {
this.addWorker(splitStrategy);
return;
} else {
// An available worker is found! Assign to it the task of executing the txb.
worker.status = 'busy'; // Worker is now busy

const result = await worker.pool.signAndExecuteTransactionBlock({
transactionBlock: txb,
client: client,
});

if (result.effects && result.effects.status.status === 'failure') {
this.removeWorker(worker);
return;
}

console.log('Transaction block execution completed!');
worker.status = 'available'; // Execution finished, the worker is now available again.
return result;
teohaik marked this conversation as resolved.
Show resolved Hide resolved
}
}

/*
Get an available worker from the workers array.
If an available worker is not found in the time span of TIMEOUT_MS, return undefined.
*/
private getAWorker(): WorkerPool | undefined {
if (!process.env.GET_WORKER_TIMEOUT_MS) {
throw new Error("Environment variable 'TIMEOUT_MS' not set.");
}
const timeoutMs = parseInt(process.env.GET_WORKER_TIMEOUT_MS);
const startTime = new Date().getTime();
while (new Date().getTime() - startTime < timeoutMs) {
const result = this._workers.find(
(worker: WorkerPool) => worker.status === 'available',
);
if (result) {
console.log('Available worker found!');
return result;
}
}
if (new Date().getTime() - startTime >= timeoutMs) {
const numBusyWorkers = this._workers.filter(
(worker: WorkerPool) => worker.status === 'busy',
).length;
console.log(
`Timeout reached - no available worker found - ${numBusyWorkers} busy workers`,
);
}
}

/*
Add a worker to the workers array.
The worker is created by splitting the main pool and the new pool
that is produced is added to the workers array.
*/
private addWorker(splitStrategy: SplitStrategy) {
const newPool = this._mainPool.split(
splitStrategy.objPred,
splitStrategy.coinPred,
);
this._workers.push({ status: 'available', pool: newPool });
}

/*
Remove the worker from the workers array and merge
it back to the main pool.
*/
private removeWorker(worker: WorkerPool) {
const index = this._workers.indexOf(worker);
if (index > -1) {
this._workers.splice(index, 1);
this._mainPool.merge(worker.pool);
} else {
throw new Error('Worker not found in workers array.');
}
}
}
Loading