Compare commits

..

2 Commits

Author SHA1 Message Date
7fdda647bf feat: implement generator 2025-04-03 19:21:48 +03:00
c95c4e5e3f chore: change config 2025-04-03 19:21:19 +03:00
116 changed files with 1026 additions and 3137 deletions

View File

@ -14,11 +14,9 @@ includes:
blake3: 321b1f88930aead7fe47961c8664005b3441f6502824769bf21eb06c3e5aaba4 blake3: 321b1f88930aead7fe47961c8664005b3441f6502824769bf21eb06c3e5aaba4
use: use:
- jump-listener.cm v0.1.8 - dex-app.cm v0.0.67+test
- jump_listener_devnet v0.1.8 - eth_offer_maker 0.0.67+devnet
- jump-forwarder.cm v0.1.5 - jump-listener.cm v0.0.5
- jump_forwarder_devnet v0.1.5
interpreters: {} interpreters: {}
@ -33,4 +31,3 @@ contract_instances:
template: contract.cm v0.0.1 template: contract.cm v0.0.1
parameters: parameters:
content: [] content: []

View File

@ -4,4 +4,4 @@ NPM_PASSWORD=
# This is needed when working with WASM modules (nodeLinker: node-modules is mandatory) # This is needed when working with WASM modules (nodeLinker: node-modules is mandatory)
NODE_OPTIONS="--experimental-wasm-modules --no-warnings=ExperimentalWarning --max-old-space-size=8192" NODE_OPTIONS="--experimental-wasm-modules --no-warnings=ExperimentalWarning --max-old-space-size=8192"
REGISTRATION_PROFILE=test REGISTRATION_PROFILE=devnet

View File

@ -21,6 +21,7 @@ export default tseslint.config(
'**/dapp-ui/src/pages/Bot/**', '**/dapp-ui/src/pages/Bot/**',
'**/dapp-ui/src/pages/BotHedging/**', '**/dapp-ui/src/pages/BotHedging/**',
'**/dapp-ui/src/pages/SwapTool/**', '**/dapp-ui/src/pages/SwapTool/**',
'**/market-maker.cm/perf_test/onchain.cjs',
], ],
}, },
// rest of the config by module // rest of the config by module

View File

@ -8,12 +8,11 @@
"scripts": { "scripts": {
"build": "yarn build:cm && yarn build:non-cm", "build": "yarn build:cm && yarn build:non-cm",
"build:production": "yarn build:cm && yarn workspace dapp-ui build:production", "build:production": "yarn build:cm && yarn workspace dapp-ui build:production",
"build:lib": "yarn workspaces foreach -Ap --include 'packages/lib' --include 'packages/cwait' run build", "build:cm": "yarn workspaces foreach -Ap --include 'packages/lib' --include 'packages/cwait' --include 'packages/*.cm' run build",
"build:cm": "yarn build:lib && yarn workspaces foreach -Ap --include 'packages/*.cm' run build",
"build:non-cm": "yarn workspaces foreach -Ap --include 'packages/*' --exclude 'packages/*.cm' --exclude 'packages/lib' --exclude 'packages/cwait' run build", "build:non-cm": "yarn workspaces foreach -Ap --include 'packages/*' --exclude 'packages/*.cm' --exclude 'packages/lib' --exclude 'packages/cwait' run build",
"create-index": "cweb-tool create-index -c ./.cweb-config/config.yaml --profile $REGISTRATION_PROFILE", "create-index": "cweb-tool create-index -c ./.cweb-config/config.yaml --profile $REGISTRATION_PROFILE",
"publish-index": "cweb-tool publish-index -c ./.cweb-config/config.yaml --profile $REGISTRATION_PROFILE", "publish-index": "cweb-tool publish-index -c ./.cweb-config/config.yaml --profile $REGISTRATION_PROFILE",
"deploy-contracts": "yarn build:cm && yarn publish-actions", "deploy-contracts": "yarn publish-actions",
"call-contracts": "yarn call-contracts:prepare && yarn call-contracts:invoke", "call-contracts": "yarn call-contracts:prepare && yarn call-contracts:invoke",
"call-contracts:prepare": "yarn workspaces foreach --all run prepare-for-package && yarn node ./.cweb-config/create-calls.mjs", "call-contracts:prepare": "yarn workspaces foreach --all run prepare-for-package && yarn node ./.cweb-config/create-calls.mjs",
"call-contracts:invoke": "cweb-tool call .cweb-config/calls.yaml -c ./.cweb-config/config.yaml --profile $REGISTRATION_PROFILE", "call-contracts:invoke": "cweb-tool call .cweb-config/calls.yaml -c ./.cweb-config/config.yaml --profile $REGISTRATION_PROFILE",
@ -47,7 +46,7 @@
"@coinweb/self-register": "0.1.3", "@coinweb/self-register": "0.1.3",
"@coinweb/claims-client": "0.1.6-debug", "@coinweb/claims-client": "0.1.6-debug",
"@coinweb/contract-kit": "0.2.6", "@coinweb/contract-kit": "0.2.6",
"@coinweb/testing-sdk": "0.0.10-mutex", "@coinweb/testing-sdk": "0.0.9-remote",
"@coinweb/minimal-sdk": "1.2.21" "@coinweb/minimal-sdk": "1.2.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -9,5 +9,5 @@ export enum PUBLIC_METHODS {
} }
export const FEE = { export const FEE = {
ADD_WORD: 10000000n, ADD_WORD: 10000n,
}; };

View File

@ -9,7 +9,6 @@ export const constructAddWordUiCommand = ({ word, contractId }: { word: string;
methodName: PUBLIC_METHODS.ADD_WORD, methodName: PUBLIC_METHODS.ADD_WORD,
methodArgs: [[word] satisfies AddWordArgs], methodArgs: [[word] satisfies AddWordArgs],
cost: FEE.ADD_WORD, cost: FEE.ADD_WORD,
withQueue: false,
}), }),
]); ]);
}; };

View File

@ -1,94 +0,0 @@
import path from 'node:path';
import { constructContractIssuer, constructContractRef, type PreparedCallInfo } from '@coinweb/contract-kit';
import { UnitTest, type ExecInfo, type RoundInfo, type UnitTestContext, type DbWriteOp } from '@coinweb/testing-sdk';
import { describe, it, expect, beforeAll } from 'vitest';
import { AddWordArgs, FEE, PUBLIC_METHODS } from '../../offchain/shared';
import { CONTRACT_INSTANCE_ID, TEST_PRIVATE_KEY, TEST_PUBLIC_KEY, waitRoundsCount } from './helpers';
const addWordRound = (creatorAccount: Buffer): RoundInfo => {
const self = constructContractIssuer(CONTRACT_INSTANCE_ID);
const kvDeleteCallInfo: PreparedCallInfo = {
ref: constructContractRef(self, []),
methodInfo: { methodName: PUBLIC_METHODS.ADD_WORD, methodArgs: ['TEST_HELLO'] satisfies AddWordArgs },
contractArgs: [],
contractInfo: { providedCweb: FEE.ADD_WORD, authenticated: null }, // null = authenticated account in tests
};
const withFunds = { type: { privateKey: creatorAccount } };
return {
txsInfo: {
// @ts-ignore
txs: [{ callInfo: kvDeleteCallInfo, withFunds }],
l1_events: [],
},
claims: [],
blocks_on: [],
};
};
const runTest = async (context: UnitTestContext, input: ExecInfo, checkFn: (results: DbWriteOp[]) => void) => {
const test = new UnitTest(context);
await test.load(path.join(import.meta.dirname, '../../../tests_data/state.json'));
await test.run(input, { checkFn });
};
describe('PUBLIC_METHODS.ADD_WORD Tests', () => {
let creatorAccount: { publicKey: Buffer; privateKey: Buffer };
beforeAll(() => {
creatorAccount = {
privateKey: Buffer.from(TEST_PRIVATE_KEY, 'hex'),
publicKey: Buffer.from(TEST_PUBLIC_KEY, 'hex'),
};
});
it('Should successfully execute', async () => {
const input: ExecInfo = {
rounds: [...waitRoundsCount(50), addWordRound(creatorAccount.privateKey), ...waitRoundsCount(2)],
};
const checkFn = (results: DbWriteOp[]) => {
expect(results).toBeDefined();
// @ts-ignore
// const errors = findAllValuesRecursivelyByKey<{ ContractError: ContractError }>(results, 'Invalid');
// expect(errors.length).toBe(0);
// @ts-ignore
// const storeOps = findAllValuesRecursivelyByKey<CwebStore>(results, 'StoreOp');
// expect(storeOps.length).toBeGreaterThan(0);
// const existingPayload = storeOps.find((s) => isEqual(s.key.first_part, createFirstPartDefault()));
// expect(existingPayload).toBeDefined();
// expect(existingPayload?.key).toStrictEqual(createDefaultKey(Number.MAX_SAFE_INTEGER));
// expect(existingPayload?.key.first_part).toStrictEqual(createFirstPartDefault());
// const { id, data, owner, tx } = existingPayload?.body as StoreKvPayload;
// expect(id).toBeDefined();
// expect(data).toBeDefined();
// expect(owner).toBeDefined();
// expect(tx).toBeDefined();
// expect(tx?.timestamp).toBeDefined();
// expect(tx?.coinwebTxId).toBeDefined();
// expect(Number(id)).toStrictEqual(Number.MAX_SAFE_INTEGER);
// expect(owner).toStrictEqual({
// auth: 'EcdsaContract',
// payload: TEST_PUBLIC_KEY,
// });
};
const context: UnitTestContext = {
name: 'add_word',
testPath: path.join(import.meta.dirname, './tests_data/add_word'),
verbose: true,
};
await runTest(context, input, checkFn);
});
});

View File

@ -1,35 +0,0 @@
import path from 'node:path';
import { getInstanceFromIndex, type RoundInfo } from '@coinweb/testing-sdk';
const onchainPackage = (await import('../../../dist/out/package.json')) as {
name: string;
};
export const CONTRACT_INSTANCE = await getInstanceFromIndex({
path: path.resolve(import.meta.dirname, '../../../tests_data/index.yaml'),
instance_alias: 'cwait-contract 0.0.1-test',
});
export const CONTRACT_INSTANCE_ID = '0x'.concat(CONTRACT_INSTANCE.instance_id.replace(/0x/, ''));
export const CONTRACT_TEMPLATE_ID = `0x${onchainPackage.name.substring(5)}`;
export const TEST_PRIVATE_KEY = '31c70848e4e3aaffcf91f134853ec966e913aa9a813115bcb81512e7625f46a9';
export const TEST_PUBLIC_KEY = '03951f89fe78e13f295d96eb7afa1e0da726df7d58f9c84f7144e5febc30efeec4';
export const waitRoundsCount = (roundsCount: number): RoundInfo[] => {
const emptyRound: RoundInfo = {
txsInfo: {
txs: [],
l1_events: [],
},
claims: [],
blocks_on: [],
};
const rounds: RoundInfo[] = [];
for (let i = 0; i < roundsCount; i++) {
rounds.push(emptyRound);
}
return rounds;
};

View File

@ -1,11 +1,8 @@
import { constructClaim } from '@coinweb/contract-kit'; import { constructClaim } from '@coinweb/contract-kit';
import { readOp, storeOp, cwait } from 'cwait'; import { readOp, storeOp, TypedClaim } from 'cwait';
import { TypedClaim } from '../../../lib/dist/shared/types';
import { AddWordArgs, createWordKey, WordClaimBody } from '../offchain/shared'; import { AddWordArgs, createWordKey, WordClaimBody } from '../offchain/shared';
import { extraLogic } from './extraLogic';
function hashCode(str: string): string { function hashCode(str: string): string {
let hash = 0; let hash = 0;
for (let i = 0, len = str.length; i < len; i++) { for (let i = 0, len = str.length; i < len; i++) {
@ -15,40 +12,16 @@ function hashCode(str: string): string {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
hash |= 0; hash |= 0;
} }
return `${(hash < 0 ? hash * -1 : hash).toString(16)}`; return `${hash.toString(16)}`;
} }
export const addWord = cwait(async (...[word]: AddWordArgs) => { export function* addWord(...[word]: AddWordArgs) {
console.log('addWord START');
const id = hashCode(word); const id = hashCode(word);
yield storeOp(constructClaim(createWordKey(id), { word }, '0x0'));
console.log('await storeOp'); const wordClaim: TypedClaim<WordClaimBody> | null = yield readOp(createWordKey(id));
await storeOp(constructClaim(createWordKey(id), { word }, '0x0'));
console.log('await extraLogic'); const newWord = (wordClaim?.body.word ?? '') + '!!!';
const [wordClaim] = await Promise.all([extraLogic(id, 1), extraLogic(id, 2)]); const newId = hashCode(newWord);
yield storeOp(constructClaim(createWordKey(newId), { word: newWord }, '0x0'));
const newWord1 = (wordClaim?.body.word ?? '') + '_1'; }
const newId1 = hashCode(newWord1);
const newWord2 = (wordClaim?.body.word ?? '') + '_2';
const newId2 = hashCode(newWord2);
const newWord3 = (wordClaim?.body.word ?? '') + '_3';
const newId3 = hashCode(newWord3);
console.log('free storeOp');
storeOp(constructClaim(createWordKey(newId1), { word: wordClaim?.body.word + '_free' }, '0x0'));
console.log('await Promise.all');
await Promise.all([
storeOp(constructClaim(createWordKey(newId2), { word: wordClaim?.body.word + '_in_promise_all' }, '0x0')),
storeOp(constructClaim(createWordKey(newId3), { word: 'WIN' }, '0x0')),
]);
console.log('free readOp');
readOp<TypedClaim<WordClaimBody>>(createWordKey(newId3));
console.log('addWord END');
});

View File

@ -1,9 +1,38 @@
import { constructCwebMain } from 'cwait'; import {
SELF_REGISTER_HANDLER_NAME,
ContractHandlers as CKContractHandlers,
MethodCallback,
selfCallWrapper,
addMethodHandler,
executeHandler,
ContractHandlers,
} from '@coinweb/contract-kit';
import { selfRegisterHandler } from '@coinweb/self-register';
import { queue } from 'lib/onchain';
import { executor } from '../../../cwait/dist/onchain';
import { PUBLIC_METHODS } from '../offchain/shared'; import { PUBLIC_METHODS } from '../offchain/shared';
import { addWord } from './addWord'; import { addWord } from './addWord';
export const cwebMain = constructCwebMain({ const addWrappers = (method: MethodCallback): MethodCallback => {
[PUBLIC_METHODS.ADD_WORD]: addWord, return selfCallWrapper(method);
}); };
const createModule = (): ContractHandlers => {
const module: ContractHandlers = { handlers: {} };
addMethodHandler(module, PUBLIC_METHODS.ADD_WORD, addWrappers(executor(addWord)));
addMethodHandler(module, SELF_REGISTER_HANDLER_NAME, selfRegisterHandler);
queue.applyQueue(module as CKContractHandlers, [PUBLIC_METHODS.ADD_WORD]);
return module;
};
export const cwebMain = () => {
const module = createModule();
executeHandler(module);
};

View File

@ -1,31 +0,0 @@
import { constructClaim } from '@coinweb/contract-kit';
import { cwait, lock, readOp, storeOp, TypedClaim } from 'cwait';
import { createWordKey, WordClaimBody } from '../offchain';
import { extraLogic2 } from './extraLogic2';
export const extraLogic = cwait(async (id: string, i: number) => {
console.log('extraLogic START ++ ', i);
console.log('extraLogic lock + ', i);
const unlock = await lock(createWordKey(id));
console.log('extraLogic readOp + ', i);
const result = await readOp<TypedClaim<WordClaimBody>>(createWordKey(id));
console.log('extraLogic storeOp + ', i);
await storeOp(
constructClaim(
createWordKey(id),
{ word: result?.body.word.split('').reverse().join('') + '_extraLogic + ' + i },
'0x0'
)
);
console.log('extraLogic unlock + ', i);
await unlock();
console.log('extraLogic return extraLogic2 + ', i);
return extraLogic2(id, i);
});

View File

@ -1,13 +0,0 @@
import { cwait, readOp, TypedClaim } from 'cwait';
import { createWordKey, WordClaimBody } from '../offchain';
export const extraLogic2 = cwait(async (id: string, i: number) => {
console.log('extraLogic2 START + ', i);
console.log('await extraLogic2 readOp + ', i);
const result = await readOp<TypedClaim<WordClaimBody>>(createWordKey(id));
console.log('extraLogic2 END + ', i);
return result;
});

View File

@ -1,14 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
reporters: 'default',
silent: false,
testTimeout: 240000,
teardownTimeout: 240000,
hookTimeout: 240000,
pool: 'forks',
},
});

