diff --git a/.cweb-config/dapp-ecosystem-test.yaml b/.cweb-config/dapp-ecosystem-test.yaml index 779766e..dd5f4a7 100644 --- a/.cweb-config/dapp-ecosystem-test.yaml +++ b/.cweb-config/dapp-ecosystem-test.yaml @@ -14,9 +14,11 @@ includes: blake3: 321b1f88930aead7fe47961c8664005b3441f6502824769bf21eb06c3e5aaba4 use: - - dex-app.cm v0.0.67+test - - eth_offer_maker 0.0.67+devnet - - jump-listener.cm v0.0.5 + - jump-listener.cm v0.1.8 + - jump_listener_devnet v0.1.8 + - jump-forwarder.cm v0.1.5 + - jump_forwarder_devnet v0.1.5 + interpreters: {} @@ -31,3 +33,4 @@ contract_instances: template: contract.cm v0.0.1 parameters: content: [] + diff --git a/.env.yarn b/.env.yarn index 0e3587e..0efe5b1 100644 --- a/.env.yarn +++ b/.env.yarn @@ -4,4 +4,4 @@ NPM_PASSWORD= # 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" -REGISTRATION_PROFILE=devnet +REGISTRATION_PROFILE=test diff --git a/package.json b/package.json index 0e00e51..0a9303c 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@coinweb/self-register": "0.1.3", "@coinweb/claims-client": "0.1.6-debug", "@coinweb/contract-kit": "0.2.6", - "@coinweb/testing-sdk": "0.0.9-remote", + "@coinweb/testing-sdk": "0.0.10-mutex", "@coinweb/minimal-sdk": "1.2.21" }, "devDependencies": { diff --git a/packages/contract.cm/src/offchain/shared/constants.ts b/packages/contract.cm/src/offchain/shared/constants.ts index ddb5d35..c28e3bb 100644 --- a/packages/contract.cm/src/offchain/shared/constants.ts +++ b/packages/contract.cm/src/offchain/shared/constants.ts @@ -9,5 +9,5 @@ export enum PUBLIC_METHODS { } export const FEE = { - ADD_WORD: 100000n, + ADD_WORD: 10000000n, }; diff --git a/packages/contract.cm/src/onchain/__tests__/handlers.test.ts b/packages/contract.cm/src/onchain/__tests__/handlers.test.ts new file mode 100644 index 0000000..b8cd823 --- /dev/null +++ b/packages/contract.cm/src/onchain/__tests__/handlers.test.ts @@ -0,0 +1,94 @@ +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(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); + }); +}); diff --git a/packages/contract.cm/src/onchain/__tests__/helpers.ts b/packages/contract.cm/src/onchain/__tests__/helpers.ts new file mode 100644 index 0000000..47473e4 --- /dev/null +++ b/packages/contract.cm/src/onchain/__tests__/helpers.ts @@ -0,0 +1,35 @@ +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; +}; diff --git a/packages/contract.cm/src/onchain/addWord.ts b/packages/contract.cm/src/onchain/addWord.ts index 715dbda..aa27151 100644 --- a/packages/contract.cm/src/onchain/addWord.ts +++ b/packages/contract.cm/src/onchain/addWord.ts @@ -27,7 +27,7 @@ export const addWord = cwait(async (...[word]: AddWordArgs) => { await storeOp(constructClaim(createWordKey(id), { word }, '0x0')); console.log('await extraLogic'); - const wordClaim = await extraLogic(id); + const [wordClaim] = await Promise.all([extraLogic(id, 1), extraLogic(id, 2)]); const newWord1 = (wordClaim?.body.word ?? '') + '_1'; const newId1 = hashCode(newWord1); diff --git a/packages/contract.cm/src/onchain/extraLogic.ts b/packages/contract.cm/src/onchain/extraLogic.ts index 50c95c0..4eea7a2 100644 --- a/packages/contract.cm/src/onchain/extraLogic.ts +++ b/packages/contract.cm/src/onchain/extraLogic.ts @@ -1,18 +1,31 @@ import { constructClaim } from '@coinweb/contract-kit'; -import { cwait, readOp, storeOp, TypedClaim } from 'cwait'; +import { cwait, lock, readOp, storeOp, TypedClaim } from 'cwait'; import { createWordKey, WordClaimBody } from '../offchain'; import { extraLogic2 } from './extraLogic2'; -export const extraLogic = cwait(async (id: string) => { - console.log('extraLogic START'); +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>(createWordKey(id)); + console.log('extraLogic storeOp + ', i); await storeOp( - constructClaim(createWordKey(id), { word: result?.body.word.split('').reverse().join('') + '_extraLogic' }, '0x0') + constructClaim( + createWordKey(id), + { word: result?.body.word.split('').reverse().join('') + '_extraLogic + ' + i }, + '0x0' + ) ); - console.log('extraLogic return extraLogic2'); - return extraLogic2(id); + console.log('extraLogic unlock + ', i); + await unlock(); + + console.log('extraLogic return extraLogic2 + ', i); + return extraLogic2(id, i); }); diff --git a/packages/contract.cm/src/onchain/extraLogic2.ts b/packages/contract.cm/src/onchain/extraLogic2.ts index 80eff4a..70bbfd7 100644 --- a/packages/contract.cm/src/onchain/extraLogic2.ts +++ b/packages/contract.cm/src/onchain/extraLogic2.ts @@ -2,12 +2,12 @@ import { cwait, readOp, TypedClaim } from 'cwait'; import { createWordKey, WordClaimBody } from '../offchain'; -export const extraLogic2 = cwait(async (id: string) => { - console.log('extraLogic2 START'); +export const extraLogic2 = cwait(async (id: string, i: number) => { + console.log('extraLogic2 START + ', i); - console.log('await readOp'); + console.log('await extraLogic2 readOp + ', i); const result = await readOp>(createWordKey(id)); - console.log('extraLogic2 END'); + console.log('extraLogic2 END + ', i); return result; }); diff --git a/packages/contract.cm/vitest.config.ts b/packages/contract.cm/vitest.config.ts new file mode 100644 index 0000000..a7b2399 --- /dev/null +++ b/packages/contract.cm/vitest.config.ts @@ -0,0 +1,14 @@ +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', + }, +}); diff --git a/packages/cwait/package.json b/packages/cwait/package.json index 9314871..4f819f8 100644 --- a/packages/cwait/package.json +++ b/packages/cwait/package.json @@ -3,7 +3,8 @@ "version": "0.0.1", "type": "module", "scripts": { - "build": "yarn g:tsc -p tsconfig.build.json", + "clean": "rm -rf dist", + "build": "yarn clean && yarn g:tsc -p tsconfig.build.json", "dev": "yarn g:tsc -p tsconfig.build.json --watch", "lint": "yarn g:lint .", "publish:lib": "node scripts/publish.js" diff --git a/packages/cwait/src/contract-kit/methods.ts b/packages/cwait/src/contract-kit/methods.ts index 7dfbef8..55bf585 100644 --- a/packages/cwait/src/contract-kit/methods.ts +++ b/packages/cwait/src/contract-kit/methods.ts @@ -1,4 +1,4 @@ -import { extractContinuations, Context } from '@coinweb/contract-kit'; +import { extractContinuations, Context, MethodCallback as MethodCallbackOrig } from '@coinweb/contract-kit'; import { getContextCall, getContextGenesis, @@ -54,6 +54,18 @@ export async function executeHandler(contractModule: ContractHandlers): Promise< }; const method = getMethodName(context); const handler = getMethodHandler(contractModule, method); - const txs = await handler(context); - writeToResultFile(txs); + + 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; + } } diff --git a/packages/cwait/src/onchain/context/context.ts b/packages/cwait/src/onchain/context/context.ts index 273a71a..8cedf7e 100644 --- a/packages/cwait/src/onchain/context/context.ts +++ b/packages/cwait/src/onchain/context/context.ts @@ -1,16 +1,38 @@ -import { Context, extractUser, getMethodArguments } from '@coinweb/contract-kit'; +import { + 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 } from '../../types'; -import { uuid } from '../utils'; +import { ExecutorMethodArgs, ResolvedOp } from '../../types'; +import { pushAwaitedTask } from '../runtime'; -import { extractFunds } from './extractFunds'; import { extractOps } from './extractOps'; let rawContext: Context | null = null; -export const setRawContext = (ctx: Context) => { - rawContext = ctx; +const initialContext = { + isChild: false, + thisId: '', + parentId: '' as string | undefined, + ops: [] as ResolvedOp[], + funds: { + availableCweb: 0n, + storedCweb: 0n, + takeOps: [] as GTake[], + }, + methodName: '', + initialArgs: [] as unknown[], + user: null as User | null, + issuer: null as ContractIssuer | null, + parentTxId: '', + needSaveResult: false, }; export const getRawContext = () => { @@ -25,71 +47,107 @@ export const getMethodArgs = () => { return getMethodArguments(getRawContext()) as [methodName: string, ...ExecutorMethodArgs]; }; -let thisId: string | undefined; +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 = { get isChild() { - return !!getMethodArgs()[5]; + return initialContext.isChild; }, get thisId() { - thisId = thisId ?? getMethodArgs()[4] ?? uuid(); - return thisId; + return initialContext.thisId; }, get parentId() { - return getMethodArgs()[5]; - }, - get ops() { - console.log('Parse ops in context'); - - const previousOps = getMethodArgs()[2] ?? []; - const { extractedOps } = extractOps(); - - const allResolvedOps = [...previousOps, ...extractedOps]; - - console.log('new ops >>>', JSON.stringify(extractedOps)); - - return allResolvedOps; - }, - get funds() { - const { availableCweb } = getCallParameters(getRawContext()); - - const { amount: storedCweb, takeOps } = extractFunds(); - - console.log('Available Cweb: ', availableCweb); - console.log('Stored Cweb: ', storedCweb); - console.log('Take Ops: ', takeOps); - - return { - availableCweb, - storedCweb, - takeOps, - }; + return initialContext.parentId; }, get methodName() { return getMethodArguments(getRawContext())[0] as string; }, get initialArgs() { - return getMethodArgs()[1] ?? []; + return initialContext.initialArgs; }, get user() { - const { authInfo } = getCallParameters(getRawContext()); - const provided = getMethodArgs()[3]; - - const user = (authInfo && extractUser(authInfo)) ?? provided; - - if (!user) { + if (!initialContext.user) { throw new Error('User not found'); } - return user; + return initialContext.user; }, get issuer() { - return getContractIssuer(getRawContext()); + if (!initialContext.issuer) { + throw new Error('Issuer not found'); + } + + return initialContext.issuer; }, get parentTxId() { - return getRawContext().call.txid; + return initialContext.parentTxId; + }, + get ops() { + return initialContext.ops; }, get needSaveResult() { - return getMethodArgs()[6] ?? false; + return initialContext.needSaveResult; + }, + get funds() { + return initialContext.funds; }, }; diff --git a/packages/cwait/src/onchain/context/extractFunds.ts b/packages/cwait/src/onchain/context/extractFunds.ts deleted file mode 100644 index 0f2899d..0000000 --- a/packages/cwait/src/onchain/context/extractFunds.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { constructTake } from '@coinweb/contract-kit'; -import { extractRead } from '@coinweb/contract-kit/dist/esm/operations/read'; - -import { getMethodArgs } from './context'; -import { extractOps } from './extractOps'; - -export const extractFunds = () => { - const { fundsOp } = extractOps(); - - const takenFundsIds = getMethodArgs()[7] ?? []; - - 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), - }; -}; diff --git a/packages/cwait/src/onchain/context/extractOps.ts b/packages/cwait/src/onchain/context/extractOps.ts index 0b06c0a..2bb917e 100644 --- a/packages/cwait/src/onchain/context/extractOps.ts +++ b/packages/cwait/src/onchain/context/extractOps.ts @@ -1,18 +1,57 @@ -import { extractContractArgs } from '@coinweb/contract-kit'; +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/result'; +import { resultKey } from '../claims'; +import { + mutexBlockLockKey, + mutexBlockUnlockKey, + mutexExecOpsKey, + MutexExecOpsResult, + MutexNotifyLockState, +} from '../mutex'; import { isResolvedBlockOp, isResolvedReadOp, isResolvedTakeOp } from '../utils'; -import { getRawContext } from './context'; +const extractFunds = (fundsOp: GRead, takenFundsIds: string[]) => { + if (!fundsOp) { + return { + takeOps: [], + amount: 0n, + }; + } -export const extractOps = () => { - const contractArgs = extractContractArgs(getRawContext().tx); + 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: [], - fundsOps: [], + fundsTakeOps: [], + storedCweb: 0n, + executionOpsTakeOp: null, }; } @@ -23,6 +62,9 @@ export const extractOps = () => { } const extractedOps: ResolvedOp[] = []; + let executedOps: MutexExecOpsResult = []; + let executionOpsProfit: bigint = 0n; + let executionOpsTakeOp: GTake | null = null; let i = 0; @@ -32,17 +74,79 @@ export const extractOps = () => { 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) && first[0] === resultKey) { - const nextAfterBlock = resolvedOps[i + 1]; + if (Array.isArray(first)) { + switch (first[0]) { + case resultKey: { + const nextAfterBlock = resolvedOps[i + 1]; - if (!isResolvedTakeOp(nextAfterBlock)) { - throw new Error('Wrong subcall result'); + if (!isResolvedTakeOp(nextAfterBlock)) { + throw new Error('Wrong subcall result'); + } + + extractedOps.push({ + ChildOp: { + ops: (nextAfterBlock.TakeOp.result as TypedClaim).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; } + } + } - extractedOps.push({ ChildOp: 0 }, ...(nextAfterBlock.TakeOp.result as TypedClaim).body); - - i += 2; + if (isResolvedTakeOp(op)) { + const keyFirstPart = op.TakeOp.result.key.first_part; + if (Array.isArray(keyFirstPart) && keyFirstPart[0] === mutexExecOpsKey) { + i++; continue; } } @@ -51,8 +155,40 @@ export const extractOps = () => { 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 { - fundsOp, - extractedOps, + fundsTakeOps: takeOps, + storedCweb: amount + executionOpsProfit, + extractedOps: allOps, + executionOpsTakeOp, }; }; diff --git a/packages/cwait/src/onchain/executor.ts b/packages/cwait/src/onchain/executor.ts deleted file mode 100644 index 9532673..0000000 --- a/packages/cwait/src/onchain/executor.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Context, NewTx, getMethodArguments, isSelfCall } from '@coinweb/contract-kit'; - -import { context, getRawContext, setRawContext } from './context'; -import { pushResolvedOp } from './promisifiedOps/resolved'; -import { constructTx } from './utils'; - -let abortExecution: ((result: boolean) => void) | null = null; - -export const executor = (method: (...args: any[]) => Promise) => { - return async (ctx: Context): Promise => { - console.log('executor-start >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); - setRawContext(ctx); - pushResolvedOp(context.ops); - - if (getMethodArguments(getRawContext()).length > 2 && !isSelfCall(ctx)) { - throw new Error('Wrong contract call, check the call arguments'); - } - - const execution = new Promise((resolve, reject) => { - abortExecution = resolve; - - method(...context.initialArgs).then( - () => { - console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-resolved'); - resolve(true); - }, - (error) => { - console.log(' <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< { - abortExecution?.(false); - }, 0); - - try { - const isFullyExecuted = await execution; - return constructTx(isFullyExecuted); - } catch (error) { - console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-error'); - console.log((error as Error).message); - throw error; - } - }; -}; - -export const stopExecution = () => { - console.log('stopExecution'); - abortExecution?.(false); -}; diff --git a/packages/cwait/src/onchain/executor/constructTx/constructTx.ts b/packages/cwait/src/onchain/executor/constructTx/constructTx.ts new file mode 100644 index 0000000..08ee09f --- /dev/null +++ b/packages/cwait/src/onchain/executor/constructTx/constructTx.ts @@ -0,0 +1,13 @@ +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; +}; diff --git a/packages/cwait/src/onchain/utils/constructTx/index.ts b/packages/cwait/src/onchain/executor/constructTx/index.ts similarity index 100% rename from packages/cwait/src/onchain/utils/constructTx/index.ts rename to packages/cwait/src/onchain/executor/constructTx/index.ts diff --git a/packages/cwait/src/onchain/executor/constructTx/prepareInThreadTxs.ts b/packages/cwait/src/onchain/executor/constructTx/prepareInThreadTxs.ts new file mode 100644 index 0000000..af304d7 --- /dev/null +++ b/packages/cwait/src/onchain/executor/constructTx/prepareInThreadTxs.ts @@ -0,0 +1,292 @@ +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)[] = []; + 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 }; +}; diff --git a/packages/cwait/src/onchain/executor/constructTx/prepareOutThreadTxs.ts b/packages/cwait/src/onchain/executor/constructTx/prepareOutThreadTxs.ts new file mode 100644 index 0000000..419285e --- /dev/null +++ b/packages/cwait/src/onchain/executor/constructTx/prepareOutThreadTxs.ts @@ -0,0 +1,141 @@ +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)[] = []; + + 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, + }; +}; diff --git a/packages/cwait/src/onchain/executor/constructTx/prepareTxs.ts b/packages/cwait/src/onchain/executor/constructTx/prepareTxs.ts new file mode 100644 index 0000000..1167ff0 --- /dev/null +++ b/packages/cwait/src/onchain/executor/constructTx/prepareTxs.ts @@ -0,0 +1,75 @@ +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, + }; +}; diff --git a/packages/cwait/src/onchain/executor/constructTx/splitTasks.ts b/packages/cwait/src/onchain/executor/constructTx/splitTasks.ts new file mode 100644 index 0000000..2db8275 --- /dev/null +++ b/packages/cwait/src/onchain/executor/constructTx/splitTasks.ts @@ -0,0 +1,26 @@ +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 }; +}; diff --git a/packages/cwait/src/onchain/executor/executor.ts b/packages/cwait/src/onchain/executor/executor.ts new file mode 100644 index 0000000..8c396f8 --- /dev/null +++ b/packages/cwait/src/onchain/executor/executor.ts @@ -0,0 +1,42 @@ +/* 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) => { + return async (ctx: Context): Promise => { + 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; + } + }; +}; diff --git a/packages/cwait/src/onchain/executor/index.ts b/packages/cwait/src/onchain/executor/index.ts new file mode 100644 index 0000000..78dc04d --- /dev/null +++ b/packages/cwait/src/onchain/executor/index.ts @@ -0,0 +1 @@ +export * from './executor'; diff --git a/packages/cwait/src/onchain/constructCwebMain.ts b/packages/cwait/src/onchain/features/constructCwebMain/constructCwebMain.ts similarity index 77% rename from packages/cwait/src/onchain/constructCwebMain.ts rename to packages/cwait/src/onchain/features/constructCwebMain/constructCwebMain.ts index 88e208b..c47b55a 100644 --- a/packages/cwait/src/onchain/constructCwebMain.ts +++ b/packages/cwait/src/onchain/features/constructCwebMain/constructCwebMain.ts @@ -2,16 +2,19 @@ import { ContractHandlers as ContractHandlersOrig, SELF_REGISTER_HANDLER_NAME } 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'; +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 Promise>) => 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)); }); diff --git a/packages/cwait/src/onchain/features/constructCwebMain/index.ts b/packages/cwait/src/onchain/features/constructCwebMain/index.ts new file mode 100644 index 0000000..1ee0946 --- /dev/null +++ b/packages/cwait/src/onchain/features/constructCwebMain/index.ts @@ -0,0 +1 @@ +export * from './constructCwebMain'; diff --git a/packages/cwait/src/onchain/promisifiedOps/cwait.ts b/packages/cwait/src/onchain/features/cwait/cwait.ts similarity index 51% rename from packages/cwait/src/onchain/promisifiedOps/cwait.ts rename to packages/cwait/src/onchain/features/cwait/cwait.ts index 4af8ba2..e8f1870 100644 --- a/packages/cwait/src/onchain/promisifiedOps/cwait.ts +++ b/packages/cwait/src/onchain/features/cwait/cwait.ts @@ -1,38 +1,29 @@ import { constructStore } from '@coinweb/contract-kit/dist/esm/operations/store'; -import { constructResultClaim } from '../claims/result'; -import { context } from '../context'; -import { stopExecution } from '../executor'; -import { opMarker } from '../global'; -import { isResolvedChildOp, isResolvedExecOp, isResolvedSlotOp } from '../utils'; -import { uuid } from '../utils'; - -import { getAwaitedTasks, pushAwaitedTask } from './awaited'; -import { getUsedOps, saveUsedOps, shiftResolvedOp } from './resolved'; +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 = Promise>(asyncCallback: TAsyncCallback) => { - console.log('cwait: ', asyncCallback.name); let isRoot = false; return (async (...args: Parameters) => { - console.log('cwait callback'); if (!isRootDetected) { isRootDetected = true; isRoot = true; } - console.log('isRoot:', isRoot); - console.log('isChild:', context.isChild); - if (isRoot) { return asyncCallback(...args); } - console.log('child logic'); - const { op, isOp } = shiftResolvedOp(); if (!isOp) { @@ -49,34 +40,32 @@ export const cwait = Promise; } else { if (isResolvedSlotOp(op)) { - console.log('cwait-slotOp'); return new Promise(() => null); } if (isResolvedExecOp(op)) { - console.log('cwait-execOp'); + setNextExec(async () => { + startSavingUsedOps(); + await asyncCallback(...args); + stopSavingUsedOps(); - saveUsedOps(); + if (!getAwaitedTasksCount()) { + console.log('push result claim'); - const result = await asyncCallback(...args); + pushAwaitedTask(constructStore(constructResultClaim(op.ExecOp.id, getUsedOps()))); + } + }, []); - const awaitedOps = getAwaitedTasks(); + stopExecution(); //Check: maybe does no affect - if (!awaitedOps.length) { - pushAwaitedTask(constructStore(constructResultClaim(op.ExecOp.id, getUsedOps()))); - - stopExecution(); - } - - return result; + return; } if (isResolvedChildOp(op)) { - return asyncCallback(...args); + return setNextExec(() => asyncCallback(...args), op.ChildOp.ops); } - console.log('cwait-error'); - throw new Error('Read operation not found'); + throw new Error('Exec or Child operation not found'); } }) as TAsyncCallback; }; diff --git a/packages/cwait/src/onchain/features/cwait/index.ts b/packages/cwait/src/onchain/features/cwait/index.ts new file mode 100644 index 0000000..e66ae58 --- /dev/null +++ b/packages/cwait/src/onchain/features/cwait/index.ts @@ -0,0 +1 @@ +export * from './cwait'; diff --git a/packages/cwait/src/onchain/features/index.ts b/packages/cwait/src/onchain/features/index.ts new file mode 100644 index 0000000..330a6d0 --- /dev/null +++ b/packages/cwait/src/onchain/features/index.ts @@ -0,0 +1,4 @@ +export * from './cwait'; +export * from './mutex'; +export * from './ops'; +export * from './constructCwebMain'; diff --git a/packages/cwait/src/onchain/features/mutex/index.ts b/packages/cwait/src/onchain/features/mutex/index.ts new file mode 100644 index 0000000..8fa8b1b --- /dev/null +++ b/packages/cwait/src/onchain/features/mutex/index.ts @@ -0,0 +1 @@ +export * from './lock'; diff --git a/packages/cwait/src/onchain/features/mutex/lock.ts b/packages/cwait/src/onchain/features/mutex/lock.ts new file mode 100644 index 0000000..75f99d6 --- /dev/null +++ b/packages/cwait/src/onchain/features/mutex/lock.ts @@ -0,0 +1,78 @@ +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((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 & { [opMarker]: boolean })[opMarker] = opMarkerValue; + + return result; +}; + +export const lock = (keys: LockedKey[] | LockedKey) => { + console.log('lockOp'); + let opMarkerValue = false; + + const result = new Promise<() => Promise>((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> & { [opMarker]: boolean })[opMarker] = opMarkerValue; + + return result; +}; diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/block.ts b/packages/cwait/src/onchain/features/ops/block.ts similarity index 79% rename from packages/cwait/src/onchain/promisifiedOps/ops/block.ts rename to packages/cwait/src/onchain/features/ops/block.ts index 56d1086..a2b0048 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/block.ts +++ b/packages/cwait/src/onchain/features/ops/block.ts @@ -1,12 +1,10 @@ import { BlockFilter, constructBlock, extractBlock } from '@coinweb/contract-kit'; -import { opMarker } from '../../global'; +import { opMarker } from '../../globals/promise'; import { isResolvedBlockOp, isResolvedSlotOp } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; +import { pushAwaitedTask, shiftResolvedOp } from '../../runtime'; export const blockOp = (filters: BlockFilter[]) => { - console.log('blockOp'); let opMarkerValue = false; const result = new Promise<[BlockFilter, boolean][] | null>((resolve, reject) => { @@ -18,12 +16,10 @@ export const blockOp = (filters: BlockFilter[]) => { opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { - console.log('blockOp-slotOp'); return; } if (!isResolvedBlockOp(op)) { - console.log('blockOp-error'); throw new Error('Block operation not found'); } diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/index.ts b/packages/cwait/src/onchain/features/ops/index.ts similarity index 100% rename from packages/cwait/src/onchain/promisifiedOps/ops/index.ts rename to packages/cwait/src/onchain/features/ops/index.ts diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts b/packages/cwait/src/onchain/features/ops/rangeRead.ts similarity index 83% rename from packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts rename to packages/cwait/src/onchain/features/ops/rangeRead.ts index 57137ab..df7fd81 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts +++ b/packages/cwait/src/onchain/features/ops/rangeRead.ts @@ -3,10 +3,9 @@ import { ClaimRange } from '@coinweb/contract-kit/dist/types/operations/read'; import { TypedClaim } from '../../../types'; import { context } from '../../context'; -import { opMarker } from '../../global'; +import { opMarker } from '../../globals/promise'; import { isResolvedReadOp, isResolvedSlotOp } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; +import { pushAwaitedTask, shiftResolvedOp } from '../../runtime'; export const rangeReadOp = ( firstPart: ClaimKey['first_part'], @@ -14,7 +13,6 @@ export const rangeReadOp = ( maxCount: number ) => { let opMarkerValue = false; - console.log('rangeReadOp'); const result = new Promise((resolve, reject) => { try { @@ -25,12 +23,10 @@ export const rangeReadOp = ( opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { - console.log('rangeReadOp-slotOp'); return; } if (!isResolvedReadOp(op)) { - console.log('rangeReadOp-error'); throw new Error('Read operation not found'); } diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/read.ts b/packages/cwait/src/onchain/features/ops/read.ts similarity index 81% rename from packages/cwait/src/onchain/promisifiedOps/ops/read.ts rename to packages/cwait/src/onchain/features/ops/read.ts index 72f0320..0b07821 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/read.ts +++ b/packages/cwait/src/onchain/features/ops/read.ts @@ -2,13 +2,11 @@ import { Claim, ClaimKey, constructRead, extractRead } from '@coinweb/contract-k import { TypedClaim } from '../../../types'; import { context } from '../../context'; -import { opMarker } from '../../global'; +import { opMarker } from '../../globals/promise'; +import { pushAwaitedTask, shiftResolvedOp } from '../../runtime'; import { isResolvedReadOp, isResolvedSlotOp } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; export const readOp = (key: ClaimKey) => { - console.log('readOp'); let opMarkerValue = false; const result = new Promise((resolve, reject) => { @@ -20,16 +18,18 @@ export const readOp = (key: ClaimKey) => { opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { - console.log('readOp-slotOp'); return; } if (!isResolvedReadOp(op)) { - console.log('readOp-error'); + 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) { diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/store.ts b/packages/cwait/src/onchain/features/ops/store.ts similarity index 71% rename from packages/cwait/src/onchain/promisifiedOps/ops/store.ts rename to packages/cwait/src/onchain/features/ops/store.ts index 57b35d7..481762e 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/store.ts +++ b/packages/cwait/src/onchain/features/ops/store.ts @@ -1,13 +1,11 @@ import { Claim, constructStore } from '@coinweb/contract-kit'; import { extractStore } from '@coinweb/contract-kit/dist/esm/operations/store'; -import { opMarker } from '../../global'; +import { opMarker } from '../../globals/promise'; import { isResolvedSlotOp, isResolvedStoreOp } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; +import { pushAwaitedTask, shiftResolvedOp } from '../../runtime'; -export const storeOp = (claim: Claim) => { - console.log('storeOp'); +export const storeOp = (claim: Claim, storeCweb?: bigint) => { let opMarkerValue = false; const result = new Promise((resolve, reject) => { @@ -15,16 +13,14 @@ export const storeOp = (claim: Claim) => { const { op, isOp } = shiftResolvedOp(); if (!isOp) { - pushAwaitedTask(constructStore(claim)); + pushAwaitedTask({ ...constructStore(claim), providedCweb: storeCweb }); opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { - console.log('storeOp-slotOp'); return; } if (!isResolvedStoreOp(op)) { - console.log('storeOp-error'); throw new Error('Store operation not found'); } diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/take.ts b/packages/cwait/src/onchain/features/ops/take.ts similarity index 80% rename from packages/cwait/src/onchain/promisifiedOps/ops/take.ts rename to packages/cwait/src/onchain/features/ops/take.ts index 2f4dcdd..56b79ab 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/take.ts +++ b/packages/cwait/src/onchain/features/ops/take.ts @@ -1,13 +1,11 @@ import { Claim, ClaimKey, constructTake, extractTake } from '@coinweb/contract-kit'; import { TypedClaim } from '../../../types'; -import { opMarker } from '../../global'; +import { opMarker } from '../../globals/promise'; import { isResolvedSlotOp, isResolvedTakeOp } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; +import { pushAwaitedTask, shiftResolvedOp } from '../../runtime'; export const takeOp = (key: ClaimKey) => { - console.log('takeOp'); let opMarkerValue = false; const result = new Promise((resolve, reject) => { @@ -19,12 +17,10 @@ export const takeOp = (key: ClaimKey) => { opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { - console.log('takeOp-slotOp'); return; } if (!isResolvedTakeOp(op)) { - console.log('takeOp-error'); throw new Error('Take operation not found'); } diff --git a/packages/cwait/src/onchain/globals/index.ts b/packages/cwait/src/onchain/globals/index.ts new file mode 100644 index 0000000..b0a9756 --- /dev/null +++ b/packages/cwait/src/onchain/globals/index.ts @@ -0,0 +1 @@ +export * from './promise'; diff --git a/packages/cwait/src/onchain/global.ts b/packages/cwait/src/onchain/globals/promise.ts similarity index 91% rename from packages/cwait/src/onchain/global.ts rename to packages/cwait/src/onchain/globals/promise.ts index 484d929..c4f8ba2 100644 --- a/packages/cwait/src/onchain/global.ts +++ b/packages/cwait/src/onchain/globals/promise.ts @@ -1,5 +1,8 @@ -import { markTaskBatch } from './promisifiedOps/awaited'; -import { getBatchId } from './utils'; +import { markTaskBatch } from '../runtime/awaitedTasks'; + +let batchId = 0; + +const getBatchId = () => batchId++; export const opMarker = Symbol('opMarker'); diff --git a/packages/cwait/src/onchain/index.ts b/packages/cwait/src/onchain/index.ts index 7be55b9..512d267 100644 --- a/packages/cwait/src/onchain/index.ts +++ b/packages/cwait/src/onchain/index.ts @@ -1,3 +1,3 @@ -export * from './context'; -export * from './executor'; -export * from './promisifiedOps'; +import './globals'; + +export * from './features'; diff --git a/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts b/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts new file mode 100644 index 0000000..7b75ff1 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts @@ -0,0 +1,72 @@ +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 | 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 | GStore)[] = 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) + : [], + }; +}; diff --git a/packages/cwait/src/onchain/mutex/calls/constructGetAccessCall.ts b/packages/cwait/src/onchain/mutex/calls/constructGetAccessCall.ts deleted file mode 100644 index 88eec8b..0000000 --- a/packages/cwait/src/onchain/mutex/calls/constructGetAccessCall.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ContractIssuer, constructContractRef } from '@coinweb/contract-kit'; - -import { constructMutexLockClaimRangeRead } from '../claims'; -import { getAccessMethodName } from '../methods'; -import { MutexGetAccessArgs } from '../types'; - -export const constructGetAccessCall = ( - issuer: ContractIssuer, - ...[claimKey, processId, uniqueId]: MutexGetAccessArgs -) => { - return { - callInfo: { - ref: constructContractRef(issuer, []), - methodInfo: { - methodName: getAccessMethodName, - methodArgs: [claimKey, processId, uniqueId] satisfies MutexGetAccessArgs, - }, - contractInfo: { - providedCweb: 800n, - authenticated: null, - }, - contractArgs: [constructMutexLockClaimRangeRead(issuer)], - }, - fee: 900n, - }; -}; diff --git a/packages/cwait/src/onchain/mutex/calls/constructLockCall.ts b/packages/cwait/src/onchain/mutex/calls/constructLockCall.ts index 4c2a69a..7cdeca5 100644 --- a/packages/cwait/src/onchain/mutex/calls/constructLockCall.ts +++ b/packages/cwait/src/onchain/mutex/calls/constructLockCall.ts @@ -1,10 +1,23 @@ import { ContractIssuer, constructContractRef } from '@coinweb/contract-kit'; -import { constructMutexBlockAccessClaimTake, constructMutexLockBlock } from '../claims'; +import { constructMutexBlockLockClaimTake, constructMutexLockBlock, constructMutexLockClaimStore } from '../claims'; import { lockMethodName } from '../methods'; import { lockFee } from '../settings'; +import { LockedKey } from '../types'; -export const constructLockCall = (issuer: ContractIssuer, lockId: string) => { +export const constructLockCall = ({ + issuer, + keys, + lockId, + timestamp, + processId, +}: { + issuer: ContractIssuer; + lockId: string; + timestamp: number; + keys: LockedKey[]; + processId: string; +}) => { return { callInfo: { ref: constructContractRef(issuer, []), @@ -18,7 +31,10 @@ export const constructLockCall = (issuer: ContractIssuer, lockId: string) => { }, contractArgs: [], }, - ops: [constructMutexLockBlock(lockId, issuer), constructMutexBlockAccessClaimTake(lockId)], - fee: lockFee + 200n, + inThreadOps: [constructMutexLockBlock(lockId, issuer), constructMutexBlockLockClaimTake(lockId)] as const, + outThreadOps: [ + constructMutexLockClaimStore({ fee: lockFee, keys, lockId, timestamp, processId, locked: false }), + ] as const, + fee: lockFee * 2n + 200n, }; }; diff --git a/packages/cwait/src/onchain/mutex/calls/constructTryOpCall.ts b/packages/cwait/src/onchain/mutex/calls/constructTryOpCall.ts deleted file mode 100644 index 04d6078..0000000 --- a/packages/cwait/src/onchain/mutex/calls/constructTryOpCall.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { constructContractRef, ContractIssuer } from '@coinweb/contract-kit'; - -import { constructMutexLockClaimRangeRead } from '../claims'; -import { tryOpMethodName } from '../methods'; -import { MutexTryOpArgs } from '../types'; - -export const constructTryOpCall = (issuer: ContractIssuer, ...[op, processId]: MutexTryOpArgs) => { - return { - callInfo: { - ref: constructContractRef(issuer, []), - methodInfo: { - methodName: tryOpMethodName, - methodArgs: [op, processId] satisfies MutexTryOpArgs, - }, - contractInfo: { - providedCweb: 800n, - authenticated: null, - }, - contractArgs: [constructMutexLockClaimRangeRead(issuer)], - }, - fee: 900n, - }; -}; diff --git a/packages/cwait/src/onchain/mutex/calls/constructUnlockCall.ts b/packages/cwait/src/onchain/mutex/calls/constructUnlockCall.ts index 4ce421d..48d102c 100644 --- a/packages/cwait/src/onchain/mutex/calls/constructUnlockCall.ts +++ b/packages/cwait/src/onchain/mutex/calls/constructUnlockCall.ts @@ -2,6 +2,7 @@ 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) => { @@ -13,12 +14,14 @@ export const constructUnlockCall = (issuer: ContractIssuer, ...[lockId, timestam methodArgs: [lockId, timestamp] satisfies MutexUnlockArgs, }, contractInfo: { - providedCweb: 1000n, + providedCweb: unlockFee, authenticated: null, }, contractArgs: [], }, - ops: notify ? [constructMutexUnlockBlock(lockId, issuer), constructMutexBlockUnlockClaimTake(lockId)] : [], - fee: 1200n, + ops: notify + ? ([constructMutexUnlockBlock(lockId, issuer), constructMutexBlockUnlockClaimTake(lockId)] as const) + : [], + fee: unlockFee + (notify ? 1000n : 800n), }; }; diff --git a/packages/cwait/src/onchain/mutex/calls/index.ts b/packages/cwait/src/onchain/mutex/calls/index.ts index 357971b..04c5a4f 100644 --- a/packages/cwait/src/onchain/mutex/calls/index.ts +++ b/packages/cwait/src/onchain/mutex/calls/index.ts @@ -1,4 +1,3 @@ -export * from './constructGetAccessCall'; export * from './constructLockCall'; -export * from './constructTryOpCall'; +export * from './constructExecOpsCall'; export * from './constructUnlockCall'; diff --git a/packages/cwait/src/onchain/mutex/claims/index.ts b/packages/cwait/src/onchain/mutex/claims/index.ts index c1f0350..588d5db 100644 --- a/packages/cwait/src/onchain/mutex/claims/index.ts +++ b/packages/cwait/src/onchain/mutex/claims/index.ts @@ -1,4 +1,4 @@ -export * from './mutexBlockAccess'; +export * from './mutexExecOps'; export * from './mutexBlockLock'; export * from './mutexBlockUnlock'; export * from './mutexLock'; diff --git a/packages/cwait/src/onchain/mutex/claims/mutexBlockAccess.ts b/packages/cwait/src/onchain/mutex/claims/mutexBlockAccess.ts deleted file mode 100644 index 8c02e2e..0000000 --- a/packages/cwait/src/onchain/mutex/claims/mutexBlockAccess.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - BlockFilter, - constructBlock, - constructClaim, - constructClaimKey, - constructStore, - constructTake, - ContractIssuer, - CwebStore, - GStore, -} from '@coinweb/contract-kit'; - -import { MutexAccessResult, MutexAccessStatus } from '../types'; - -export const mutexBlockAccessKey = 'mutex_block_access'; - -export const constructMutexBlockAccessClaimKey = (uniqueId: string) => - constructClaimKey([mutexBlockAccessKey], [uniqueId]); - -export const constructMutexBlockAccessClaim = (uniqueId: string, status: MutexAccessStatus) => - constructClaim(constructMutexBlockAccessClaimKey(uniqueId), { status } satisfies MutexAccessResult, '0x0'); - -export const constructMutexBlockAccessClaimStore = (uniqueId: string, status: MutexAccessStatus): GStore => - constructStore(constructMutexBlockAccessClaim(uniqueId, status)); - -export const constructMutexBlockAccessClaimTake = (uniqueId: string) => - constructTake(constructMutexBlockAccessClaimKey(uniqueId)); - -export const constructMutexBlockAccessBlockFilter = (lockId: string, issuer: ContractIssuer): BlockFilter => { - const { first_part: first, second_part: second } = constructMutexBlockAccessClaimKey(lockId); - - return { - issuer, - first, - second, - }; -}; - -export const constructMutexAccessBlock = (lockId: string, issuer: ContractIssuer) => { - return constructBlock([constructMutexBlockAccessBlockFilter(lockId, issuer)]); -}; diff --git a/packages/cwait/src/onchain/mutex/claims/mutexBlockLock.ts b/packages/cwait/src/onchain/mutex/claims/mutexBlockLock.ts index 5985ead..92de7cc 100644 --- a/packages/cwait/src/onchain/mutex/claims/mutexBlockLock.ts +++ b/packages/cwait/src/onchain/mutex/claims/mutexBlockLock.ts @@ -8,15 +8,17 @@ import { ContractIssuer, } from '@coinweb/contract-kit'; -export const mutexBlockLockKey = 'mutex_block_lock'; +import { MutexNotifyLockState } from '../types'; + +export const mutexBlockLockKey = '_mutex_block_lock'; export const constructMutexBlockLockClaimKey = (lockId: string) => constructClaimKey([mutexBlockLockKey], [lockId]); -export const constructMutexBlockLockClaim = ({ lockId }: { lockId: string }) => - constructClaim(constructMutexBlockLockClaimKey(lockId), {}, '0x0'); +export const constructMutexBlockLockClaim = ({ lockId, timestamp }: MutexNotifyLockState) => + constructClaim(constructMutexBlockLockClaimKey(lockId), { lockId, timestamp } satisfies MutexNotifyLockState, '0x0'); -export const constructMutexBlockLockClaimStore = ({ lockId }: { lockId: string }) => - constructStore(constructMutexBlockLockClaim({ lockId })); +export const constructMutexBlockLockClaimStore = ({ lockId, timestamp }: MutexNotifyLockState) => + constructStore(constructMutexBlockLockClaim({ lockId, timestamp })); export const constructMutexBlockLockClaimTake = (lockId: string) => constructTake(constructMutexBlockLockClaimKey(lockId)); diff --git a/packages/cwait/src/onchain/mutex/claims/mutexBlockUnlock.ts b/packages/cwait/src/onchain/mutex/claims/mutexBlockUnlock.ts index 6f7f7fd..11eab3f 100644 --- a/packages/cwait/src/onchain/mutex/claims/mutexBlockUnlock.ts +++ b/packages/cwait/src/onchain/mutex/claims/mutexBlockUnlock.ts @@ -10,7 +10,7 @@ import { GStore, } from '@coinweb/contract-kit'; -export const mutexBlockUnlockKey = 'mutex_block_unlock'; +export const mutexBlockUnlockKey = '_mutex_block_unlock'; export const constructMutexBlockUnlockClaimKey = (uniqueId: string) => constructClaimKey([mutexBlockUnlockKey], [uniqueId]); diff --git a/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts b/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts new file mode 100644 index 0000000..0770554 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts @@ -0,0 +1,47 @@ +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 => 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)]); +}; diff --git a/packages/cwait/src/onchain/mutex/claims/mutexLock.ts b/packages/cwait/src/onchain/mutex/claims/mutexLock.ts index 725d80d..2e135c5 100644 --- a/packages/cwait/src/onchain/mutex/claims/mutexLock.ts +++ b/packages/cwait/src/onchain/mutex/claims/mutexLock.ts @@ -6,13 +6,13 @@ import { ContractIssuer, GRead, } from '@coinweb/contract-kit'; -import { CwebRead } from '@coinweb/contract-kit/dist/esm/operations/read'; +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 mutexLockKey = '_mutex_lock'; export const constructMutexLockFirstPart = () => [mutexLockKey]; @@ -43,17 +43,17 @@ export const constructMutexLockClaim = ({ export const constructMutexLockClaimStore = ({ fee, keys, - locked, lockId, timestamp, processId, + locked, }: { fee: bigint; keys: LockedKey[]; - locked: boolean; lockId: string; timestamp: number; processId: string; + locked: boolean; }) => constructStore(constructMutexLockClaim({ fee, keys, locked, lockId, timestamp, processId })); export const constructMutexLockClaimRangeRead = (issuer: ContractIssuer): GRead => diff --git a/packages/cwait/src/onchain/mutex/index.ts b/packages/cwait/src/onchain/mutex/index.ts index c5a0114..16dd33a 100644 --- a/packages/cwait/src/onchain/mutex/index.ts +++ b/packages/cwait/src/onchain/mutex/index.ts @@ -1,20 +1,33 @@ import { execLockMethodName, - getAccessMethodName, + execOps, + execOpsMethodName, lockMethodName, mutexExecLock, - mutexGetAccess, 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, - [getAccessMethodName]: mutexGetAccess, [lockMethodName]: mutexLock, [unlockMethodName]: mutexUnlock, + [execOpsMethodName]: execOps, + [preReadExecTakeOpsMethodName]: preReadExecTakeOps, + [saveExecOpResultMethodName]: saveExecOpResult, + [waitMethodName]: wait, + [notifyLockMethodName]: notifyLock, }; diff --git a/packages/cwait/src/onchain/mutex/methods/execLock.ts b/packages/cwait/src/onchain/mutex/methods/execLock.ts index 3f79a2f..c5a8934 100644 --- a/packages/cwait/src/onchain/mutex/methods/execLock.ts +++ b/packages/cwait/src/onchain/mutex/methods/execLock.ts @@ -13,10 +13,10 @@ import { getCallParameters, getContractIssuer } from 'lib/onchain'; import { toHex, TypedClaim } from 'lib/shared'; import { lockFee } from '../settings'; -import { MutexLockState } from '../types'; +import { MutexLockState, MutexWaitNotifyArgs } from '../types'; import { isMatchLockKeys } from '../utils'; -import { lockMethodName, notifyLockMethodName } from './names'; +import { waitMethodName } from './names'; export const mutexExecLock = (context: Context) => { const { availableCweb } = getCallParameters(context); @@ -59,25 +59,17 @@ export const mutexExecLock = (context: Context) => { callInfo: { ref: constructContractRef(issuer, []), methodInfo: { - methodName: lockMethodName, - methodArgs: [], + 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: lockFee, - authenticated: null, - }, - contractArgs: [], - }, - }, - { - callInfo: { - ref: constructContractRef(issuer, []), - methodInfo: { - methodName: notifyLockMethodName, - methodArgs: [(lockCandidate.key.second_part as [number, string])[1]], - }, - contractInfo: { - providedCweb: 200n, + providedCweb: availableCweb + BigInt(lockCandidate.fees_stored) - 1000n, authenticated: null, }, contractArgs: [], diff --git a/packages/cwait/src/onchain/mutex/methods/execOps.ts b/packages/cwait/src/onchain/mutex/methods/execOps.ts new file mode 100644 index 0000000..a1f3b24 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/execOps.ts @@ -0,0 +1,121 @@ +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(context); + + const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map( + ({ content }) => content as TypedClaim + ); + + 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, + }, + }, + ] + ), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/methods/getAccess.ts b/packages/cwait/src/onchain/mutex/methods/getAccess.ts deleted file mode 100644 index a3365ad..0000000 --- a/packages/cwait/src/onchain/mutex/methods/getAccess.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { constructContinueTx, Context, extractContractArgs, extractRead } from '@coinweb/contract-kit'; -import { getContractArguments } from 'lib/onchain'; -import { TypedClaim } from 'lib/shared'; - -import { constructMutexBlockAccessClaimStore } from '../claims'; -import { MutexAccessStatus, MutexGetAccessArgs, MutexLockState } from '../types'; -import { isMatchLockKeys } from '../utils'; - -export const mutexGetAccess = (context: Context) => { - const [claimKey, processId, uniqueId] = getContractArguments(context); - - const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map( - ({ content }) => content as TypedClaim - ); - - if (!lockQueue) { - throw new Error('No lock queue found'); - } - - const isLockedByOtherProcess = lockQueue.some( - ({ body }) => body.locked && body.processId !== processId && body.keys.some((key) => isMatchLockKeys(key, claimKey)) - ); - - return [ - constructContinueTx(context, [ - constructMutexBlockAccessClaimStore( - uniqueId, - isLockedByOtherProcess ? MutexAccessStatus.DENIED : MutexAccessStatus.GRANTED - ), - ]), - ]; -}; diff --git a/packages/cwait/src/onchain/mutex/methods/index.ts b/packages/cwait/src/onchain/mutex/methods/index.ts index 78f9c05..3cbc6ba 100644 --- a/packages/cwait/src/onchain/mutex/methods/index.ts +++ b/packages/cwait/src/onchain/mutex/methods/index.ts @@ -1,7 +1,7 @@ export * from './execLock'; -export * from './getAccess'; export * from './lock'; export * from './names'; export * from './notifyLock'; -export * from './tryOp'; +export * from './execOps'; export * from './unlock'; +export * from './saveExecOpResult'; diff --git a/packages/cwait/src/onchain/mutex/methods/names.ts b/packages/cwait/src/onchain/mutex/methods/names.ts index b430203..823cf34 100644 --- a/packages/cwait/src/onchain/mutex/methods/names.ts +++ b/packages/cwait/src/onchain/mutex/methods/names.ts @@ -1,6 +1,8 @@ export const lockMethodName = '_mutex_lock'; export const execLockMethodName = '_mutex_execLock'; -export const getAccessMethodName = '_mutex_get_access'; export const unlockMethodName = '_mutex_unlock'; -export const tryOpMethodName = '_mutex_try_op'; 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'; diff --git a/packages/cwait/src/onchain/mutex/methods/notifyLock.ts b/packages/cwait/src/onchain/mutex/methods/notifyLock.ts index 3d5e838..a49ed88 100644 --- a/packages/cwait/src/onchain/mutex/methods/notifyLock.ts +++ b/packages/cwait/src/onchain/mutex/methods/notifyLock.ts @@ -1,11 +1,11 @@ import { constructContinueTx, Context } from '@coinweb/contract-kit'; -import { getContractArguments } from 'lib/onchain'; +import { getMethodArguments } from 'lib/onchain'; import { constructMutexBlockLockClaimStore } from '../claims/mutexBlockLock'; import { MutexNotifyLockArgs } from '../types'; export const notifyLock = (context: Context) => { - const [lockId] = getContractArguments(context); + const [lockInfo] = getMethodArguments(context); - return [constructContinueTx(context, [constructMutexBlockLockClaimStore({ lockId })])]; + return [constructContinueTx(context, [constructMutexBlockLockClaimStore(lockInfo)])]; }; diff --git a/packages/cwait/src/onchain/mutex/methods/preReadExecTakeOps.ts b/packages/cwait/src/onchain/mutex/methods/preReadExecTakeOps.ts new file mode 100644 index 0000000..b90a951 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/preReadExecTakeOps.ts @@ -0,0 +1,49 @@ +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(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, + }, + }, + ] + ), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/methods/saveExecOpResult.ts b/packages/cwait/src/onchain/mutex/methods/saveExecOpResult.ts new file mode 100644 index 0000000..7b27181 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/saveExecOpResult.ts @@ -0,0 +1,33 @@ +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(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)])]; +}; diff --git a/packages/cwait/src/onchain/mutex/methods/tryOp.ts b/packages/cwait/src/onchain/mutex/methods/tryOp.ts deleted file mode 100644 index ce986d2..0000000 --- a/packages/cwait/src/onchain/mutex/methods/tryOp.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { constructContinueTx, Context, extractContractArgs, extractRead } from '@coinweb/contract-kit'; -import { getContractArguments } from 'lib/onchain'; -import { TypedClaim } from 'lib/shared'; - -import { MutexLockState, MutexTryOpArgs } from '../types'; -import { isMatchLockKeys } from '../utils'; - -export const tryOp = (context: Context) => { - const [op, processId] = getContractArguments(context); - - const claimKey = 'StoreOp' in op ? op.StoreOp.key : op.TakeOp.key; - - const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map( - ({ content }) => content as TypedClaim - ); - - if (!lockQueue) { - throw new Error('No lock queue found'); - } - - const isLockedByOtherProcess = lockQueue.some( - ({ body }) => body.locked && body.processId !== processId && body.keys.some((key) => isMatchLockKeys(key, claimKey)) - ); - - if (isLockedByOtherProcess) { - return []; - } - - return [constructContinueTx(context, [op])]; -}; diff --git a/packages/cwait/src/onchain/mutex/methods/unlock.ts b/packages/cwait/src/onchain/mutex/methods/unlock.ts index ec5167c..9b6ab01 100644 --- a/packages/cwait/src/onchain/mutex/methods/unlock.ts +++ b/packages/cwait/src/onchain/mutex/methods/unlock.ts @@ -1,7 +1,7 @@ import { constructContinueTx, constructContractRef, constructTake, Context, passCwebFrom } from '@coinweb/contract-kit'; -import { getCallParameters, getContractArguments, getContractIssuer } from 'lib/onchain'; +import { getCallParameters, getContractIssuer, getMethodArguments } from 'lib/onchain'; -import { constructMutexLockClaimKey } from '../claims'; +import { constructMutexBlockUnlockClaimStore, constructMutexLockClaimKey } from '../claims'; import { lockFee } from '../settings'; import { MutexUnlockArgs } from '../types'; @@ -11,12 +11,16 @@ export const mutexUnlock = (context: Context) => { const { availableCweb } = getCallParameters(context); const issuer = getContractIssuer(context); - const [lockId, timestamp] = getContractArguments(context); + const [lockId, timestamp] = getMethodArguments(context); return [ constructContinueTx( context, - [passCwebFrom(issuer, availableCweb), constructTake(constructMutexLockClaimKey(lockId, timestamp))], + [ + passCwebFrom(issuer, availableCweb), + constructTake(constructMutexLockClaimKey(lockId, timestamp)), + constructMutexBlockUnlockClaimStore(lockId), + ], [ { callInfo: { diff --git a/packages/cwait/src/onchain/mutex/methods/wait.ts b/packages/cwait/src/onchain/mutex/methods/wait.ts new file mode 100644 index 0000000..7ed1eff --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/wait.ts @@ -0,0 +1,62 @@ +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(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: [], + }, + }, + ] + ), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/settings.ts b/packages/cwait/src/onchain/mutex/settings.ts index e277112..5f8f9ad 100644 --- a/packages/cwait/src/onchain/mutex/settings.ts +++ b/packages/cwait/src/onchain/mutex/settings.ts @@ -2,3 +2,5 @@ export const lockFee = 5000n; export const unlockFee = 1000n + lockFee; export const rangeReadLimit = 10000; + +export const waitSteps = 2; diff --git a/packages/cwait/src/onchain/mutex/types.ts b/packages/cwait/src/onchain/mutex/types.ts index 9a54e83..9766fbf 100644 --- a/packages/cwait/src/onchain/mutex/types.ts +++ b/packages/cwait/src/onchain/mutex/types.ts @@ -1,4 +1,5 @@ -import { Claim, ClaimKey, GStore, GTake } from '@coinweb/contract-kit'; +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 & Partial>; @@ -8,19 +9,30 @@ export type MutexLockState = { processId: string; }; -export enum MutexAccessStatus { - GRANTED = 'granted', - DENIED = 'denied', -} - -export type MutexAccessResult = { - status: MutexAccessStatus; +export type MutexNotifyLockState = { + timestamp: number; + lockId: string; }; -export type MutexGetAccessArgs = [claimKey: ClaimKey, processId: string, uniqueId: string]; +export type MutexExecOpsResult = ( + | { + ok: true; + resolved: ResolvedOperation; + } + | { + ok: false; + error: string; + } +)[]; export type MutexUnlockArgs = [lockId: string, timestamp: number, notify?: boolean]; -export type MutexTryOpArgs = [op: GTake | GStore, processId: string]; +export type MutexExecOpArgs = [ops: (GTake | GStore)[], processId: string, execId?: string]; -export type MutexNotifyLockArgs = [lockId: 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[]]; diff --git a/packages/cwait/src/onchain/promisifiedOps/index.ts b/packages/cwait/src/onchain/promisifiedOps/index.ts deleted file mode 100644 index 672baa5..0000000 --- a/packages/cwait/src/onchain/promisifiedOps/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './awaited'; -export * from './cwait'; -export * from './ops'; -export * from './resolved'; diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/lock.ts b/packages/cwait/src/onchain/promisifiedOps/ops/lock.ts deleted file mode 100644 index 7bb3fd6..0000000 --- a/packages/cwait/src/onchain/promisifiedOps/ops/lock.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ResolvedLockOp } from '../../../types'; -import { opMarker } from '../../global'; -import { LockedKey } from '../../mutex'; -import { isResolvedLockOp, isResolvedSlotOp, uuid } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; - -export const lockOp = (keys: LockedKey[] | LockedKey) => { - console.log('lockOp'); - let opMarkerValue = false; - - const result = new Promise((resolve, reject) => { - try { - const { op, isOp } = shiftResolvedOp(); - - if (!isOp) { - pushAwaitedTask({ LockOp: { lockId: uuid(), keys: Array.isArray(keys) ? keys : [keys] } }); - opMarkerValue = true; - } else { - if (isResolvedSlotOp(op)) { - console.log('storeOp-slotOp'); - return; - } - - if (!isResolvedLockOp(op)) { - console.log('lockOp-error'); - throw new Error('Lock operation not found'); - } - - const result = op.LockOp; - - resolve(result); - } - } catch (error) { - reject(error); - } - }); - - (result as Promise & { [opMarker]: boolean })[opMarker] = opMarkerValue; - - return result; -}; diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/unlock.ts b/packages/cwait/src/onchain/promisifiedOps/ops/unlock.ts deleted file mode 100644 index 404792a..0000000 --- a/packages/cwait/src/onchain/promisifiedOps/ops/unlock.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { opMarker } from '../../global'; -import { isResolvedSlotOp, isResolvedUnlockOp } from '../../utils'; -import { pushAwaitedTask } from '../awaited'; -import { shiftResolvedOp } from '../resolved'; - -export const unlock = (lockId: string) => { - console.log('lockOp'); - let opMarkerValue = false; - - const result = new Promise((resolve, reject) => { - try { - const { op, isOp } = shiftResolvedOp(); - - if (!isOp) { - pushAwaitedTask({ UnlockOp: { lockId } }); - 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 & { [opMarker]: boolean })[opMarker] = opMarkerValue; - - return result; -}; diff --git a/packages/cwait/src/onchain/promisifiedOps/awaited.ts b/packages/cwait/src/onchain/runtime/awaitedTasks.ts similarity index 87% rename from packages/cwait/src/onchain/promisifiedOps/awaited.ts rename to packages/cwait/src/onchain/runtime/awaitedTasks.ts index 23c1dbc..f2b3934 100644 --- a/packages/cwait/src/onchain/promisifiedOps/awaited.ts +++ b/packages/cwait/src/onchain/runtime/awaitedTasks.ts @@ -6,7 +6,7 @@ export const pushAwaitedTask = (op: PreparedOp) => { awaitedTasks.push({ op, batchId: -1 }); }; -export const getAwaitedTasks = () => awaitedTasks; +export const getAwaitedTasks = () => [...awaitedTasks]; export const markTaskBatch = (count: number, batchId: number) => { for (let i = 1; i <= count; i++) { diff --git a/packages/cwait/src/onchain/runtime/execLoop.ts b/packages/cwait/src/onchain/runtime/execLoop.ts new file mode 100644 index 0000000..6630337 --- /dev/null +++ b/packages/cwait/src/onchain/runtime/execLoop.ts @@ -0,0 +1,54 @@ +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; +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 => { + const nextExec = execQueue.pop(); + + if (nextExec) { + const execution = new Promise((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; +}; diff --git a/packages/cwait/src/onchain/runtime/index.ts b/packages/cwait/src/onchain/runtime/index.ts new file mode 100644 index 0000000..bf12790 --- /dev/null +++ b/packages/cwait/src/onchain/runtime/index.ts @@ -0,0 +1,3 @@ +export * from './awaitedTasks'; +export * from './execLoop'; +export * from './resolvedOps'; diff --git a/packages/cwait/src/onchain/promisifiedOps/resolved.ts b/packages/cwait/src/onchain/runtime/resolvedOps.ts similarity index 59% rename from packages/cwait/src/onchain/promisifiedOps/resolved.ts rename to packages/cwait/src/onchain/runtime/resolvedOps.ts index b53ff78..96db33c 100644 --- a/packages/cwait/src/onchain/promisifiedOps/resolved.ts +++ b/packages/cwait/src/onchain/runtime/resolvedOps.ts @@ -4,8 +4,13 @@ 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 { @@ -15,8 +20,8 @@ export const pushResolvedOp = (op: ResolvedOp | ResolvedOp[]) => { export const shiftResolvedOp = () => { const result = { - isOp: resolvedOps.length > 0, - op: resolvedOps.shift(), + isOp: resolvedOps.length > 0 && !isFreezed, + op: isFreezed ? undefined : resolvedOps.shift(), } as | { isOp: true; @@ -31,12 +36,26 @@ export const shiftResolvedOp = () => { usedOps.push(result.op); } + console.log('shiftResolvedOp: ', JSON.stringify(result)); + return result; }; export const getUsedOps = () => usedOps; -export const saveUsedOps = () => { +export const startSavingUsedOps = () => { usedOps = []; isSavingUsed = true; }; + +export const stopSavingUsedOps = () => { + isSavingUsed = false; +}; + +export const freezeResolvedOps = () => { + isFreezed = true; +}; + +export const unfreezeResolvedOps = () => { + isFreezed = false; +}; diff --git a/packages/cwait/src/onchain/utils/batchId.ts b/packages/cwait/src/onchain/utils/batchId.ts deleted file mode 100644 index 3880b73..0000000 --- a/packages/cwait/src/onchain/utils/batchId.ts +++ /dev/null @@ -1,3 +0,0 @@ -let batchId = 0; - -export const getBatchId = () => batchId++; diff --git a/packages/cwait/src/onchain/utils/callstack.ts b/packages/cwait/src/onchain/utils/callstack.ts deleted file mode 100644 index ea22049..0000000 --- a/packages/cwait/src/onchain/utils/callstack.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const getStack = ({ skip = 0 }: { skip?: number } = {}) => - new Error().stack - ?.split('\n') - .slice(2 + skip) - .map((line) => { - const match = line.match(/at\s+([^\s(]+)/); - return match ? match[1] : ''; - }) - .filter((name) => name && name !== 'Promise') - .join('@') || ''; diff --git a/packages/cwait/src/onchain/utils/constructTx/constructTx.ts b/packages/cwait/src/onchain/utils/constructTx/constructTx.ts deleted file mode 100644 index bf5ae39..0000000 --- a/packages/cwait/src/onchain/utils/constructTx/constructTx.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { prepareTx } from './prepareTxs'; - -export const constructTx = (isFullyExecuted: boolean) => { - const { calls } = prepareTx(isFullyExecuted); - - const { txs } = prepareTx(isFullyExecuted, calls); - - return txs; -}; diff --git a/packages/cwait/src/onchain/utils/constructTx/prepareInThreadTxs.ts b/packages/cwait/src/onchain/utils/constructTx/prepareInThreadTxs.ts deleted file mode 100644 index 0458c90..0000000 --- a/packages/cwait/src/onchain/utils/constructTx/prepareInThreadTxs.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - constructBlock, - constructContinueTx, - constructContractRef, - FullCallInfo, - NewTx, - passCwebFrom, - PreparedOperation, - sendCwebInterface, -} from '@coinweb/contract-kit'; -import { CwebBlock } from '@coinweb/contract-kit/dist/esm/operations/block'; -import { GBlock } from '@coinweb/contract-kit/dist/types/operations/generics'; - -import { ExecutorMethodArgs, ResolvedOp, ResolvedSlotOp, Task } from '../../../types'; -import { constructFundsClaimRangRead, constructFundsClaimStore } from '../../claims/funds'; -import { constructResultBlockFilter, constructResultClaimTake, resultKey } from '../../claims/result'; -import { context, getRawContext } from '../../context'; -import { isPreparedBlockOp, isPreparedExecOp } from '../typeGuards'; - -export const prepareInThreadTxs = ({ - cwebPerCall, - outThreadTasksCount, - outThreadFee, - tasks, -}: { - tasks: Task[]; - cwebPerCall: bigint; - outThreadTasksCount: number; - outThreadFee: bigint; -}): { - txs: NewTx[]; - calls: number; -} => { - if (!tasks.length) { - const { constructSendCweb } = sendCwebInterface(); - const { availableCweb, takeOps, storedCweb } = context.funds; - const { user } = context; - - const restOfAvailableCweb = availableCweb - outThreadFee; - - const restOfCweb = restOfAvailableCweb + storedCweb - BigInt(takeOps.length) * 100n - 3000n; - - const txs = - restOfCweb > 0n - ? [ - constructContinueTx(getRawContext(), [ - passCwebFrom(context.issuer, restOfAvailableCweb), - ...takeOps, - ...constructSendCweb(restOfCweb, user, null), - ]), - ] - : []; - - return { txs, calls: 0 }; - } - - let txFee = 0n; - let callsPrepared = 0; - const resolvedSlotOps = new Array(outThreadTasksCount).fill({ SlotOp: 0 }) satisfies ResolvedSlotOp[]; - - const resolvedChildOps: ResolvedOp[] = [...context.ops, ...resolvedSlotOps]; - - //Arg for the main call - const callArgs: PreparedOperation[] = []; - - //Info for separate child call - const childCalls: FullCallInfo[] = []; - - //Block ops for separate child call - const childBlocks = tasks - .filter((task): task is { op: GBlock; batchId: number } => isPreparedBlockOp(task.op)) - .map(({ op }) => op); - - tasks.forEach((task) => { - if (isPreparedExecOp(task.op)) { - console.log('Child call info'); - const id = task.op.ExecOp.id; - - callArgs.push(constructBlock([constructResultBlockFilter(id)]), constructResultClaimTake(id)); - txFee += 200n; - - childCalls.push({ - callInfo: { - ref: constructContractRef(context.issuer, []), - methodInfo: { - methodName: context.methodName, - methodArgs: [ - context.initialArgs, - [...resolvedChildOps, { ExecOp: { id } }], - context.user, - id, - context.thisId, - true, - ] satisfies ExecutorMethodArgs, - }, - contractInfo: { - providedCweb: cwebPerCall - 700n, - authenticated: null, - }, - contractArgs: [], - }, - }); - - callsPrepared++; - } else { - callArgs.push(task.op); - txFee += 100n; - } - - resolvedChildOps.push({ SlotOp: 0 }); - }); - - 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', JSON.stringify(callArgs)); - - 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; - - resultOps.push( - constructFundsClaimStore(context.parentId, cwebToStore), - ...takeOps, - passCwebFrom(context.issuer, availableCweb) - ); - } - - if (resultOps.length) { - returnTxs.push(constructContinueTx(getRawContext(), resultOps)); - } - } else { - txFee += 800n + outThreadFee + BigInt(takeOps.length) * 100n; - callsPrepared++; - - 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]), - ] satisfies ExecutorMethodArgs, - }, - contractInfo: { - providedCweb: cwebPerCall - txFee + storedCweb, - authenticated: null, - }, - contractArgs: [constructFundsClaimRangRead(context.thisId), ...callArgs], - }, - }, - ] - ) - ); - - if (childCalls.length) { - returnTxs.push(constructContinueTx(getRawContext(), childBlocks, childCalls)); - } - } - } - - return { txs: returnTxs, calls: callsPrepared }; -}; diff --git a/packages/cwait/src/onchain/utils/constructTx/prepareOutThreadTxs.ts b/packages/cwait/src/onchain/utils/constructTx/prepareOutThreadTxs.ts deleted file mode 100644 index 390b5af..0000000 --- a/packages/cwait/src/onchain/utils/constructTx/prepareOutThreadTxs.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { - constructContinueTx, - constructContractRef, - FullCallInfo, - NewTx, - PreparedOperation, -} from '@coinweb/contract-kit'; - -import { ExecutorMethodArgs, Task } from '../../../types'; -import { context, getRawContext } from '../../context'; -import { isPreparedExecOp } from '../typeGuards'; - -export const prepareOutThreadTxs = ({ - tasks, - cwebPerCall, -}: { - tasks: Task[]; - cwebPerCall: bigint; -}): { - txs: NewTx[]; - fee: bigint; - calls: number; -} => { - const siblingCallResolvedOps = [...context.ops]; - - const siblingTxInfo: { - [batchId: number]: { - calls: FullCallInfo[]; - ops: PreparedOperation[]; - }; - } = {}; - - let txFee = 0n; - let callsPrepared = 0; - - tasks.forEach((task) => { - if (isPreparedExecOp(task.op)) { - console.log('Sibling call info'); - - const id = task.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 - 700n, - authenticated: null, - }, - contractArgs: [], - }, - }; - - if (siblingTxInfo[task.batchId]) { - siblingTxInfo[task.batchId].calls.push(callInfo); - } else { - siblingTxInfo[task.batchId] = { - calls: [callInfo], - ops: [], - }; - } - - callsPrepared++; - } else { - if (siblingTxInfo[task.batchId]) { - siblingTxInfo[task.batchId].ops.push(task.op); - } else { - siblingTxInfo[task.batchId] = { - calls: [], - ops: [task.op], - }; - } - - txFee += 100n; - } - - siblingCallResolvedOps.push({ SlotOp: 0 }); - }); - - const siblingTxs = Object.values(siblingTxInfo).map(({ calls, ops }) => - constructContinueTx(getRawContext(), ops, calls) - ); - - txFee += BigInt(siblingTxs.length) * 100n; - - return { txs: siblingTxs, fee: txFee, calls: callsPrepared }; -}; diff --git a/packages/cwait/src/onchain/utils/constructTx/prepareTxs.ts b/packages/cwait/src/onchain/utils/constructTx/prepareTxs.ts deleted file mode 100644 index cc0ddf4..0000000 --- a/packages/cwait/src/onchain/utils/constructTx/prepareTxs.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { constructContinueTx, NewTx, passCwebFrom, sendCwebInterface } from '@coinweb/contract-kit'; - -import { context, getRawContext } from '../../context'; -import { getAwaitedTasks } from '../../promisifiedOps'; - -import { prepareInThreadTxs } from './prepareInThreadTxs'; -import { prepareOutThreadTxs } from './prepareOutThreadTxs'; -import { splitTasks } from './splitTasks'; - -export const prepareTx = (isFullyExecuted: boolean, callsCount?: number): { txs: NewTx[]; calls: number } => { - console.log('Calls Count: ', callsCount); - - const awaitedTasks = getAwaitedTasks(); - - console.log('Awaited Tasks: ', JSON.stringify(awaitedTasks)); - - if (!awaitedTasks.length) { - if (context.isChild) { - return { txs: [], calls: 0 }; - } - - const { constructSendCweb } = sendCwebInterface(); - const { availableCweb, takeOps, storedCweb } = context.funds; - const { user } = context; - - const restOfCweb = availableCweb + storedCweb - BigInt(takeOps.length) * 100n - 3000n; - - const txs = - restOfCweb > 0n - ? [ - constructContinueTx(getRawContext(), [ - passCwebFrom(context.issuer, availableCweb), - ...takeOps, - ...constructSendCweb(restOfCweb, user, null), - ]), - ] - : []; - - return { txs, calls: 0 }; - } - - const { inThreadTasks, outThreadTasks } = splitTasks(awaitedTasks, isFullyExecuted); - - const { availableCweb } = context.funds; - - const cwebPerCall = availableCweb / BigInt(callsCount || 1); - const { - txs: outThreadTxs, - fee: outThreadFee, - calls: outThreadCallsPrepared, - } = prepareOutThreadTxs({ tasks: outThreadTasks, cwebPerCall }); - - const { txs: inThreadTxs, calls: inThreadCallsPrepared } = prepareInThreadTxs({ - tasks: inThreadTasks, - cwebPerCall, - outThreadTasksCount: outThreadTasks.length, - outThreadFee, - }); - - return { txs: [...inThreadTxs, ...outThreadTxs], calls: inThreadCallsPrepared + outThreadCallsPrepared }; -}; diff --git a/packages/cwait/src/onchain/utils/constructTx/splitTasks.ts b/packages/cwait/src/onchain/utils/constructTx/splitTasks.ts deleted file mode 100644 index ee90bf1..0000000 --- a/packages/cwait/src/onchain/utils/constructTx/splitTasks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Task } from '../../../types'; -import { isPreparedBlockOp } from '../typeGuards'; - -export const splitTasks = (awaitedTasks: Task[], isFullyExecuted: boolean) => { - const awaitedTasksBatched: (Task | Task[])[] = []; - const preparedTasks: (Task | Task[])[] = []; - - awaitedTasks.forEach((task) => { - if (task.batchId === -1) { - awaitedTasksBatched.push(task); - return; - } - - const latestTask = awaitedTasksBatched.at(-1); - - if (Array.isArray(latestTask) && latestTask.at(-1)?.batchId === task.batchId) { - latestTask.push(task); - return; - } - - awaitedTasksBatched.push([task]); - }); - - awaitedTasksBatched.forEach((task, i) => { - if ( - i === awaitedTasksBatched.length - 1 || - !Array.isArray(task) || - (task.length > 1 && task.some(({ op }) => isPreparedBlockOp(op))) - ) { - preparedTasks.push(task); - } else { - preparedTasks.push(...task.map((task) => ({ ...task, batchId: -1 }))); - } - }); - - let inThreadTasks: Task[] = []; - let outThreadTasks: Task[] = []; - - if (!isFullyExecuted) { - const preparedCallTasks = preparedTasks.at(-1)!; - inThreadTasks = [...(Array.isArray(preparedCallTasks) ? preparedCallTasks : [preparedCallTasks])]; - outThreadTasks = preparedTasks.slice(0, -1).flat(); - } else { - outThreadTasks = preparedTasks.flat(); - } - - return { inThreadTasks, outThreadTasks }; -}; diff --git a/packages/cwait/src/onchain/utils/index.ts b/packages/cwait/src/onchain/utils/index.ts index d9e397c..b72ca62 100644 --- a/packages/cwait/src/onchain/utils/index.ts +++ b/packages/cwait/src/onchain/utils/index.ts @@ -1,5 +1,2 @@ -export * from './batchId'; -export * from './callstack'; -export * from './constructTx'; -export * from './typeGuards'; +export * from './opTypeGuards'; export * from './uuid'; diff --git a/packages/cwait/src/onchain/utils/typeGuards.ts b/packages/cwait/src/onchain/utils/opTypeGuards.ts similarity index 67% rename from packages/cwait/src/onchain/utils/typeGuards.ts rename to packages/cwait/src/onchain/utils/opTypeGuards.ts index ff95a00..c825dab 100644 --- a/packages/cwait/src/onchain/utils/typeGuards.ts +++ b/packages/cwait/src/onchain/utils/opTypeGuards.ts @@ -12,10 +12,14 @@ 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, @@ -25,97 +29,69 @@ import { } from '../../types'; export const isResolvedSlotOp = (op?: ResolvedOp | null): op is ResolvedSlotOp => { - if (op && 'SlotOp' in op) { - console.log('isResolvedSlotOp >>> ', JSON.stringify(op)); - } - return !!(op && 'SlotOp' in op); }; export const isResolvedExecOp = (op?: ResolvedOp | null): op is ResolvedExecOp => { - if (op && 'ExecOp' in op) { - console.log('isResolvedExecOp >>> ', JSON.stringify(op)); - } - return !!(op && 'ExecOp' in op); }; export const isResolvedChildOp = (op?: ResolvedOp | null): op is ResolvedChildOp => { - if (op && 'ChildOp' in op) { - console.log('isResolvedChildOp >>> ', JSON.stringify(op)); - } - return !!(op && 'ChildOp' in op); }; export const isResolvedLockOp = (op?: ResolvedOp | null): op is ResolvedLockOp => { - if (op && 'LockOp' in op) { - console.log('isResolvedLockOp >>> ', JSON.stringify(op)); - } - return !!(op && 'LockOp' in op); }; export const isResolvedUnlockOp = (op?: ResolvedOp | null): op is ResolvedUnlockOp => { - if (op && 'UnlockOp' in op) { - console.log('isResolvedUnlockOp >>> ', JSON.stringify(op)); - } - return !!(op && 'UnlockOp' in op); }; export const isResolvedBlockOp = (op?: ResolvedOp | null): op is GBlock => { - if (op && 'BlockOp' in op) { - console.log('isResolvedBlockOp >>> ', JSON.stringify(op)); - } - return isResolvedBlock(op as ResolvedOperation); //TODO: Fix contract-kit types }; export const isResolvedStoreOp = (op?: ResolvedOp | null): op is GStore => { - if (op && 'StoreOp' in op) { - console.log('isResolvedStoreOp >>> ', JSON.stringify(op)); - } - return isResolvedStore(op as ResolvedOperation); //TODO: Fix contract-kit types }; export const isResolvedCallOp = (op?: ResolvedOp | null): op is GCall => { - if (op && 'CallOp' in op) { - console.log('isResolvedCallOp >>> ', JSON.stringify(op)); - } - return isResolvedCall(op as ResolvedOperation); //TODO: Fix contract-kit types }; export const isResolvedTakeOp = (op?: ResolvedOp | null): op is GTake => { - if (op && 'TakeOp' in op) { - console.log('isResolvedTakeOp >>> ', JSON.stringify(op)); - } - return isResolvedTake(op as ResolvedOperation); //TODO: Fix contract-kit types }; export const isResolvedReadOp = (op?: ResolvedOp | null): op is GRead => { - if (op && 'ReadOp' in op) { - console.log('isResolvedReadOp >>> ', JSON.stringify(op)); - } - return isResolvedRead(op as ResolvedOperation); //TODO: Fix contract-kit types }; export const isPreparedExecOp = (op?: PreparedOp | null): op is PreparedExecOp => { - if (op && 'ExecOp' in op) { - console.log('isPreparedExecOp >>> ', JSON.stringify(op)); - } - return !!(op && 'ExecOp' in op); }; export const isPreparedBlockOp = (op?: PreparedOp | null): op is GBlock => { - if (op && 'BlockOp' in op) { - console.log('isPreparedBlockOp >>> ', JSON.stringify(op)); - } - 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 => { + 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 => { + return !!(op && 'TakeOp' in op); +}; diff --git a/packages/cwait/src/onchain/utils/uuid.ts b/packages/cwait/src/onchain/utils/uuid.ts index 9921fef..f7d8772 100644 --- a/packages/cwait/src/onchain/utils/uuid.ts +++ b/packages/cwait/src/onchain/utils/uuid.ts @@ -3,9 +3,8 @@ import { context } from '../context'; let next = 0n; export const uuid = () => { - const parentTxId = context.parentTxId; + const id = (BigInt(`0x${context.thisId}`) * 3n + BigInt(`0x${context.parentTxId}`) + next).toString(16).slice(-32); - const id = `${parentTxId}${next.toString(16)}`; next += 1n; return id; diff --git a/packages/cwait/src/types.ts b/packages/cwait/src/types.ts index c0601b9..ca22d77 100644 --- a/packages/cwait/src/types.ts +++ b/packages/cwait/src/types.ts @@ -1,4 +1,13 @@ -import { Claim, ClaimKey, OrdJson, PreparedOperation, ResolvedOperation, User } from '@coinweb/contract-kit'; +import { + Claim, + ClaimKey, + CwebStore, + GStore, + OrdJson, + PreparedOperation, + ResolvedOperation, + User, +} from '@coinweb/contract-kit'; import { LockedKey } from './onchain/mutex'; @@ -11,7 +20,14 @@ export type TypedClaim & { providedCweb?: bigint }; + +export type PreparedOp = + | PreparedOperation + | PreparedExecOp + | PreparedLockOp + | PreparedUnlockOp + | PreparedExtendedStoreOp; export type Task = { op: PreparedOp; @@ -74,6 +102,7 @@ export type ExecutorMethodArgs = [ caller?: User, thisId?: string, parentId?: string, - saveResult?: boolean, + shouldSaveResult?: boolean, takenFundsIds?: string[], + execOpsIndexes?: number[], ]; diff --git a/packages/cwait/tsconfig.json b/packages/cwait/tsconfig.json index 5611da8..e84c22c 100644 --- a/packages/cwait/tsconfig.json +++ b/packages/cwait/tsconfig.json @@ -9,5 +9,5 @@ "esModuleInterop": true, "types": ["vitest/globals"] }, - "include": ["**/*.ts", "vitest.*.ts", "__tests__", "scripts/publish.js", "src/onchain/utils"] + "include": ["**/*.ts", "vitest.*.ts", "__tests__", "scripts/publish.js", "src/onchain/utils", "src/onchain/executor/constructTx"] } diff --git a/packages/ui/.env b/packages/ui/.env index 2964227..2b76c00 100644 --- a/packages/ui/.env +++ b/packages/ui/.env @@ -1,4 +1,4 @@ VITE_API_URL='https://api-cloud.coinweb.io/wallet' VITE_EXPLORER_URL='https://explorer.coinweb.io' -VITE_CONTRACT_ADDRESS="0xbf9ca1687e3441bed1afd495405778a9c06c2f5173829d47d9b87dfb150063d8" +VITE_CONTRACT_ADDRESS="0xc599810e4861b7b1ae25695d58eeef10556fc84f87f72fafa31c0921e86e92be" diff --git a/yarn.lock b/yarn.lock index 0e862fd..2bbc788 100644 --- a/yarn.lock +++ b/yarn.lock @@ -379,16 +379,16 @@ __metadata: languageName: node linkType: hard -"@coinweb/testing-sdk@npm:0.0.9-remote": - version: 0.0.9-remote - resolution: "@coinweb/testing-sdk@npm:0.0.9-remote" +"@coinweb/testing-sdk@npm:0.0.10-mutex": + version: 0.0.10-mutex + resolution: "@coinweb/testing-sdk@npm:0.0.10-mutex" dependencies: "@coinweb/contract-kit": "npm:0.2.0" - "@coinweb/minimal-sdk": "npm:1.2.19" + "@coinweb/minimal-sdk": "npm:1.2.18" json-stable-stringify: "npm:^1.1.1" lodash.isequal: "npm:^4.5.0" secp256k1: "npm:^5.0.0" - checksum: 10c0/80212780455d4bc2c1082d62ce382c6d07c6b5a107dc372f630c2257de686ec7e072c5d7a0b966e611b73f8b49ef9d6d8b68fbd66de1d968992a6b18444f218b + checksum: 10c0/84785ecc631510aa39189792826cff454700cba96d13d6c9f93f4166ee035fb1c8a8c0f2eab0f51d7d5fe127c661c7964792f8dfacffb9bec5eaf3a6da7fc744 languageName: node linkType: hard