View File

@ -3,8 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"clean": "rm -rf dist", "build": "yarn g:tsc -p tsconfig.build.json",
"build": "yarn clean && yarn g:tsc -p tsconfig.build.json",
"dev": "yarn g:tsc -p tsconfig.build.json --watch", "dev": "yarn g:tsc -p tsconfig.build.json --watch",
"lint": "yarn g:lint .", "lint": "yarn g:lint .",
"publish:lib": "node scripts/publish.js" "publish:lib": "node scripts/publish.js"

View File

@ -1,23 +0,0 @@
declare module '@coinweb/contract-kit/dist/esm/operations/store' {
export * from '@coinweb/contract-kit/dist/types/operations/store';
}
declare module '@coinweb/contract-kit/dist/esm/context' {
export * from '@coinweb/contract-kit/dist/types/context';
}
declare module '@coinweb/contract-kit/dist/esm/method' {
export * from '@coinweb/contract-kit/dist/types/method';
}
declare module '@coinweb/contract-kit/dist/esm/operations/block' {
export * from '@coinweb/contract-kit/dist/types/operations/block';
}
declare module '@coinweb/contract-kit/dist/esm/operations/call' {
export * from '@coinweb/contract-kit/dist/types/operations/call';
}
declare module '@coinweb/contract-kit/dist/esm/operations/read' {
export * from '@coinweb/contract-kit/dist/types/operations/read';
}

View File

@ -1,3 +0,0 @@
export * from './types';
export * from './methods';
export * from './wrappers';

View File

@ -1,71 +0,0 @@
import { extractContinuations, Context, MethodCallback as MethodCallbackOrig } from '@coinweb/contract-kit';
import {
getContextCall,
getContextGenesis,
getContextSystem,
getContextTx,
writeToResultFile,
} from '@coinweb/contract-kit/dist/esm/context';
import { getMethodName } from '@coinweb/contract-kit/dist/esm/method';
import { ContractHandlers, MethodCallback } from './types';
/**
* Adds a method handler for a specific method name.
* @param contract_module - Contract module containing the method.
* @param methodName - The name of the method.
* @param handler - The method callback to add.
*/
export function addMethodHandler(contract_module: ContractHandlers, methodName: string, handler: MethodCallback): void {
contract_module.handlers[methodName] = handler;
}
/**
* Retrieves the method handler for a specific method name.
* @param contract_module - Contract module containing the method.
* @param methodName - The name of the method.
* @returns The method callback for the specified method name.
* @throws Will throw an error if no handler is specified for the method name.
*/
export function getMethodHandler(contract_module: ContractHandlers, methodName: string): MethodCallback {
const handler = contract_module.handlers[methodName];
if (!handler) {
throw Error('Handler not specified for this method name');
}
return handler;
}
/**
* Executes the handler for the method specified in the transaction context.
* The results (new transactions) are written to the result file.
* @param contractModule - Contract module containing the method.
*/
export async function executeHandler(contractModule: ContractHandlers): Promise<void> {
const contextTx = getContextTx();
const contextCall = getContextCall();
const genesis = getContextGenesis();
const system = getContextSystem();
const context: Context = {
tx: contextTx,
call: contextCall,
genesis,
system,
continuations: extractContinuations(contextTx),
};
const method = getMethodName(context);
const handler = getMethodHandler(contractModule, method);
console.log('EXECUTE >>>', method);
try {
const txs = await handler(context);
writeToResultFile(txs);
} catch (error) {
if (error instanceof Error) {
console.log('ERROR >>>', error.message);
console.log('ERROR >>>', error.stack);
} else {
console.log('ERROR >>>', error);
}
throw error;
}
}

View File

@ -1,3 +0,0 @@
declare namespace os {
function readdir(path: string): [any[], number];
}

View File

@ -1,3 +0,0 @@
declare namespace std {
function open(path: string, mode: string): any;
}

View File

@ -0,0 +1,11 @@
declare module '@coinweb/contract-kit/dist/esm/operations/store' {
export * from '@coinweb/contract-kit/dist/types/operations/store';
}
declare module '@coinweb/contract-kit/dist/esm/context' {
export * from '@coinweb/contract-kit/dist/types/context';
}
declare module '@coinweb/contract-kit/dist/esm/method' {
export * from '@coinweb/contract-kit/dist/types/method';
}

View File

@ -1,7 +0,0 @@
import { Context, NewTx } from '@coinweb/contract-kit';
export type MethodCallback = (context: Context) => Promise<NewTx[]> | NewTx[];
export type ContractHandlers = {
handlers: { [key: string]: MethodCallback };
};

View File

@ -1,17 +0,0 @@
import { Context, isSelfCall } from '@coinweb/contract-kit';
import { MethodCallback } from './types';
/**
* Wraps a method callback to ensure that it can only be called by the contract itself.
* @param handler - The method callback to wrap.
* @returns A new method callback that throws an error if the call is not from the contract itself.
*/
export function selfCallWrapper(handler: MethodCallback): MethodCallback {
return async (context: Context) => {
if (!isSelfCall(context)) {
throw new Error('Only contract itself can call it');
}
return handler(context);
};
}

View File

@ -1,3 +1,2 @@
export * from './contract-kit';
export * from './onchain'; export * from './onchain';
export * from './types'; export * from './types';

View File

@ -1,34 +0,0 @@
import {
constructClaim,
constructClaimKey,
constructRangeRead,
constructStore,
constructTake,
CwebStore,
GRead,
GStore,
} from '@coinweb/contract-kit';
import { CwebRead } from '@coinweb/contract-kit/dist/esm/operations/read';
import { toHex } from 'lib/shared';
import { context } from '../context';
import { uuid } from '../utils';
export const fundsKey = '_funds';
export const constructFundsClaimKeyFirstPart = (processId: string) => [fundsKey, processId];
export const constructFundsClaimKey = (processId: string, claimId?: string) =>
constructClaimKey(constructFundsClaimKeyFirstPart(processId), [claimId ?? uuid()]);
export const constructFundsClaim = (processId: string, amount: bigint) =>
constructClaim(constructFundsClaimKey(processId), {}, toHex(amount));
export const constructFundsClaimStore = (processId: string, amount: bigint): GStore<CwebStore> =>
constructStore(constructFundsClaim(processId, amount));
export const constructFundsClaimRangRead = (processId: string): GRead<CwebRead> =>
constructRangeRead(context.issuer, constructFundsClaimKeyFirstPart(processId), {}, 10000);
export const constructFundsClaimTakeOp = (processId: string, id: string) =>
constructTake(constructFundsClaimKey(processId, id));

View File

@ -1,2 +0,0 @@
export * from './funds';
export * from './result';

View File

@ -1,37 +0,0 @@
import {
BlockFilter,
constructClaim,
constructClaimKey,
constructStore,
constructTake,
CwebStore,
GStore,
constructRead,
} from '@coinweb/contract-kit';
import { ResolvedOp } from '../../types';
import { context } from '../context';
export const resultKey = '_result';
export const constructResultClaimKey = (id: string) => constructClaimKey([resultKey], [id]);
export const constructResultClaim = (id: string, result: ResolvedOp[]) =>
constructClaim(constructResultClaimKey(id), result, '0x0');
export const constructResultClaimStore = (id: string, result: ResolvedOp[]): GStore<CwebStore> =>
constructStore(constructResultClaim(id, result));
export const constructResultClaimRead = (id: string) => constructRead(context.issuer, constructResultClaimKey(id));
export const constructResultClaimTake = (id: string) => constructTake(constructResultClaimKey(id));
export const constructResultBlockFilter = (id: string): BlockFilter => {
const { first_part: first, second_part: second } = constructResultClaimKey(id);
return {
issuer: context.issuer,
first,
second,
};
};

View File

@ -1,38 +1,9 @@
import { import { constructContractIssuer, Context, extractUser, getAuthenticated, getContractId } from '@coinweb/contract-kit';
Context,
ContractIssuer,
extractContractArgs,
extractUser,
getMethodArguments,
GTake,
User,
} from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
import { getCallParameters, getContractIssuer } from 'lib/onchain';
import { ExecutorMethodArgs, ResolvedOp } from '../../types';
import { pushAwaitedTask } from '../runtime';
import { extractOps } from './extractOps';
let rawContext: Context | null = null; let rawContext: Context | null = null;
const initialContext = { export const setRawContext = (ctx: Context) => {
isChild: false, rawContext = ctx;
thisId: '',
parentId: '' as string | undefined,
ops: [] as ResolvedOp[],
funds: {
availableCweb: 0n,
storedCweb: 0n,
takeOps: [] as GTake<CwebTake>[],
},
methodName: '',
initialArgs: [] as unknown[],
user: null as User | null,
issuer: null as ContractIssuer | null,
parentTxId: '',
needSaveResult: false,
}; };
export const getRawContext = () => { export const getRawContext = () => {
@ -43,111 +14,14 @@ export const getRawContext = () => {
return rawContext; return rawContext;
}; };
export const getMethodArgs = () => {
return getMethodArguments(getRawContext()) as [methodName: string, ...ExecutorMethodArgs];
};
export const handleContext = (ctx: Context) => {
rawContext = ctx;
const contractArgs = extractContractArgs(getRawContext().tx);
const [
methodName,
initialArgs,
resolvedOps,
caller,
thisId,
parentId,
shouldSaveResult,
takenFundsIds = [],
execOpsIndexes = [],
] = getMethodArgs();
initialContext.isChild = !!parentId;
initialContext.thisId =
thisId ??
(BigInt(`0x${parentId ?? '0'}`) * 3n + BigInt(`0x${getRawContext().call.txid}`)).toString(16).slice(0, 32);
initialContext.parentId = parentId;
initialContext.methodName = methodName;
initialContext.initialArgs = initialArgs ?? [];
initialContext.needSaveResult = shouldSaveResult ?? false;
const { authInfo } = getCallParameters(getRawContext());
initialContext.user = (authInfo && extractUser(authInfo)) ?? caller ?? null;
initialContext.issuer = getContractIssuer(getRawContext());
initialContext.parentTxId = getRawContext().call.txid;
console.log('Parse ops in context');
const { extractedOps, executionOpsTakeOp, storedCweb, fundsTakeOps } = extractOps({
contractArgs,
takenFundsIds,
execOpsIndexes,
});
const previousOps = resolvedOps ?? [];
const allResolvedOps = [...previousOps, ...extractedOps];
console.log('new ops >>>', JSON.stringify(extractedOps));
initialContext.ops = allResolvedOps;
const { availableCweb } = getCallParameters(getRawContext());
console.log('Available Cweb: ', availableCweb);
console.log('Stored Cweb: ', storedCweb);
initialContext.funds = {
availableCweb: availableCweb,
storedCweb: storedCweb,
takeOps: fundsTakeOps,
};
if (executionOpsTakeOp) {
pushAwaitedTask(executionOpsTakeOp);
}
return !!executionOpsTakeOp; //This is a flag to check if the execution should restart / TODO: refactor;
};
export const context = { export const context = {
get isChild() { get issuer() {
return initialContext.isChild; return constructContractIssuer(getContractId(getRawContext().tx));
}, },
get thisId() { get authenticated() {
return initialContext.thisId; return getAuthenticated(getRawContext().tx);
},
get parentId() {
return initialContext.parentId;
},
get methodName() {
return getMethodArguments(getRawContext())[0] as string;
},
get initialArgs() {
return initialContext.initialArgs;
}, },
get user() { get user() {
if (!initialContext.user) { return extractUser(getAuthenticated(getRawContext().tx));
throw new Error('User not found');
}
return initialContext.user;
},
get issuer() {
if (!initialContext.issuer) {
throw new Error('Issuer not found');
}
return initialContext.issuer;
},
get parentTxId() {
return initialContext.parentTxId;
},
get ops() {
return initialContext.ops;
},
get needSaveResult() {
return initialContext.needSaveResult;
},
get funds() {
return initialContext.funds;
}, },
}; };

View File

@ -1,194 +0,0 @@
import { constructTake, extractRead, GRead, GTake, ResolvedOperation, ResolvedRead } from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
import { ResolvedOp, TypedClaim } from '../../types';
import { resultKey } from '../claims';
import {
mutexBlockLockKey,
mutexBlockUnlockKey,
mutexExecOpsKey,
MutexExecOpsResult,
MutexNotifyLockState,
} from '../mutex';
import { isResolvedBlockOp, isResolvedReadOp, isResolvedTakeOp } from '../utils';
const extractFunds = (fundsOp: GRead<ResolvedRead>, takenFundsIds: string[]) => {
if (!fundsOp) {
return {
takeOps: [],
amount: 0n,
};
}
const claims = extractRead(fundsOp);
const newClaims = claims?.filter(({ content }) => !takenFundsIds.includes((content.key.second_part as [string])[0]));
if (!newClaims?.length) {
return {
takeOps: [],
amount: 0n,
};
}
return {
takeOps: newClaims.map(({ content }) => constructTake(content.key)),
amount: newClaims.reduce((acc, { content }) => acc + BigInt(content.fees_stored), 0n),
};
};
export const extractOps = ({
contractArgs,
takenFundsIds,
execOpsIndexes,
}: {
contractArgs: ResolvedOperation[];
takenFundsIds: string[];
execOpsIndexes: number[];
}) => {
if (!contractArgs.length) {
return {
extractedOps: [],
fundsTakeOps: [],
storedCweb: 0n,
executionOpsTakeOp: null,
};
}
const [fundsOp, ...resolvedOps] = contractArgs;
if (!isResolvedReadOp(fundsOp)) {
throw new Error('Wrong funds claims');
}
const extractedOps: ResolvedOp[] = [];
let executedOps: MutexExecOpsResult = [];
let executionOpsProfit: bigint = 0n;
let executionOpsTakeOp: GTake<CwebTake> | null = null;
let i = 0;
while (i < resolvedOps.length) {
const op = resolvedOps[i];
if (isResolvedBlockOp(op)) {
const { first } = op.BlockOp.blocks_on[0][0];
console.log('first >>>', JSON.stringify(first));
//Maybe it is needed to check more conditions here
if (Array.isArray(first)) {
switch (first[0]) {
case resultKey: {
const nextAfterBlock = resolvedOps[i + 1];
if (!isResolvedTakeOp(nextAfterBlock)) {
throw new Error('Wrong subcall result');
}
extractedOps.push({
ChildOp: {
ops: (nextAfterBlock.TakeOp.result as TypedClaim<ResolvedOp[]>).body,
},
});
i += 2;
continue;
}
case mutexBlockLockKey: {
const nextAfterBlock = resolvedOps[i + 1];
if (!isResolvedTakeOp(nextAfterBlock)) {
throw new Error('Wrong mutex lock result');
}
extractedOps.push({
LockOp: nextAfterBlock.TakeOp.result.body as MutexNotifyLockState,
});
i += 2;
continue;
}
case mutexBlockUnlockKey: {
const nextAfterBlock = resolvedOps[i + 1];
if (!isResolvedTakeOp(nextAfterBlock)) {
throw new Error('Wrong mutex unlock result');
}
extractedOps.push({ UnlockOp: 0 });
i += 2;
continue;
}
case mutexExecOpsKey: {
const nextAfterBlock = resolvedOps[i + 1];
const execOpResultClaim = extractRead(nextAfterBlock)?.[0]?.content;
if (!execOpResultClaim) {
throw new Error('Wrong mutex exec result claim');
}
executedOps = execOpResultClaim.body as MutexExecOpsResult;
executionOpsProfit = BigInt(execOpResultClaim.fees_stored);
executionOpsTakeOp = constructTake(execOpResultClaim.key);
i += 2;
continue;
}
default:
break;
}
}
}
if (isResolvedTakeOp(op)) {
const keyFirstPart = op.TakeOp.result.key.first_part;
if (Array.isArray(keyFirstPart) && keyFirstPart[0] === mutexExecOpsKey) {
i++;
continue;
}
}
extractedOps.push(op);
i++;
}
const allOps: ResolvedOp[] = [];
const totalOpsCount = extractedOps.length + executedOps.length;
for (let i = 0; i < totalOpsCount; i++) {
if (execOpsIndexes.includes(i)) {
const op = executedOps.shift();
if (!op) {
throw new Error('Wrong mutex exec result place');
}
if (op.ok) {
allOps.push(op.resolved);
} else {
allOps.push({ SlotOp: { ok: false, error: op.error } });
}
} else {
const op = extractedOps.shift();
if (!op) {
throw new Error('Contract call args parsing error');
}
allOps.push(op);
}
}
const { takeOps, amount } = extractFunds(fundsOp, takenFundsIds);
return {
fundsTakeOps: takeOps,
storedCweb: amount + executionOpsProfit,
extractedOps: allOps,
executionOpsTakeOp,
};
};

View File

@ -0,0 +1,94 @@
import {
Context,
extractContractArgs,
NewTx,
ResolvedOperation,
getMethodArguments,
constructContinueTx,
constructContractRef,
} from '@coinweb/contract-kit';
import { getCallParameters, queue } from 'lib/onchain';
import { context, getRawContext, setRawContext } from './context';
import { getAwaitedOps } from './ops/awaited';
import { pushResolvedOp } from './ops/resolved';
import { signal } from './signal';
const handleState = () => {
const ctx = getRawContext();
const resolvedOps = extractContractArgs(ctx.tx);
const methodArgs = getMethodArguments(ctx) as [unknown, unknown[], ResolvedOperation[]];
const initialArgs = methodArgs[1] ?? [];
const previousOps = methodArgs[2] ?? [];
const allResolvedOps = [...previousOps, ...resolvedOps];
pushResolvedOp(allResolvedOps);
return { args: initialArgs, methodName: methodArgs[0] as string, ops: allResolvedOps };
};
export const executor =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(method: (...args: any[]) => Generator) =>
(ctx: Context): NewTx[] => {
setRawContext(ctx);
const { args, methodName, ops } = handleState();
const execution = method(...args);
let value: unknown;
let isValue = false;
while (true) {
let result: IteratorResult<unknown>;
if (!isValue) {
result = execution.next();
isValue = true;
} else {
result = execution.next(value);
}
value = result.value;
if (result.done || result.value === signal) {
break;
}
}
const { authInfo, availableCweb } = getCallParameters(ctx);
const awaitedOps = getAwaitedOps();
if (!awaitedOps.length) {
return queue.gateway.unlock(ctx);
}
const txFee = 700n + BigInt(awaitedOps.length) * 100n;
return [
constructContinueTx(
ctx,
[],
[
{
callInfo: {
ref: constructContractRef(context.issuer, []),
methodInfo: {
methodName,
methodArgs: [args, ops],
},
contractInfo: {
providedCweb: availableCweb - txFee,
authenticated: authInfo,
},
contractArgs: awaitedOps,
},
},
]
),
];
};

View File

@ -1,13 +0,0 @@
import { getAwaitedTasks } from '../../runtime';
import { prepareTx } from './prepareTxs';
export const constructTx = (isFullyExecuted: boolean) => {
const awaitedTasks = getAwaitedTasks();
const { calls, txFee } = prepareTx({ awaitedTasks, isFullyExecuted, txFee: 0n });
const { txs } = prepareTx({ awaitedTasks, isFullyExecuted, txFee, callsCount: calls });
return txs;
};

View File

@ -1 +0,0 @@
export * from './constructTx';

View File

@ -1,292 +0,0 @@
import {
constructBlock,
constructContinueTx,
constructContractRef,
FullCallInfo,
GTake,
NewTx,
passCwebFrom,
PreparedOperation,
sendCwebInterface,
} from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
import { ExecutorMethodArgs, PreparedExtendedStoreOp, PreparedOp, ResolvedSlotOp } from '../../../types';
import { constructFundsClaimRangRead, constructFundsClaimStore } from '../../claims/funds';
import { constructResultBlockFilter, constructResultClaimTake, resultKey } from '../../claims/result';
import { context, getRawContext } from '../../context';
import { constructExecOpsCall, constructLockCall, constructUnlockCall, mutexExecOpsKey } from '../../mutex';
import {
isPreparedExecOp,
isPreparedLockOp,
isPreparedStoreOp,
isPreparedTakeOp,
isPreparedUnlockOp,
} from '../../utils/opTypeGuards';
import { uuid } from '../../utils/uuid';
export const prepareInThreadTxs = ({
cwebPerCall,
outThreadTasksCount,
outThreadFee,
ops,
}: {
ops: PreparedOp[];
cwebPerCall: bigint;
outThreadTasksCount: number;
outThreadFee: bigint;
}): {
txs: NewTx[];
calls: number;
txFee: bigint;
} => {
if (!ops.length) {
const { constructSendCweb } = sendCwebInterface();
const { availableCweb, takeOps, storedCweb } = context.funds;
const { user } = context;
const restOfAvailableCweb = availableCweb - outThreadFee;
const fee = BigInt(takeOps.length) * 100n + 3000n; //TODO: Remove magic numbers
const restOfCweb = restOfAvailableCweb + storedCweb - fee;
const txs =
restOfCweb > 0n
? [
constructContinueTx(getRawContext(), [
passCwebFrom(context.issuer, restOfAvailableCweb),
...takeOps,
...constructSendCweb(restOfCweb, user, null),
]),
]
: [];
return { txs, calls: 0, txFee: fee };
}
let txFee = 0n;
let callsPrepared = 0;
const resolvedSlotOps = new Array(outThreadTasksCount).fill({ SlotOp: { ok: true } }) satisfies ResolvedSlotOp[];
//Mutex exec ops
const preparedExecOps: (PreparedExtendedStoreOp | GTake<CwebTake>)[] = [];
const excOpsIndexes: number[] = [];
// const resolvedChildOps: ResolvedOp[] = [...context.ops, ...resolvedSlotOps];
//Children
//Arg for the main call
const callArgs: PreparedOperation[] = [];
//Info for separate parallel calls
const parallelCalls: FullCallInfo[] = [];
const outThreadOps: PreparedOperation[] = [];
ops.forEach((op, i) => {
switch (true) {
case isPreparedExecOp(op): {
const id = op.ExecOp.id;
callArgs.push(constructBlock([constructResultBlockFilter(id)]), constructResultClaimTake(id));
txFee += 200n;
const childOps = [
...context.ops,
...resolvedSlotOps,
...ops.map((_, j) => (i === j ? { ExecOp: { id } } : { SlotOp: { ok: true } })),
];
parallelCalls.push({
callInfo: {
ref: constructContractRef(context.issuer, []),
methodInfo: {
methodName: context.methodName,
methodArgs: [
context.initialArgs,
childOps,
context.user,
id,
context.thisId,
true,
] satisfies ExecutorMethodArgs,
},
contractInfo: {
providedCweb: cwebPerCall,
authenticated: null,
},
contractArgs: [],
},
});
callsPrepared++;
txFee += 700n;
break;
}
case isPreparedStoreOp(op): {
if ((op.StoreOp.key.first_part as [string])[0] !== resultKey) {
preparedExecOps.push(op);
excOpsIndexes.push(i);
} else {
callArgs.push(op);
txFee += 100n;
}
break;
}
case isPreparedTakeOp(op): {
//TODO: simplify to decrease coupling
if ((op.TakeOp.key.first_part as [string])[0] !== mutexExecOpsKey) {
preparedExecOps.push(op);
excOpsIndexes.push(i);
} else {
callArgs.push(op);
txFee += 100n;
}
break;
}
case isPreparedLockOp(op): {
const {
callInfo,
fee,
inThreadOps,
outThreadOps: outThreadOpsFromLockCall,
} = constructLockCall({
issuer: context.issuer,
lockId: op.LockOp.lockId,
timestamp: op.LockOp.timestamp,
keys: op.LockOp.keys,
processId: context.thisId,
});
parallelCalls.push({ callInfo });
callArgs.push(...inThreadOps);
txFee += fee;
outThreadOps.push(...outThreadOpsFromLockCall);
break;
}
case isPreparedUnlockOp(op): {
console.log('prepareInThreadTxs >>> unlockOp');
const { callInfo, fee, ops } = constructUnlockCall(
context.issuer,
op.UnlockOp.lockId,
op.UnlockOp.timestamp,
true
);
parallelCalls.push({ callInfo });
callArgs.push(...ops);
txFee += fee;
break;
}
default:
callArgs.push(op);
txFee += 100n;
}
});
if (preparedExecOps.length > 0) {
const execId = uuid();
const { callInfo, fee, ops } = constructExecOpsCall({
issuer: context.issuer,
ops: preparedExecOps,
processId: context.thisId,
execId,
});
parallelCalls.push({ callInfo });
txFee += fee;
console.log(txFee);
console.log(context.funds.availableCweb);
callArgs.push(...ops);
}
const returnTxs: NewTx[] = [];
if (callArgs.length) {
const { storedCweb, takeOps } = context.funds;
const latestCallArg = callArgs.at(-1)!;
if ('StoreOp' in latestCallArg && (latestCallArg.StoreOp.key.first_part as [string])[0] === resultKey) {
//SAVE RESULT CLAIMS
console.log('SAVE RESULT CLAIMS');
if (callArgs.length > 1) {
throw new Error('Unexpected count of result ops');
}
const resultOps = [];
if (context.needSaveResult) {
resultOps.push(...callArgs);
}
if (context.parentId) {
const { availableCweb, storedCweb, takeOps } = context.funds;
const cwebToStore = availableCweb + storedCweb - BigInt(takeOps.length) * 100n - 500n;
console.log('cwebToStore: ', cwebToStore);
resultOps.push(
passCwebFrom(context.issuer, availableCweb),
...takeOps,
constructFundsClaimStore(context.parentId, cwebToStore)
);
}
if (resultOps.length) {
returnTxs.push(constructContinueTx(getRawContext(), resultOps));
}
} else {
txFee += 800n + outThreadFee + BigInt(takeOps.length) * 100n;
callsPrepared++;
console.log('provided cweb: ', cwebPerCall - txFee + storedCweb);
returnTxs.push(
constructContinueTx(
getRawContext(),
[passCwebFrom(context.issuer, cwebPerCall - outThreadFee), ...takeOps],
[
{
callInfo: {
ref: constructContractRef(context.issuer, []),
methodInfo: {
methodName: context.methodName,
methodArgs: [
context.initialArgs,
[...context.ops, ...resolvedSlotOps],
context.user,
context.thisId,
context.parentId,
context.needSaveResult,
takeOps.map((op) => (op.TakeOp.key.second_part as [string])[0]),
excOpsIndexes,
] satisfies ExecutorMethodArgs,
},
contractInfo: {
providedCweb: cwebPerCall - txFee + storedCweb,
authenticated: null,
},
contractArgs: [constructFundsClaimRangRead(context.thisId), ...callArgs],
},
},
]
)
);
if (parallelCalls.length || outThreadOps.length) {
returnTxs.push(constructContinueTx(getRawContext(), outThreadOps, parallelCalls));
}
}
}
return { txs: returnTxs, calls: callsPrepared, txFee };
};

View File

@ -1,141 +0,0 @@
import {
constructContinueTx,
constructContractRef,
FullCallInfo,
GTake,
NewTx,
PreparedOperation,
} from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
import { ExecutorMethodArgs, PreparedExtendedStoreOp, PreparedOp } from '../../../types';
import { context, getRawContext } from '../../context';
import { constructLockCall, constructExecOpsCall, constructUnlockCall } from '../../mutex';
import {
isPreparedBlockOp,
isPreparedExecOp,
isPreparedLockOp,
isPreparedStoreOp,
isPreparedTakeOp,
isPreparedUnlockOp,
} from '../../utils/opTypeGuards';
export const prepareOutThreadTxs = ({
ops,
cwebPerCall,
}: {
ops: PreparedOp[];
cwebPerCall: bigint;
}): {
txs: NewTx[];
txFee: bigint;
calls: number;
} => {
const siblingCallResolvedOps = [...context.ops];
const preparedCalls: FullCallInfo[] = [];
const preparedOps: PreparedOperation[] = [];
const preparedExecOps: (PreparedExtendedStoreOp | GTake<CwebTake>)[] = [];
let txFee = 0n;
let callsPrepared = 0;
ops.forEach((op) => {
switch (true) {
case isPreparedExecOp(op): {
const id = op.ExecOp.id;
const callInfo = {
callInfo: {
ref: constructContractRef(context.issuer, []),
methodInfo: {
methodName: context.methodName,
methodArgs: [
context.initialArgs,
[...siblingCallResolvedOps, { ExecOp: { id } }],
context.user,
id,
context.parentId ?? context.thisId,
false,
] satisfies ExecutorMethodArgs,
},
contractInfo: {
providedCweb: cwebPerCall,
authenticated: null,
},
contractArgs: [],
},
};
preparedCalls.push(callInfo);
txFee += 700n;
callsPrepared++;
break;
}
case isPreparedStoreOp(op):
case isPreparedTakeOp(op): {
preparedExecOps.push(op);
break;
}
case isPreparedLockOp(op): {
const { callInfo, fee, outThreadOps } = constructLockCall({
issuer: context.issuer,
lockId: op.LockOp.lockId,
timestamp: op.LockOp.timestamp,
keys: op.LockOp.keys,
processId: context.thisId,
});
preparedCalls.push({ callInfo });
txFee += fee;
preparedOps.push(...outThreadOps);
break;
}
case isPreparedUnlockOp(op): {
console.log('prepareOutThreadTxs >>> unlockOp');
const { callInfo, fee } = constructUnlockCall(context.issuer, op.UnlockOp.lockId, op.UnlockOp.timestamp, false);
preparedCalls.push({ callInfo });
txFee += fee;
break;
}
case isPreparedBlockOp(op): {
break;
}
default:
preparedOps.push(op);
txFee += 100n;
}
siblingCallResolvedOps.push({ SlotOp: { ok: true } });
});
if (preparedExecOps.length > 0) {
const { callInfo, fee } = constructExecOpsCall({
issuer: context.issuer,
ops: preparedExecOps,
processId: context.thisId,
});
preparedCalls.push({ callInfo });
txFee += fee;
}
if (preparedCalls.length > 0 || preparedOps.length > 0) {
return {
txs: [constructContinueTx(getRawContext(), preparedOps, preparedCalls)],
txFee,
calls: callsPrepared,
};
}
return {
txs: [],
txFee: 0n,
calls: 0,
};
};

View File

@ -1,75 +0,0 @@
import { constructContinueTx, NewTx, passCwebFrom, sendCwebInterface } from '@coinweb/contract-kit';
import { Task } from '../../../types';
import { context, getRawContext } from '../../context';
import { prepareInThreadTxs } from './prepareInThreadTxs';
import { prepareOutThreadTxs } from './prepareOutThreadTxs';
import { splitTasks } from './splitTasks';
export const prepareTx = ({
awaitedTasks,
isFullyExecuted,
txFee,
callsCount,
}: {
awaitedTasks: Task[];
isFullyExecuted: boolean;
txFee: bigint;
callsCount?: number;
}): { txs: NewTx[]; calls: number; txFee: bigint } => {
if (!awaitedTasks.length) {
if (context.isChild) {
return { txs: [], calls: 0, txFee: 0n };
}
const { constructSendCweb } = sendCwebInterface();
const { availableCweb, takeOps, storedCweb } = context.funds;
const { user } = context;
const fee = BigInt(takeOps.length) * 100n + 3000n; //TODO: Remove magic numbers
const restOfCweb = availableCweb + storedCweb - fee;
const txs =
restOfCweb > 0n
? [
constructContinueTx(getRawContext(), [
passCwebFrom(context.issuer, availableCweb),
...takeOps,
...constructSendCweb(restOfCweb, user, null),
]),
]
: [];
return { txs, calls: 0, txFee: fee };
}
const { inThreadOps, outThreadOps } = splitTasks(awaitedTasks, isFullyExecuted);
const { availableCweb } = context.funds;
const cwebPerCall = (availableCweb - txFee) / BigInt(callsCount || 1);
const {
txs: outThreadTxs,
calls: outThreadCallsPrepared,
txFee: outThreadFee,
} = prepareOutThreadTxs({ ops: outThreadOps, cwebPerCall });
const {
txs: inThreadTxs,
calls: inThreadCallsPrepared,
txFee: inThreadFee,
} = prepareInThreadTxs({
ops: inThreadOps,
cwebPerCall,
outThreadTasksCount: outThreadOps.length,
outThreadFee,
});
return {
txs: [...inThreadTxs, ...outThreadTxs],
calls: inThreadCallsPrepared + outThreadCallsPrepared,
txFee: inThreadFee + outThreadFee,
};
};

View File

@ -1,26 +0,0 @@
import { PreparedOp, Task } from '../../../types';
export const splitTasks = (awaitedTasks: Task[], isFullyExecuted: boolean) => {
if (awaitedTasks.length === 0) {
return { inThreadOps: [], outThreadOps: [] };
}
const inThreadOps: PreparedOp[] = [];
let outThreadOps: PreparedOp[] = [];
if (!isFullyExecuted) {
const latestTaskBatchId = awaitedTasks.at(-1)!.batchId ?? -1;
awaitedTasks.forEach((task) => {
if (task.batchId === latestTaskBatchId) {
inThreadOps.push(task.op);
} else {
outThreadOps.push(task.op);
}
});
} else {
outThreadOps = awaitedTasks.map((task) => task.op);
}
return { inThreadOps, outThreadOps };
};

View File

@ -1,42 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Context, NewTx, getMethodArguments, isSelfCall } from '@coinweb/contract-kit';
import { context, getRawContext, handleContext } from '../context';
import { execLoop, setNextExec, stopExecution } from '../runtime';
import { pushResolvedOp } from '../runtime/resolvedOps';
import { constructTx } from './constructTx';
export const executor = (method: (...args: any[]) => Promise<void>) => {
return async (ctx: Context): Promise<NewTx[]> => {
console.log('executor-start >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
const shouldRestart = handleContext(ctx);
pushResolvedOp(context.ops);
if (getMethodArguments(getRawContext()).length > 2 && !isSelfCall(ctx)) {
throw new Error('Wrong contract call, check the call arguments');
}
if (shouldRestart) {
console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-finish-restart');
return constructTx(false);
}
//@ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
os.setTimeout(() => {
stopExecution();
}, 0);
try {
setNextExec(() => method(...context.initialArgs), []);
const isFullyExecuted = await execLoop();
console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-finish-end');
return constructTx(isFullyExecuted);
} catch (error) {
console.log((error as Error).message);
console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-error');
throw error;
}
};
};

View File

@ -1 +0,0 @@
export * from './executor';

View File

@ -1,30 +0,0 @@
import { ContractHandlers as ContractHandlersOrig, SELF_REGISTER_HANDLER_NAME } from '@coinweb/contract-kit';
import { selfRegisterHandler } from '@coinweb/self-register';
import { queue } from 'lib/onchain';
import { addMethodHandler, ContractHandlers, executeHandler } from '../../../contract-kit';
import { executor } from '../../executor';
import { mutexMethods } from '../../mutex';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const constructCwebMain = (methods: Record<string, (...args: any[]) => Promise<void>>) => async () => {
const module: ContractHandlers = { handlers: {} };
Object.entries(methods).forEach(([name, handler]) => {
if (name.startsWith('_')) {
throw new Error('Do not start method names with "_" because they are reserved for internal use');
}
addMethodHandler(module, name, executor(handler));
});
Object.entries(mutexMethods).forEach(([name, handler]) => {
addMethodHandler(module, name, handler);
});
addMethodHandler(module, SELF_REGISTER_HANDLER_NAME, selfRegisterHandler);
queue.applyQueue(module as ContractHandlersOrig, []);
await executeHandler(module);
};

View File

@ -1 +0,0 @@
export * from './constructCwebMain';

View File

@ -1,71 +0,0 @@
import { constructStore } from '@coinweb/contract-kit/dist/esm/operations/store';
import { constructResultClaim } from '../../claims/result';
import { opMarker } from '../../globals/promise';
import { setNextExec, stopExecution } from '../../runtime';
import { getAwaitedTasksCount, pushAwaitedTask } from '../../runtime/awaitedTasks';
import { getUsedOps, startSavingUsedOps, stopSavingUsedOps, shiftResolvedOp } from '../../runtime/resolvedOps';
import { isResolvedChildOp, isResolvedExecOp, isResolvedSlotOp } from '../../utils';
import { uuid } from '../../utils';
let isRootDetected = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const cwait = <TAsyncCallback extends (...args: any[]) => Promise<unknown>>(asyncCallback: TAsyncCallback) => {
let isRoot = false;
return (async (...args: Parameters<TAsyncCallback>) => {
if (!isRootDetected) {
isRootDetected = true;
isRoot = true;
}
if (isRoot) {
return asyncCallback(...args);
}
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask({
ExecOp: {
id: uuid(),
},
});
const result = new Promise(() => null) as Promise<unknown> & { [opMarker]: boolean };
result[opMarker] = true;
return result as ReturnType<TAsyncCallback>;
} else {
if (isResolvedSlotOp(op)) {
return new Promise(() => null);
}
if (isResolvedExecOp(op)) {
setNextExec(async () => {
startSavingUsedOps();
await asyncCallback(...args);
stopSavingUsedOps();
if (!getAwaitedTasksCount()) {
console.log('push result claim');
pushAwaitedTask(constructStore(constructResultClaim(op.ExecOp.id, getUsedOps())));
}
}, []);
stopExecution(); //Check: maybe does no affect
return;
}
if (isResolvedChildOp(op)) {
return setNextExec(() => asyncCallback(...args), op.ChildOp.ops);
}
throw new Error('Exec or Child operation not found');
}
}) as TAsyncCallback;
};

View File

@ -1 +0,0 @@
export * from './cwait';

View File

@ -1,4 +0,0 @@
export * from './cwait';
export * from './mutex';
export * from './ops';
export * from './constructCwebMain';

View File

@ -1 +0,0 @@
export * from './lock';

View File

@ -1,78 +0,0 @@
import { getTime } from 'lib/onchain';
import { opMarker } from '../../globals/promise';
import { LockedKey } from '../../mutex';
import { pushAwaitedTask, shiftResolvedOp } from '../../runtime';
import { isResolvedLockOp, isResolvedSlotOp, isResolvedUnlockOp, uuid } from '../../utils';
const unlock = (lockId: string, timestamp: number) => {
console.log('unlockOp');
let opMarkerValue = false;
const result = new Promise<void>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask({ UnlockOp: { lockId, timestamp } });
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
console.log('unlockOp-slotOp');
return;
}
if (!isResolvedUnlockOp(op)) {
console.log('unlockOp-error');
throw new Error('Unlock operation not found');
}
resolve();
}
} catch (error) {
reject(error);
}
});
(result as Promise<void> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};
export const lock = (keys: LockedKey[] | LockedKey) => {
console.log('lockOp');
let opMarkerValue = false;
const result = new Promise<() => Promise<void>>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask({
LockOp: { lockId: uuid(), keys: Array.isArray(keys) ? keys : [keys], timestamp: getTime() },
});
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
console.log('lockOp-slotOp');
return;
}
if (!isResolvedLockOp(op)) {
console.log('lockOp-error');
throw new Error('Lock operation not found');
}
const result = op.LockOp;
resolve(() => unlock(result.lockId, result.timestamp));
}
} catch (error) {
reject(error);
}
});
(result as Promise<() => Promise<void>> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};

View File

@ -1,38 +0,0 @@
import { BlockFilter, constructBlock, extractBlock } from '@coinweb/contract-kit';
import { opMarker } from '../../globals/promise';
import { isResolvedBlockOp, isResolvedSlotOp } from '../../utils';
import { pushAwaitedTask, shiftResolvedOp } from '../../runtime';
export const blockOp = (filters: BlockFilter[]) => {
let opMarkerValue = false;
const result = new Promise<[BlockFilter, boolean][] | null>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask(constructBlock(filters));
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
return;
}
if (!isResolvedBlockOp(op)) {
throw new Error('Block operation not found');
}
const result = extractBlock(op);
resolve(result);
}
} catch (error) {
reject(error);
}
});
(result as Promise<[BlockFilter, boolean][] | null> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};

View File

@ -1,44 +0,0 @@
import { Claim, ClaimKey, constructRangeRead, extractRead } from '@coinweb/contract-kit';
import { ClaimRange } from '@coinweb/contract-kit/dist/types/operations/read';
import { TypedClaim } from '../../../types';
import { context } from '../../context';
import { opMarker } from '../../globals/promise';
import { isResolvedReadOp, isResolvedSlotOp } from '../../utils';
import { pushAwaitedTask, shiftResolvedOp } from '../../runtime';
export const rangeReadOp = <TClaims extends Claim[] = TypedClaim[]>(
firstPart: ClaimKey['first_part'],
range: ClaimRange,
maxCount: number
) => {
let opMarkerValue = false;
const result = new Promise<TClaims | null>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask(constructRangeRead(context.issuer, firstPart, range, maxCount));
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
return;
}
if (!isResolvedReadOp(op)) {
throw new Error('Read operation not found');
}
const claim = (extractRead(op)?.map((result) => result.content) ?? null) as TClaims | null;
resolve(claim);
}
} catch (error) {
reject(error);
}
});
(result as Promise<TClaims | null> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};

View File

@ -1,43 +0,0 @@
import { Claim, ClaimKey, constructRead, extractRead } from '@coinweb/contract-kit';
import { TypedClaim } from '../../../types';
import { context } from '../../context';
import { opMarker } from '../../globals/promise';
import { pushAwaitedTask, shiftResolvedOp } from '../../runtime';
import { isResolvedReadOp, isResolvedSlotOp } from '../../utils';
export const readOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => {
let opMarkerValue = false;
const result = new Promise<TClaim | null>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask(constructRead(context.issuer, key));
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
return;
}
if (!isResolvedReadOp(op)) {
console.log(JSON.stringify(op));
throw new Error('Read operation not found');
}
const claim = (extractRead(op)?.[0]?.content ?? null) as TClaim | null;
console.log('ResolveRead claim: ', claim);
resolve(claim);
}
} catch (error) {
reject(error);
}
});
(result as Promise<TClaim | null> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};

View File

@ -1,39 +0,0 @@
import { Claim, constructStore } from '@coinweb/contract-kit';
import { extractStore } from '@coinweb/contract-kit/dist/esm/operations/store';
import { opMarker } from '../../globals/promise';
import { isResolvedSlotOp, isResolvedStoreOp } from '../../utils';
import { pushAwaitedTask, shiftResolvedOp } from '../../runtime';
export const storeOp = (claim: Claim, storeCweb?: bigint) => {
let opMarkerValue = false;
const result = new Promise<Claim | null>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask({ ...constructStore(claim), providedCweb: storeCweb });
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
return;
}
if (!isResolvedStoreOp(op)) {
throw new Error('Store operation not found');
}
const result = extractStore(op);
resolve(result);
}
} catch (error) {
reject(error);
}
});
(result as Promise<Claim | null> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};

View File

@ -1,38 +0,0 @@
import { Claim, ClaimKey, constructTake, extractTake } from '@coinweb/contract-kit';
import { TypedClaim } from '../../../types';
import { opMarker } from '../../globals/promise';
import { isResolvedSlotOp, isResolvedTakeOp } from '../../utils';
import { pushAwaitedTask, shiftResolvedOp } from '../../runtime';
export const takeOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => {
let opMarkerValue = false;
const result = new Promise<TClaim | null>((resolve, reject) => {
try {
const { op, isOp } = shiftResolvedOp();
if (!isOp) {
pushAwaitedTask(constructTake(key));
opMarkerValue = true;
} else {
if (isResolvedSlotOp(op)) {
return;
}
if (!isResolvedTakeOp(op)) {
throw new Error('Take operation not found');
}
const claim = extractTake(op);
resolve(claim as TClaim | null);
}
} catch (error) {
reject(error);
}
});
(result as Promise<TClaim | null> & { [opMarker]: boolean })[opMarker] = opMarkerValue;
return result;
};

View File

@ -1 +0,0 @@
export * from './promise';

View File

@ -1,38 +0,0 @@
import { markTaskBatch } from '../runtime/awaitedTasks';
let batchId = 0;
const getBatchId = () => batchId++;
export const opMarker = Symbol('opMarker');
const wrapPromiseUtil =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<T extends (values: any) => any>(nativePromiseUtil: T): T =>
((values: Parameters<T>) => {
let argsWithMarker = 0;
values.forEach((arg) => {
if (arg instanceof Promise && (arg as Promise<unknown> & { [opMarker]: boolean })[opMarker]) {
argsWithMarker++;
}
});
if (argsWithMarker > 0) {
markTaskBatch(argsWithMarker, getBatchId());
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return nativePromiseUtil(values);
}) as T;
const nativePromiseAll = Promise.all.bind(Promise);
const nativePromiseRace = Promise.race.bind(Promise);
const nativePromiseAny = Promise.any.bind(Promise);
const nativePromiseSettled = Promise.allSettled.bind(Promise);
const nativePromiseAllSettled = Promise.allSettled.bind(Promise);
globalThis.Promise.all = wrapPromiseUtil(nativePromiseAll);
globalThis.Promise.race = wrapPromiseUtil(nativePromiseRace);
globalThis.Promise.any = wrapPromiseUtil(nativePromiseAny);
globalThis.Promise.allSettled = wrapPromiseUtil(nativePromiseSettled);
globalThis.Promise.allSettled = wrapPromiseUtil(nativePromiseAllSettled);

View File

@ -1,3 +1,3 @@
import './globals'; export * from './context';
export * from './executor';
export * from './features'; export * from './ops';

View File

@ -1,72 +0,0 @@
import { constructContractRef, ContractIssuer, CwebStore, GStore, GTake } from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
import { PreparedExtendedStoreOp } from '../../../types';
import {
constructMutexExecOpsBlock,
constructMutexExecOpsClaimRead,
constructMutexLockClaimRangeRead,
} from '../claims';
import { execOpsMethodName } from '../methods';
import { MutexExecOpArgs } from '../types';
export const constructExecOpsCall = ({
issuer,
ops,
processId,
execId,
}: {
issuer: ContractIssuer;
ops: (GTake<CwebTake> | PreparedExtendedStoreOp)[];
processId: string;
execId?: string;
}) => {
const opsFee = BigInt(ops.length) * 100n;
const execCallInnerFee = 700n;
const takeOpsAdditionalFee = BigInt(ops.filter((op) => 'TakeOp' in op).length) * 100n;
const prepareTakeOpsInnerFee = takeOpsAdditionalFee > 0n ? 700n : 0n;
const storedCweb = ops
.filter((op) => 'StoreOp' in op)
.reduce((total, op) => {
if (op.providedCweb) {
return total + op.providedCweb;
}
return total + BigInt(op.StoreOp.fees_stored);
}, 0n);
const callFee = 700n;
const providedCweb = opsFee + execCallInnerFee + takeOpsAdditionalFee + prepareTakeOpsInnerFee + storedCweb + callFee;
const contractArgsFee = 100n;
const preparedOps: (GTake<CwebTake> | GStore<CwebStore>)[] = ops.map((op) => {
if ('StoreOp' in op) {
const preparedStoreOp = { ...op };
delete preparedStoreOp.providedCweb;
return preparedStoreOp;
}
return op;
});
return {
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: execOpsMethodName,
methodArgs: [preparedOps, processId, execId] satisfies MutexExecOpArgs,
},
contractInfo: {
providedCweb,
authenticated: null,
},
contractArgs: [constructMutexLockClaimRangeRead(issuer)],
},
fee: providedCweb + contractArgsFee,
ops: execId
? ([constructMutexExecOpsBlock(execId, issuer), constructMutexExecOpsClaimRead(issuer, execId)] as const)
: [],
};
};

View File

@ -1,40 +0,0 @@
import { ContractIssuer, constructContractRef } from '@coinweb/contract-kit';
import { constructMutexBlockLockClaimTake, constructMutexLockBlock, constructMutexLockClaimStore } from '../claims';
import { lockMethodName } from '../methods';
import { lockFee } from '../settings';
import { LockedKey } from '../types';
export const constructLockCall = ({
issuer,
keys,
lockId,
timestamp,
processId,
}: {
issuer: ContractIssuer;
lockId: string;
timestamp: number;
keys: LockedKey[];
processId: string;
}) => {
return {
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: lockMethodName,
methodArgs: [],
},
contractInfo: {
providedCweb: lockFee,
authenticated: null,
},
contractArgs: [],
},
inThreadOps: [constructMutexLockBlock(lockId, issuer), constructMutexBlockLockClaimTake(lockId)] as const,
outThreadOps: [
constructMutexLockClaimStore({ fee: lockFee, keys, lockId, timestamp, processId, locked: false }),
] as const,
fee: lockFee * 2n + 200n,
};
};

View File

@ -1,27 +0,0 @@
import { constructContractRef, ContractIssuer } from '@coinweb/contract-kit';
import { constructMutexBlockUnlockClaimTake, constructMutexUnlockBlock } from '../claims';
import { unlockMethodName } from '../methods';
import { unlockFee } from '../settings';
import { MutexUnlockArgs } from '../types';
export const constructUnlockCall = (issuer: ContractIssuer, ...[lockId, timestamp, notify]: MutexUnlockArgs) => {
return {
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: unlockMethodName,
methodArgs: [lockId, timestamp] satisfies MutexUnlockArgs,
},
contractInfo: {
providedCweb: unlockFee,
authenticated: null,
},
contractArgs: [],
},
ops: notify
? ([constructMutexUnlockBlock(lockId, issuer), constructMutexBlockUnlockClaimTake(lockId)] as const)
: [],
fee: unlockFee + (notify ? 1000n : 800n),
};
};

View File

@ -1,3 +0,0 @@
export * from './constructLockCall';
export * from './constructExecOpsCall';
export * from './constructUnlockCall';

View File

@ -1,4 +0,0 @@
export * from './mutexExecOps';
export * from './mutexBlockLock';
export * from './mutexBlockUnlock';
export * from './mutexLock';

View File

@ -1,38 +0,0 @@
import {
BlockFilter,
constructBlock,
constructClaim,
constructClaimKey,
constructStore,
constructTake,
ContractIssuer,
} from '@coinweb/contract-kit';
import { MutexNotifyLockState } from '../types';
export const mutexBlockLockKey = '_mutex_block_lock';
export const constructMutexBlockLockClaimKey = (lockId: string) => constructClaimKey([mutexBlockLockKey], [lockId]);
export const constructMutexBlockLockClaim = ({ lockId, timestamp }: MutexNotifyLockState) =>
constructClaim(constructMutexBlockLockClaimKey(lockId), { lockId, timestamp } satisfies MutexNotifyLockState, '0x0');
export const constructMutexBlockLockClaimStore = ({ lockId, timestamp }: MutexNotifyLockState) =>
constructStore(constructMutexBlockLockClaim({ lockId, timestamp }));
export const constructMutexBlockLockClaimTake = (lockId: string) =>
constructTake(constructMutexBlockLockClaimKey(lockId));
export const constructMutexLockBlockFilter = (lockId: string, issuer: ContractIssuer): BlockFilter => {
const { first_part: first, second_part: second } = constructMutexBlockLockClaimKey(lockId);
return {
issuer,
first,
second,
};
};
export const constructMutexLockBlock = (lockId: string, issuer: ContractIssuer) => {
return constructBlock([constructMutexLockBlockFilter(lockId, issuer)]);
};

View File

@ -1,39 +0,0 @@
import {
BlockFilter,
constructBlock,
constructClaim,
constructClaimKey,
constructStore,
constructTake,
ContractIssuer,
CwebStore,
GStore,
} from '@coinweb/contract-kit';
export const mutexBlockUnlockKey = '_mutex_block_unlock';
export const constructMutexBlockUnlockClaimKey = (uniqueId: string) =>
constructClaimKey([mutexBlockUnlockKey], [uniqueId]);
export const constructMutexBlockUnlockClaim = (uniqueId: string) =>
constructClaim(constructMutexBlockUnlockClaimKey(uniqueId), {}, '0x0');
export const constructMutexBlockUnlockClaimStore = (uniqueId: string): GStore<CwebStore> =>
constructStore(constructMutexBlockUnlockClaim(uniqueId));
export const constructMutexBlockUnlockClaimTake = (uniqueId: string) =>
constructTake(constructMutexBlockUnlockClaimKey(uniqueId));
export const constructMutexUnlockBlockFilter = (lockId: string, issuer: ContractIssuer): BlockFilter => {
const { first_part: first, second_part: second } = constructMutexBlockUnlockClaimKey(lockId);
return {
issuer,
first,
second,
};
};
export const constructMutexUnlockBlock = (lockId: string, issuer: ContractIssuer) => {
return constructBlock([constructMutexUnlockBlockFilter(lockId, issuer)]);
};

View File

@ -1,47 +0,0 @@
import {
BlockFilter,
constructBlock,
constructClaim,
constructClaimKey,
constructRead,
constructStore,
constructTake,
ContractIssuer,
CwebStore,
GStore,
} from '@coinweb/contract-kit';
import { toHex } from 'lib/shared';
import { MutexExecOpsResult } from '../types';
export const mutexExecOpsKey = '_mutex_exec_ops';
export const constructMutexExecOpsClaimKey = (execId: string) => constructClaimKey([mutexExecOpsKey], [execId]);
export const constructMutexExecOpsClaim = (execId: string, result: MutexExecOpsResult, storeCweb: bigint) =>
constructClaim(constructMutexExecOpsClaimKey(execId), result, toHex(storeCweb));
export const constructMutexExecOpsClaimStore = (
execId: string,
result: MutexExecOpsResult,
storeCweb: bigint = 0n
): GStore<CwebStore> => constructStore(constructMutexExecOpsClaim(execId, result, storeCweb));
export const constructMutexExecOpsClaimRead = (issuer: ContractIssuer, execId: string) =>
constructRead(issuer, constructMutexExecOpsClaimKey(execId));
export const constructMutexExecOpsClaimTake = (execId: string) => constructTake(constructMutexExecOpsClaimKey(execId));
export const constructMutexExecOpsFilter = (lockId: string, issuer: ContractIssuer): BlockFilter => {
const { first_part: first, second_part: second } = constructMutexExecOpsClaimKey(lockId);
return {
issuer,
first,
second,
};
};
export const constructMutexExecOpsBlock = (lockId: string, issuer: ContractIssuer) => {
return constructBlock([constructMutexExecOpsFilter(lockId, issuer)]);
};

View File

@ -1,63 +0,0 @@
import {
constructClaim,
constructClaimKey,
constructRangeRead,
constructStore,
ContractIssuer,
GRead,
} from '@coinweb/contract-kit';
import { CwebRead } from '@coinweb/contract-kit/dist/types/operations/read';
import { toHex } from 'lib/shared';
import { rangeReadLimit } from '../settings';
import { LockedKey, MutexLockState } from '../types';
export const mutexLockKey = '_mutex_lock';
export const constructMutexLockFirstPart = () => [mutexLockKey];
export const constructMutexLockClaimKey = (lockId: string, timestamp: number) =>
constructClaimKey(constructMutexLockFirstPart(), [timestamp, lockId]);
export const constructMutexLockClaim = ({
fee,
keys,
locked,
lockId,
timestamp,
processId,
}: {
lockId: string;
timestamp: number;
locked: boolean;
keys: LockedKey[];
fee: bigint;
processId: string;
}) =>
constructClaim(
constructMutexLockClaimKey(lockId, timestamp),
{ locked, keys, processId } satisfies MutexLockState,
toHex(fee)
);
export const constructMutexLockClaimStore = ({
fee,
keys,
lockId,
timestamp,
processId,
locked,
}: {
fee: bigint;
keys: LockedKey[];
lockId: string;
timestamp: number;
processId: string;
locked: boolean;
}) => constructStore(constructMutexLockClaim({ fee, keys, locked, lockId, timestamp, processId }));
export const constructMutexLockClaimRangeRead = (issuer: ContractIssuer): GRead<CwebRead> =>
constructRangeRead(issuer, constructMutexLockFirstPart(), {}, rangeReadLimit);
// export const constructMutexLockClaimTake = (lockId: string, timestamp: number) =>
// constructTake(constructMutexLockClaimKey(lockId, timestamp));

View File

@ -1,33 +0,0 @@
import {
execLockMethodName,
execOps,
execOpsMethodName,
lockMethodName,
mutexExecLock,
mutexLock,
mutexUnlock,
notifyLock,
notifyLockMethodName,
preReadExecTakeOpsMethodName,
saveExecOpResult,
saveExecOpResultMethodName,
unlockMethodName,
waitMethodName,
} from './methods';
import { preReadExecTakeOps } from './methods/preReadExecTakeOps';
import { wait } from './methods/wait';
export * from './claims';
export * from './types';
export * from './calls';
export const mutexMethods = {
[execLockMethodName]: mutexExecLock,
[lockMethodName]: mutexLock,
[unlockMethodName]: mutexUnlock,
[execOpsMethodName]: execOps,
[preReadExecTakeOpsMethodName]: preReadExecTakeOps,
[saveExecOpResultMethodName]: saveExecOpResult,
[waitMethodName]: wait,
[notifyLockMethodName]: notifyLock,
};

View File

@ -1,81 +0,0 @@
import {
constructClaim,
constructContinueTx,
constructContractRef,
constructStore,
constructTake,
Context,
extractContractArgs,
extractRead,
passCwebFrom,
} from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer } from 'lib/onchain';
import { toHex, TypedClaim } from 'lib/shared';
import { lockFee } from '../settings';
import { MutexLockState, MutexWaitNotifyArgs } from '../types';
import { isMatchLockKeys } from '../utils';
import { waitMethodName } from './names';
export const mutexExecLock = (context: Context) => {
const { availableCweb } = getCallParameters(context);
const issuer = getContractIssuer(context);
const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map(
({ content }) => content as TypedClaim<MutexLockState>
);
if (!lockQueue) {
throw new Error('No lock queue found');
}
const alreadyLockedKeys = lockQueue.filter(({ body }) => body.locked).map(({ key }) => key);
const lockCandidate = lockQueue.find(
({ body, key }) => !body.locked && !alreadyLockedKeys.some((lockedKey) => isMatchLockKeys(lockedKey, key))
);
if (!lockCandidate) {
return [];
}
return [
constructContinueTx(
context,
[
passCwebFrom(issuer, availableCweb),
constructTake(lockCandidate.key),
constructStore(
constructClaim(
lockCandidate.key,
{ ...lockCandidate.body, locked: true },
toHex(BigInt(lockCandidate.fees_stored) - lockFee)
)
),
],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: waitMethodName,
methodArgs: [
0,
{
timestamp: (lockCandidate.key.second_part as [number, string])[0],
lockId: (lockCandidate.key.second_part as [number, string])[1],
},
] satisfies MutexWaitNotifyArgs,
},
contractInfo: {
providedCweb: availableCweb + BigInt(lockCandidate.fees_stored) - 1000n,
authenticated: null,
},
contractArgs: [],
},
},
]
),
];
};

View File

@ -1,121 +0,0 @@
import {
constructContinueTx,
constructContractRef,
constructRead,
Context,
extractContractArgs,
extractRead,
PreparedOperation,
} from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer, getMethodArguments } from 'lib/onchain';
import { TypedClaim } from 'lib/shared';
import { constructMutexExecOpsClaimStore } from '../claims';
import { MutexLockState, MutexExecOpArgs, MutexSaveExecOpResultArgs, MutexPreReadTakeOpsArgs } from '../types';
import { isMatchLockKeys } from '../utils';
import { saveExecOpResultMethodName } from './names';
export const execOps = (context: Context) => {
const { availableCweb } = getCallParameters(context);
const issuer = getContractIssuer(context);
const [ops, processId, execId] = getMethodArguments<MutexExecOpArgs>(context);
const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map(
({ content }) => content as TypedClaim<MutexLockState>
);
let availableOps: PreparedOperation[] = [];
const unavailableIndexes: number[] = [];
console.log('extractContractArgs(context.tx) >>>', JSON.stringify(extractContractArgs(context.tx)));
console.log('ops >>>', JSON.stringify(ops));
console.log('lockQueue >>>', JSON.stringify(lockQueue));
if (!lockQueue) {
availableOps = ops;
} else {
ops.forEach((op, i) => {
const claimKey = 'StoreOp' in op ? op.StoreOp.key : op.TakeOp.key;
const isLockedByOtherProcess = lockQueue.some(
({ body }) =>
body.locked && body.processId !== processId && body.keys.some((key) => isMatchLockKeys(key, claimKey))
);
if (isLockedByOtherProcess) {
unavailableIndexes.push(i);
} else {
availableOps.push(op);
}
});
}
if (availableOps.length === 0) {
return execId
? [
constructContinueTx(context, [
constructMutexExecOpsClaimStore(execId, new Array(ops.length).fill({ ok: false })),
]),
]
: [];
}
if (!execId) {
return [constructContinueTx(context, availableOps)];
}
const preReadTakeOps = availableOps.filter((op) => 'TakeOp' in op).map((op) => constructRead(issuer, op.TakeOp.key));
if (preReadTakeOps.length > 0) {
const fee = 700n + BigInt(preReadTakeOps.length) * 100n;
return [
constructContinueTx(
context,
[],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: saveExecOpResultMethodName,
methodArgs: [execId, unavailableIndexes, availableOps] satisfies MutexPreReadTakeOpsArgs,
},
contractInfo: {
providedCweb: availableCweb - fee,
authenticated: null,
},
contractArgs: preReadTakeOps,
},
},
]
),
];
}
const fee = 700n + BigInt(availableOps.length) * 100n;
return [
constructContinueTx(
context,
[],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: saveExecOpResultMethodName,
methodArgs: [execId, unavailableIndexes] satisfies MutexSaveExecOpResultArgs,
},
contractInfo: {
providedCweb: availableCweb - fee,
authenticated: null,
},
contractArgs: availableOps,
},
},
]
),
];
};

View File

@ -1,7 +0,0 @@
export * from './execLock';
export * from './lock';
export * from './names';
export * from './notifyLock';
export * from './execOps';
export * from './unlock';
export * from './saveExecOpResult';

View File

@ -1,34 +0,0 @@
import { constructContinueTx, constructContractRef, Context } from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer } from 'lib/onchain';
import { constructMutexLockClaimRangeRead } from '../claims';
import { execLockMethodName } from './names';
export const mutexLock = (context: Context) => {
const { availableCweb } = getCallParameters(context);
const issuer = getContractIssuer(context);
return [
constructContinueTx(
context,
[],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: execLockMethodName,
methodArgs: [],
},
contractInfo: {
providedCweb: availableCweb - 700n,
authenticated: null,
},
contractArgs: [constructMutexLockClaimRangeRead(issuer)],
},
},
]
),
];
};

View File

@ -1,8 +0,0 @@
export const lockMethodName = '_mutex_lock';
export const execLockMethodName = '_mutex_execLock';
export const unlockMethodName = '_mutex_unlock';
export const notifyLockMethodName = '_mutex_notify_lock';
export const execOpsMethodName = '_mutex_execOps';
export const saveExecOpResultMethodName = '_mutex_save_exec_op_result';
export const preReadExecTakeOpsMethodName = '_mutex_pre_read_exec_take_ops';
export const waitMethodName = '_mutex_wait';

View File

@ -1,11 +0,0 @@
import { constructContinueTx, Context } from '@coinweb/contract-kit';
import { getMethodArguments } from 'lib/onchain';
import { constructMutexBlockLockClaimStore } from '../claims/mutexBlockLock';
import { MutexNotifyLockArgs } from '../types';
export const notifyLock = (context: Context) => {
const [lockInfo] = getMethodArguments<MutexNotifyLockArgs>(context);
return [constructContinueTx(context, [constructMutexBlockLockClaimStore(lockInfo)])];
};

View File

@ -1,49 +0,0 @@
import {
constructContinueTx,
constructContractRef,
Context,
extractContractArgs,
extractRead,
passCwebFrom,
} from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer, getMethodArguments } from 'lib/onchain';
import { MutexPreReadTakeOpsArgs, MutexSaveExecOpResultArgs } from '../types';
import { saveExecOpResultMethodName } from './names';
export const preReadExecTakeOps = (context: Context) => {
const { availableCweb } = getCallParameters(context);
const issuer = getContractIssuer(context);
const [execId, unavailableIndexes, ops] = getMethodArguments<MutexPreReadTakeOpsArgs>(context);
const storedCweb = extractContractArgs(context.tx)
.map((op) => BigInt(extractRead(op)?.[0]?.content.fees_stored ?? 0))
.reduce((total, op) => total + op, 0n);
const fee = 700n + BigInt(ops.length) * 100n;
return [
constructContinueTx(
context,
[passCwebFrom(issuer, availableCweb)],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: saveExecOpResultMethodName,
methodArgs: [execId, unavailableIndexes] satisfies MutexSaveExecOpResultArgs,
},
contractInfo: {
providedCweb: availableCweb + storedCweb - fee,
authenticated: null,
},
contractArgs: ops,
},
},
]
),
];
};

View File

@ -1,33 +0,0 @@
import { constructContinueTx, Context, extractContractArgs } from '@coinweb/contract-kit';
import { getCallParameters, getMethodArguments } from 'lib/onchain';
import { constructMutexExecOpsClaimStore } from '../claims';
import { MutexExecOpsResult, MutexSaveExecOpResultArgs } from '../types';
export const saveExecOpResult = (context: Context) => {
const { availableCweb } = getCallParameters(context);
const [execId, unavailableIndexes] = getMethodArguments<MutexSaveExecOpResultArgs>(context);
const resolvedOps = extractContractArgs(context.tx);
const result = new Array(resolvedOps.length + unavailableIndexes.length)
.fill({ ok: false, error: 'Resource is busy' })
.map((value: MutexExecOpsResult[number], i) => {
if (unavailableIndexes.includes(i)) {
return value;
}
const resolvedOp = resolvedOps.shift();
if (!resolvedOp) {
throw new Error('An error occurred while saving the exec op result');
}
return {
ok: true as const,
resolved: resolvedOp,
};
});
return [constructContinueTx(context, [constructMutexExecOpsClaimStore(execId, result, availableCweb - 200n)])];
};

View File

@ -1,42 +0,0 @@
import { constructContinueTx, constructContractRef, constructTake, Context, passCwebFrom } from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer, getMethodArguments } from 'lib/onchain';
import { constructMutexBlockUnlockClaimStore, constructMutexLockClaimKey } from '../claims';
import { lockFee } from '../settings';
import { MutexUnlockArgs } from '../types';
import { lockMethodName } from './names';
export const mutexUnlock = (context: Context) => {
const { availableCweb } = getCallParameters(context);
const issuer = getContractIssuer(context);
const [lockId, timestamp] = getMethodArguments<MutexUnlockArgs>(context);
return [
constructContinueTx(
context,
[
passCwebFrom(issuer, availableCweb),
constructTake(constructMutexLockClaimKey(lockId, timestamp)),
constructMutexBlockUnlockClaimStore(lockId),
],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: lockMethodName,
methodArgs: [],
},
contractInfo: {
providedCweb: lockFee,
authenticated: null,
},
contractArgs: [],
},
},
]
),
];
};

View File

@ -1,62 +0,0 @@
import { constructContinueTx, constructContractRef, Context } from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer, getMethodArguments } from 'lib/onchain';
import { waitSteps } from '../settings';
import { MutexNotifyLockArgs, MutexWaitNotifyArgs } from '../types';
import { notifyLockMethodName, waitMethodName } from './names';
export const wait = (context: Context) => {
const issuer = getContractIssuer(context);
const [step, lockInfo] = getMethodArguments<MutexWaitNotifyArgs>(context);
const { availableCweb } = getCallParameters(context);
//Including the notifyLock method as step
if (step + 1 < waitSteps) {
return [
constructContinueTx(
context,
[],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: waitMethodName,
methodArgs: [step + 1, lockInfo] satisfies MutexWaitNotifyArgs,
},
contractInfo: {
providedCweb: availableCweb - 800n,
authenticated: null,
},
contractArgs: [],
},
},
]
),
];
}
return [
constructContinueTx(
context,
[],
[
{
callInfo: {
ref: constructContractRef(issuer, []),
methodInfo: {
methodName: notifyLockMethodName,
methodArgs: [lockInfo] satisfies MutexNotifyLockArgs,
},
contractInfo: {
providedCweb: availableCweb - 800n,
authenticated: null,
},
contractArgs: [],
},
},
]
),
];
};

View File

@ -1,6 +0,0 @@
export const lockFee = 5000n;
export const unlockFee = 1000n + lockFee;
export const rangeReadLimit = 10000;
export const waitSteps = 2;

View File

@ -1,38 +0,0 @@
import { ClaimKey, CwebStore, GStore, GTake, PreparedOperation, ResolvedOperation } from '@coinweb/contract-kit';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
export type LockedKey = Omit<ClaimKey, 'second_part'> & Partial<Pick<ClaimKey, 'second_part'>>;
export type MutexLockState = {
locked: boolean;
keys: LockedKey[];
processId: string;
};
export type MutexNotifyLockState = {
timestamp: number;
lockId: string;
};
export type MutexExecOpsResult = (
| {
ok: true;
resolved: ResolvedOperation;
}
| {
ok: false;
error: string;
}
)[];
export type MutexUnlockArgs = [lockId: string, timestamp: number, notify?: boolean];
export type MutexExecOpArgs = [ops: (GTake<CwebTake> | GStore<CwebStore>)[], processId: string, execId?: string];
export type MutexWaitNotifyArgs = [step: number, lockInfo: MutexNotifyLockState];
export type MutexNotifyLockArgs = [lockInfo: MutexNotifyLockState];
export type MutexPreReadTakeOpsArgs = [execId: string, unavailableIndexes: number[], ops: PreparedOperation[]];
export type MutexSaveExecOpResultArgs = [execId: string, unavailableIndexes: number[]];

View File

@ -1 +0,0 @@
export * from './isMatchLockKeys';

View File

@ -1,9 +0,0 @@
import { LockedKey } from '../types';
export const isMatchLockKeys = (lockKey1: LockedKey, lockKey2: LockedKey) => {
if (!lockKey1.second_part || !lockKey2.second_part) {
return lockKey1.first_part === lockKey2.first_part;
}
return lockKey1.first_part === lockKey2.first_part && lockKey1.second_part === lockKey2.second_part;
};

View File

@ -0,0 +1,13 @@
import { PreparedOperation } from '@coinweb/contract-kit';
const awaitedOps: PreparedOperation[] = [];
export const pushAwaitedOp = (op: PreparedOperation | PreparedOperation[]) => {
if (Array.isArray(op)) {
awaitedOps.push(...op);
} else {
awaitedOps.push(op);
}
};
export const getAwaitedOps = () => awaitedOps;

View File

@ -0,0 +1,5 @@
export const blockOp = () => {
return new Promise<void>((resolve) => {
resolve();
});
};

View File

@ -1,5 +1,6 @@
export * from './awaited';
export * from './block'; export * from './block';
export * from './rangeRead';
export * from './read'; export * from './read';
export * from './resolved';
export * from './store'; export * from './store';
export * from './take'; export * from './take';

View File

@ -0,0 +1,28 @@
import { Claim, ClaimKey, constructRead, extractRead, isResolvedRead } from '@coinweb/contract-kit';
import { TypedClaim } from '../../types';
import { context } from '../context';
import { signal } from '../signal';
import { pushAwaitedOp } from './awaited';
import { shiftResolvedOp } from './resolved';
export const readOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey): TClaim | null => {
const resolvedOp = shiftResolvedOp();
if (!resolvedOp.isOp) {
pushAwaitedOp(constructRead(context.issuer, key));
// @ts-expect-error
return signal;
} else {
if (!resolvedOp.op) {
return resolvedOp.op;
}
if (isResolvedRead(resolvedOp.op)) {
return (extractRead(resolvedOp.op)?.[0].content as TClaim | undefined) ?? null;
}
throw new Error('Read operation not found');
}
};

View File

@ -0,0 +1,25 @@
import { ResolvedOperation } from '@coinweb/contract-kit';
const resolvedOps: ResolvedOperation[] = [];
export const pushResolvedOp = (op: ResolvedOperation | ResolvedOperation[]) => {
if (Array.isArray(op)) {
resolvedOps.push(...op);
} else {
resolvedOps.push(op);
}
};
export const shiftResolvedOp = () =>
({
isOp: resolvedOps.length > 0,
op: resolvedOps.shift(),
}) as
| {
isOp: true;
op: ResolvedOperation | null;
}
| {
isOp: false;
op: undefined;
};

View File

@ -0,0 +1,26 @@
import { Claim, constructStore, isResolvedStore } from '@coinweb/contract-kit';
import { signal } from '../signal';
import { pushAwaitedOp } from './awaited';
import { shiftResolvedOp } from './resolved';
export const storeOp = (claim: Claim): Claim | null => {
const resolvedOp = shiftResolvedOp();
if (!resolvedOp.isOp) {
pushAwaitedOp(constructStore(claim));
// @ts-expect-error
return signal;
} else {
if (!resolvedOp.op) {
return resolvedOp.op;
}
if (isResolvedStore(resolvedOp.op)) {
return resolvedOp.op.StoreOp;
}
throw new Error('Store operation not found');
}
};

View File

@ -0,0 +1,27 @@
import { constructTake, extractTake, isResolvedTake, Claim, ClaimKey } from '@coinweb/contract-kit';
import { TypedClaim } from '../../types';
import { signal } from '../signal';
import { pushAwaitedOp } from './awaited';
import { shiftResolvedOp } from './resolved';
export const takeOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey): TClaim | null => {
const resolvedOp = shiftResolvedOp();
if (!resolvedOp.isOp) {
pushAwaitedOp(constructTake(key));
// @ts-expect-error
return signal;
} else {
if (!resolvedOp.op) {
return resolvedOp.op;
}
if (isResolvedTake(resolvedOp.op)) {
return extractTake(resolvedOp.op) as TClaim;
}
throw new Error('Take operation not found');
}
};

View File

@ -1,17 +0,0 @@
import { PreparedOp, Task } from '../../types';
const awaitedTasks: Task[] = [];
export const pushAwaitedTask = (op: PreparedOp) => {
awaitedTasks.push({ op, batchId: -1 });
};
export const getAwaitedTasks = () => [...awaitedTasks];
export const markTaskBatch = (count: number, batchId: number) => {
for (let i = 1; i <= count; i++) {
awaitedTasks.at(i * -1)!.batchId = batchId;
}
};
export const getAwaitedTasksCount = () => awaitedTasks.length;

View File

@ -1,54 +0,0 @@
import { ResolvedOp } from '../../types';
import { pushResolvedOp } from './resolvedOps';
let abortExecution: ((isFullyExecuted: boolean) => void) | null = null;
export const stopExecution = (isFullyExecuted = false) => {
console.log('stopExecution');
abortExecution?.(isFullyExecuted);
};
type ExecTask = () => Promise<unknown>;
type Exec = { task: ExecTask; ops: ResolvedOp[] };
const execQueue: Exec[] = [];
export const setNextExec = (task: ExecTask, ops: ResolvedOp[]) => {
return new Promise((resolve, reject) => {
execQueue.push({
task: () => task().then(resolve, reject),
ops,
});
});
};
export const execLoop = async (): Promise<boolean> => {
const nextExec = execQueue.pop();
if (nextExec) {
const execution = new Promise<boolean>((resolve, reject) => {
abortExecution = resolve;
pushResolvedOp(nextExec.ops);
nextExec.task().then(
() => {
resolve(true);
},
(error) => {
console.log(error);
reject(error);
}
);
});
const isFullyExecuted = await execution;
if (isFullyExecuted) {
return true;
}
return execLoop();
}
return false;
};

View File

@ -1,3 +0,0 @@
export * from './awaitedTasks';
export * from './execLoop';
export * from './resolvedOps';

View File

@ -1,61 +0,0 @@
import { ResolvedOp } from '../../types';
const resolvedOps: ResolvedOp[] = [];
let usedOps: ResolvedOp[] = [];
let isSavingUsed = false;
let isFreezed = false;
export const pushResolvedOp = (op: ResolvedOp | ResolvedOp[]) => {
if (isFreezed) {
return;
}
if (Array.isArray(op)) {
resolvedOps.push(...op);
} else {
resolvedOps.push(op);
}
};
export const shiftResolvedOp = () => {
const result = {
isOp: resolvedOps.length > 0 && !isFreezed,
op: isFreezed ? undefined : resolvedOps.shift(),
} as
| {
isOp: true;
op: ResolvedOp | null;
}
| {
isOp: false;
op: undefined;
};
if (isSavingUsed && result.op) {
usedOps.push(result.op);
}
console.log('shiftResolvedOp: ', JSON.stringify(result));
return result;
};
export const getUsedOps = () => usedOps;
export const startSavingUsedOps = () => {
usedOps = [];
isSavingUsed = true;
};
export const stopSavingUsedOps = () => {
isSavingUsed = false;
};
export const freezeResolvedOps = () => {
isFreezed = true;
};
export const unfreezeResolvedOps = () => {
isFreezed = false;
};

View File

@ -0,0 +1 @@
export const signal = Symbol('signal');

View File

@ -1,2 +0,0 @@
export * from './opTypeGuards';
export * from './uuid';

View File

@ -1,97 +0,0 @@
import {
CwebStore,
isResolvedBlock,
isResolvedRead,
isResolvedStore,
isResolvedTake,
ResolvedOperation,
ResolvedRead,
ResolvedTake,
} from '@coinweb/contract-kit';
import { CwebBlock } from '@coinweb/contract-kit/dist/esm/operations/block';
import { CwebCallRefResolved, isResolvedCall } from '@coinweb/contract-kit/dist/esm/operations/call';
import { ResolvedBlock } from '@coinweb/contract-kit/dist/types/operations/block';
import { GBlock, GCall, GRead, GStore, GTake } from '@coinweb/contract-kit/dist/types/operations/generics';
import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take';
import {
PreparedExecOp,
PreparedExtendedStoreOp,
PreparedLockOp,
PreparedOp,
PreparedUnlockOp,
ResolvedChildOp,
ResolvedExecOp,
ResolvedLockOp,
ResolvedOp,
ResolvedSlotOp,
ResolvedUnlockOp,
} from '../../types';
export const isResolvedSlotOp = (op?: ResolvedOp | null): op is ResolvedSlotOp => {
return !!(op && 'SlotOp' in op);
};
export const isResolvedExecOp = (op?: ResolvedOp | null): op is ResolvedExecOp => {
return !!(op && 'ExecOp' in op);
};
export const isResolvedChildOp = (op?: ResolvedOp | null): op is ResolvedChildOp => {
return !!(op && 'ChildOp' in op);
};
export const isResolvedLockOp = (op?: ResolvedOp | null): op is ResolvedLockOp => {
return !!(op && 'LockOp' in op);
};
export const isResolvedUnlockOp = (op?: ResolvedOp | null): op is ResolvedUnlockOp => {
return !!(op && 'UnlockOp' in op);
};
export const isResolvedBlockOp = (op?: ResolvedOp | null): op is GBlock<ResolvedBlock> => {
return isResolvedBlock(op as ResolvedOperation); //TODO: Fix contract-kit types
};
export const isResolvedStoreOp = (op?: ResolvedOp | null): op is GStore<CwebStore> => {
return isResolvedStore(op as ResolvedOperation); //TODO: Fix contract-kit types
};
export const isResolvedCallOp = (op?: ResolvedOp | null): op is GCall<CwebCallRefResolved> => {
return isResolvedCall(op as ResolvedOperation); //TODO: Fix contract-kit types
};
export const isResolvedTakeOp = (op?: ResolvedOp | null): op is GTake<ResolvedTake> => {
return isResolvedTake(op as ResolvedOperation); //TODO: Fix contract-kit types
};
export const isResolvedReadOp = (op?: ResolvedOp | null): op is GRead<ResolvedRead> => {
return isResolvedRead(op as ResolvedOperation); //TODO: Fix contract-kit types
};
export const isPreparedExecOp = (op?: PreparedOp | null): op is PreparedExecOp => {
return !!(op && 'ExecOp' in op);
};
export const isPreparedBlockOp = (op?: PreparedOp | null): op is GBlock<CwebBlock> => {
return !!(op && 'BlockOp' in op);
};
export const isPreparedLockOp = (op?: PreparedOp | null): op is PreparedLockOp => {
return !!(op && 'LockOp' in op);
};
export const isPreparedUnlockOp = (op?: PreparedOp | null): op is PreparedUnlockOp => {
return !!(op && 'UnlockOp' in op);
};
export const isPreparedStoreOp = (op?: PreparedOp | null): op is GStore<CwebStore> => {
return !!(op && 'StoreOp' in op);
};
export const isPreparedExtendedStoreOp = (op?: PreparedOp | null): op is PreparedExtendedStoreOp => {
return !!(op && 'StoreOp' in op);
};
export const isPreparedTakeOp = (op?: PreparedOp | null): op is GTake<CwebTake> => {
return !!(op && 'TakeOp' in op);
};

View File

@ -1,11 +0,0 @@
import { context } from '../context';
let next = 0n;
export const uuid = () => {
const id = (BigInt(`0x${context.thisId}`) * 3n + BigInt(`0x${context.parentTxId}`) + next).toString(16).slice(-32);
next += 1n;
return id;
};

View File

@ -1,15 +1,4 @@
import { import { Claim, ClaimKey, OrdJson } from '@coinweb/contract-kit';
Claim,
ClaimKey,
CwebStore,
GStore,
OrdJson,
PreparedOperation,
ResolvedOperation,
User,
} from '@coinweb/contract-kit';
import { LockedKey } from './onchain/mutex';
export type HexBigInt = `0x${string}`; export type HexBigInt = `0x${string}`;
@ -18,91 +7,3 @@ export type TypedClaim<TBody extends OrdJson = OrdJson, TKey extends ClaimKey =
fees_stored: HexBigInt; fees_stored: HexBigInt;
key: TKey; key: TKey;
}; };
export type ResolvedSlotOp = {
SlotOp:
| {
ok: false;
error: string;
}
| {
ok: true;
};
};
export type ResolvedExecOp = {
ExecOp: {
id: string;
};
};
export type ResolvedChildOp = {
ChildOp: {
ops: ResolvedOp[];
};
};
export type ResolvedLockOp = {
LockOp: {
lockId: string;
timestamp: number;
};
};
export type ResolvedUnlockOp = {
UnlockOp: 0;
};
export type ResolvedOp =
| ResolvedOperation
| ResolvedSlotOp
| ResolvedExecOp
| ResolvedChildOp
| ResolvedLockOp
| ResolvedUnlockOp;
export type PreparedExecOp = {
ExecOp: {
id: string;
};
};
export type PreparedLockOp = {
LockOp: {
lockId: string;
keys: LockedKey[];
timestamp: number;
};
};
export type PreparedUnlockOp = {
UnlockOp: {
lockId: string;
timestamp: number;
};
};
export type PreparedExtendedStoreOp = GStore<CwebStore> & { providedCweb?: bigint };
export type PreparedOp =
| PreparedOperation
| PreparedExecOp
| PreparedLockOp
| PreparedUnlockOp
| PreparedExtendedStoreOp;
export type Task = {
op: PreparedOp;
batchId: number;
};
export type ExecutorMethodArgs = [
initialArgs?: unknown[],
resolvedOps?: ResolvedOp[],
caller?: User,
thisId?: string,
parentId?: string,
shouldSaveResult?: boolean,
takenFundsIds?: string[],
execOpsIndexes?: number[],
];

View File

@ -9,5 +9,5 @@
"esModuleInterop": true, "esModuleInterop": true,
"types": ["vitest/globals"] "types": ["vitest/globals"]
}, },
"include": ["**/*.ts", "vitest.*.ts", "__tests__", "scripts/publish.js", "src/onchain/utils", "src/onchain/executor/constructTx"] "include": ["**/*.ts", "vitest.*.ts", "__tests__", "scripts/publish.js"]
} }

View File

@ -1,4 +1,4 @@
VITE_API_URL='https://api-cloud.coinweb.io/wallet' VITE_API_URL='https://api-cloud.coinweb.io/wallet'
VITE_EXPLORER_URL='https://explorer.coinweb.io' VITE_EXPLORER_URL='https://explorer.coinweb.io'
VITE_CONTRACT_ADDRESS="0xc599810e4861b7b1ae25695d58eeef10556fc84f87f72fafa31c0921e86e92be" VITE_CONTRACT_ADDRESS="0xe1e81e77f901f630ab760a84b6bb84c96c3a4ec996a287efac2c8502e94a287f"

View File

@ -5,13 +5,13 @@ import { QRCodeSVG } from 'qrcode.react';
export const App = () => { export const App = () => {
const [word, setWord] = useState(''); const [word, setWord] = useState('');
const [qrCode, setQrCode] = useState('');
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => { const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setQrCode(constructAddWordUiCommand({ word, contractId: import.meta.env.VITE_CONTRACT_ADDRESS ?? '' }));
}; };
const qrCode = constructAddWordUiCommand({ word, contractId: import.meta.env.VITE_CONTRACT_ADDRESS ?? '' });
return ( return (
<div> <div>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
@ -28,24 +28,28 @@ export const App = () => {
value={word} value={word}
onChange={(e) => setWord(e.target.value)} onChange={(e) => setWord(e.target.value)}
/> />
<button
type="submit"
className="w-full self-end rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:outline-none focus:ring-4 focus:ring-blue-300 sm:w-auto dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Submit
</button>
</div> </div>
</form> </form>
<div className="flex w-full justify-center py-4"> <div className="flex w-full justify-center py-4">
{word && ( <QRCodeSVG
<QRCodeSVG onClick={() => {
onClick={() => { navigator.clipboard.writeText(qrCode).catch((err) => {
navigator.clipboard.writeText(qrCode).catch((err) => { console.error(err);
console.error(err); });
}); }}
}} value={qrCode}
value={qrCode} size={320}
size={320} level="L"
level="L" className="hover:cursor-pointer"
className="hover:cursor-pointer" bgColor="transparent"
bgColor="transparent" fgColor="black"
fgColor="black" />
/>
)}
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,220 @@
import {
type GqlClaimFilter,
type GqlIssuedClaim,
type NetworkName,
NodeConnection,
connect_to_node,
fetch_claims,
} from '@coinweb/wallet-lib';
import { type Pagination } from 'dex-app.cm';
import { Currency, CONTRACT_PARAMS } from '../constants';
type IndexClaimKey = {
first_part: unknown[];
second_part: [number | string, ...unknown[]];
};
let cwebWalletNode: NodeConnection | null = null;
const getCwebWalletNode = () => {
if (!cwebWalletNode) {
cwebWalletNode = connect_to_node(import.meta.env.VITE_API_URL as string);
}
return cwebWalletNode;
};
type ContractClient = {
fetchClaims: <T>(
firstPart: NonNullable<unknown>,
secondPart: T,
range?: {
start: T extends null ? unknown : T | null;
end: T extends null ? unknown : T | null;
},
pagination?: Pagination
) => Promise<GqlIssuedClaim[]>;
updateClient: () => void;
contractAddress: string;
networkName: NetworkName;
};
const createClient = (contractAddress: string, networkName: NetworkName) => {
const createClaimFilter = <T extends NonNullable<unknown>>(
firstPart: NonNullable<unknown>,
secondPart: T | null = null,
range?: {
start: T;
end: T;
}
): GqlClaimFilter => {
return {
issuer: { FromSmartContract: contractAddress },
keyFirstPart: firstPart,
keySecondPart: secondPart,
startsAtKeySecondPart: range?.start ?? null,
endsAtKeySecondPart: range?.end ?? null,
};
};
const fetchClaims = async <T extends NonNullable<unknown>>(
firstPart: NonNullable<unknown>,
secondPart: T | null = null,
range?: {
start: T;
end: T;
},
pagination?: Pagination
): Promise<GqlIssuedClaim[]> => {
if (!contractAddress || !networkName) {
throw new Error('Network parameters are not defined');
}
const filter = createClaimFilter(firstPart, secondPart, range);
const loadClaims = async () => {
const rawData = await fetch_claims(getCwebWalletNode(), [filter], networkName, true);
const sortedData = rawData.sort((a, b) => {
let aValue: string | number | bigint = (a.content.key as IndexClaimKey).second_part[0];
let bValue: string | number | bigint = (b.content.key as IndexClaimKey).second_part[0];
if (typeof aValue === 'string' && typeof bValue === 'string') {
if (aValue.startsWith('0x')) {
aValue = BigInt(aValue);
} else {
aValue = BigInt(`0x${aValue}`);
}
if (bValue.startsWith('0x')) {
bValue = BigInt(bValue);
} else {
bValue = BigInt(`0x${bValue}`);
}
}
if (aValue < bValue) {
return 1;
} else if ((a.content.key as IndexClaimKey).second_part[0] > (b.content.key as IndexClaimKey).second_part[0]) {
return -1;
} else {
return 0;
}
});
if (pagination) {
if (pagination.offset < 0) {
return sortedData.slice(0, pagination.limit + pagination.offset);
}
return sortedData.slice(pagination.offset, pagination.offset + pagination.limit);
}
return sortedData;
};
const claimsRequest = loadClaims();
const result = await claimsRequest;
return result;
};
const updateClient = () => {};
return {
fetchClaims,
updateClient,
contractAddress,
networkName,
} as ContractClient;
};
export const baseClients: {
[key in Exclude<Currency, Currency.CWEB>]: ContractClient;
} = {
[Currency.ETH]: createClient(
CONTRACT_PARAMS[Currency.ETH].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.ETH].NETWORK_NAME
),
[Currency.BNB]: createClient(
CONTRACT_PARAMS[Currency.BNB].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.BNB].NETWORK_NAME
),
[Currency.USDT_ETH]: createClient(
CONTRACT_PARAMS[Currency.USDT_ETH].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.USDT_ETH].NETWORK_NAME
),
[Currency.USDT_BNB]: createClient(
CONTRACT_PARAMS[Currency.USDT_BNB].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.USDT_BNB].NETWORK_NAME
),
[Currency.BTC]: createClient(
CONTRACT_PARAMS[Currency.BTC].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.BTC].NETWORK_NAME
),
[Currency.LTC]: createClient(
CONTRACT_PARAMS[Currency.BTC].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.BTC].NETWORK_NAME
),
[Currency.EGLD]: createClient(
CONTRACT_PARAMS[Currency.BTC].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.BTC].NETWORK_NAME
),
[Currency.TRX]: createClient(
CONTRACT_PARAMS[Currency.TRX].L2_CONTRACT_ADDRESS_BASE,
CONTRACT_PARAMS[Currency.TRX].NETWORK_NAME
),
};
export const makerClients: {
[key in Exclude<Currency, Currency.CWEB>]: ContractClient;
} = {
[Currency.ETH]: createClient(
CONTRACT_PARAMS[Currency.ETH].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.ETH].NETWORK_NAME
),
[Currency.BNB]: createClient(
CONTRACT_PARAMS[Currency.BNB].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.BNB].NETWORK_NAME
),
[Currency.USDT_ETH]: createClient(
CONTRACT_PARAMS[Currency.USDT_ETH].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.USDT_ETH].NETWORK_NAME
),
[Currency.USDT_BNB]: createClient(
CONTRACT_PARAMS[Currency.USDT_BNB].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.USDT_BNB].NETWORK_NAME
),
[Currency.BTC]: createClient(
CONTRACT_PARAMS[Currency.BTC].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.BTC].NETWORK_NAME
),
[Currency.LTC]: createClient(
CONTRACT_PARAMS[Currency.BTC].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.BTC].NETWORK_NAME
),
[Currency.EGLD]: createClient(
CONTRACT_PARAMS[Currency.BTC].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.BTC].NETWORK_NAME
),
[Currency.TRX]: createClient(
CONTRACT_PARAMS[Currency.TRX].L2_CONTRACT_ADDRESS_MAKER,
CONTRACT_PARAMS[Currency.TRX].NETWORK_NAME
),
};
export const clients: Record<string, ContractClient> = new Proxy(
Object.fromEntries(
[...Object.values(baseClients), ...Object.values(makerClients)].map((client) => [client.contractAddress, client])
),
{
get(clients, contractId: string) {
if (!(contractId in clients)) {
throw new Error(`Client for contractId=${String(contractId)} not found`);
}
return clients[contractId];
},
}
);

View File

@ -0,0 +1,17 @@
import { type Client, getAllActiveOrders, type Pagination } from 'market-maker.cm';
import { Currency } from '../constants';
import { makerClients } from './client';
export const getActiveMarketOrders = async (currency: Currency, pagination?: Pagination) => {
if (currency === Currency.CWEB) {
throw new Error(`Cannot get active orders for: ${currency}`);
}
const client: Client = {
fetchClaims: (...params) => makerClients[currency].fetchClaims(...params, pagination),
};
return getAllActiveOrders(client);
};

View File

@ -0,0 +1,36 @@
import { type Client, type Pagination, getOrderById, getActiveOrderIds, type OrderData } from 'dex-app.cm';
// import { decimalsForCurrency } from '@/utils';
import { Currency } from '../constants';
import { baseClients } from './client';
export const getAllActivePositions = async (currency: Currency, pagination?: Pagination): Promise<OrderData[]> => {
if (currency === Currency.CWEB) {
throw new Error(`Cannot get active positions for: ${currency}`);
}
const client: Client = {
fetchClaims: (...params) => baseClients[currency].fetchClaims(...params, pagination),
};
const ids = await getActiveOrderIds(client);
return await Promise.all(ids.map((id: string) => getOrderById(client, id)));
/*
return (
(await Promise.all(ids.map((id: string) => getOrderById(client, id))))
// TODO: Remove this filter when we have a proper solution for reversed orders
.filter((position) => {
if (['btcswap'].includes(import.meta.env.MODE)) {
return position.l1Amount === position.minL1Amount; // order.isOwnerless
} else {
return (
position.funds >= 1n * 10n ** BigInt(decimalsForCurrency(Currency.CWEB)) &&
position.l1Amount !== position.minL1Amount
);
}
})
);
*/
};

View File

@ -0,0 +1,21 @@
import { type PubKey } from '@coinweb/wallet-lib';
import { type Client, getAllOwnClaims, type Pagination } from 'market-maker.cm';
import { Currency } from '../constants';
import { makerClients } from './client';
export const getAllMarketClaim = async (currency: Currency, pubKey: PubKey, pagination?: Pagination) => {
if (currency === Currency.CWEB) {
throw new Error(`Cannot get all market claim for: ${currency}`);
}
const client: Client = {
fetchClaims: (...params) => makerClients[currency].fetchClaims(...params, pagination),
};
return getAllOwnClaims(client, {
auth: 'EcdsaContract',
payload: pubKey,
});
};

View File

@ -0,0 +1,21 @@
import { type PubKey } from '@coinweb/wallet-lib';
import { type Client, getBalance, type Pagination } from 'market-maker.cm';
import { Currency } from '../constants';
import { makerClients } from './client';
export const getAllMarketCollateralBalance = async (currency: Currency, pubKey: PubKey, pagination?: Pagination) => {
if (currency === Currency.CWEB) {
throw new Error(`Cannot get market balance for: ${currency}`);
}
const client: Client = {
fetchClaims: (...params) => makerClients[currency].fetchClaims(...params, pagination),
};
return getBalance(client, {
auth: 'EcdsaContract',
payload: pubKey,
});
};

Some files were not shown because too many files have changed in this diff Show More