diff --git a/packages/contract.cm/src/offchain/shared/constants.ts b/packages/contract.cm/src/offchain/shared/constants.ts index d687481..ddb5d35 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: 10000n, + ADD_WORD: 100000n, }; diff --git a/packages/contract.cm/src/onchain/addWord.ts b/packages/contract.cm/src/onchain/addWord.ts index 0b4ed0d..715dbda 100644 --- a/packages/contract.cm/src/onchain/addWord.ts +++ b/packages/contract.cm/src/onchain/addWord.ts @@ -1,5 +1,5 @@ import { constructClaim } from '@coinweb/contract-kit'; -import { readOp, storeOp } from 'cwait'; +import { readOp, storeOp, cwait } from 'cwait'; import { TypedClaim } from '../../../lib/dist/shared/types'; import { AddWordArgs, createWordKey, WordClaimBody } from '../offchain/shared'; @@ -18,28 +18,37 @@ function hashCode(str: string): string { return `${(hash < 0 ? hash * -1 : hash).toString(16)}`; } -export const addWord = async (...[word]: AddWordArgs) => { +export const addWord = cwait(async (...[word]: AddWordArgs) => { + console.log('addWord START'); + const id = hashCode(word); + console.log('await storeOp'); await storeOp(constructClaim(createWordKey(id), { word }, '0x0')); + console.log('await extraLogic'); const wordClaim = await extraLogic(id); - const newWord1 = (wordClaim?.body.word ?? '') + '!'; + const newWord1 = (wordClaim?.body.word ?? '') + '_1'; const newId1 = hashCode(newWord1); - const newWord2 = (wordClaim?.body.word ?? '') + '!!'; + const newWord2 = (wordClaim?.body.word ?? '') + '_2'; const newId2 = hashCode(newWord2); - const newWord3 = (wordClaim?.body.word ?? '') + '!!!'; + const newWord3 = (wordClaim?.body.word ?? '') + '_3'; const newId3 = hashCode(newWord3); - storeOp(constructClaim(createWordKey(newId1), { word: newWord1 }, '0x0')); + console.log('free storeOp'); + storeOp(constructClaim(createWordKey(newId1), { word: wordClaim?.body.word + '_free' }, '0x0')); + console.log('await Promise.all'); await Promise.all([ - storeOp(constructClaim(createWordKey(newId2), { word: newWord2 }, '0x0')), - storeOp(constructClaim(createWordKey(newId3), { word: newWord3 }, '0x0')), + storeOp(constructClaim(createWordKey(newId2), { word: wordClaim?.body.word + '_in_promise_all' }, '0x0')), + storeOp(constructClaim(createWordKey(newId3), { word: 'WIN' }, '0x0')), ]); + console.log('free readOp'); readOp>(createWordKey(newId3)); -}; + + console.log('addWord END'); +}); diff --git a/packages/contract.cm/src/onchain/contract.ts b/packages/contract.cm/src/onchain/contract.ts index 1e010f2..1b5d48d 100644 --- a/packages/contract.cm/src/onchain/contract.ts +++ b/packages/contract.cm/src/onchain/contract.ts @@ -1,7 +1,6 @@ -import { SELF_REGISTER_HANDLER_NAME, ContractHandlers as CKContractHandlers } from '@coinweb/contract-kit'; +import { SELF_REGISTER_HANDLER_NAME } from '@coinweb/contract-kit'; import { selfRegisterHandler } from '@coinweb/self-register'; import { addMethodHandler, ContractHandlers, executeHandler, executor } from 'cwait'; -import { queue } from 'lib/onchain'; import { PUBLIC_METHODS } from '../offchain/shared'; @@ -14,8 +13,6 @@ const createModule = (): ContractHandlers => { addMethodHandler(module, SELF_REGISTER_HANDLER_NAME, selfRegisterHandler); - queue.applyQueue(module as CKContractHandlers, [PUBLIC_METHODS.ADD_WORD]); - return module; }; diff --git a/packages/contract.cm/src/onchain/extraLogic.ts b/packages/contract.cm/src/onchain/extraLogic.ts index da66a75..50c95c0 100644 --- a/packages/contract.cm/src/onchain/extraLogic.ts +++ b/packages/contract.cm/src/onchain/extraLogic.ts @@ -1,9 +1,18 @@ -import { readOp, TypedClaim } from 'cwait'; +import { constructClaim } from '@coinweb/contract-kit'; +import { cwait, readOp, storeOp, TypedClaim } from 'cwait'; import { createWordKey, WordClaimBody } from '../offchain'; -export const extraLogic = async (id: string) => { +import { extraLogic2 } from './extraLogic2'; + +export const extraLogic = cwait(async (id: string) => { + console.log('extraLogic START'); const result = await readOp>(createWordKey(id)); - return result; -}; + await storeOp( + constructClaim(createWordKey(id), { word: result?.body.word.split('').reverse().join('') + '_extraLogic' }, '0x0') + ); + + console.log('extraLogic return extraLogic2'); + return extraLogic2(id); +}); diff --git a/packages/contract.cm/src/onchain/extraLogic2.ts b/packages/contract.cm/src/onchain/extraLogic2.ts new file mode 100644 index 0000000..80eff4a --- /dev/null +++ b/packages/contract.cm/src/onchain/extraLogic2.ts @@ -0,0 +1,13 @@ +import { cwait, readOp, TypedClaim } from 'cwait'; + +import { createWordKey, WordClaimBody } from '../offchain'; + +export const extraLogic2 = cwait(async (id: string) => { + console.log('extraLogic2 START'); + + console.log('await readOp'); + const result = await readOp>(createWordKey(id)); + + console.log('extraLogic2 END'); + return result; +}); diff --git a/packages/cwait/src/contract-kit/declarations.d.ts b/packages/cwait/src/contract-kit/declarations.d.ts index 4a3db87..6acb4f0 100644 --- a/packages/cwait/src/contract-kit/declarations.d.ts +++ b/packages/cwait/src/contract-kit/declarations.d.ts @@ -17,3 +17,7 @@ declare module '@coinweb/contract-kit/dist/esm/operations/block' { declare module '@coinweb/contract-kit/dist/esm/operations/call' { export * from '@coinweb/contract-kit/dist/types/operations/call'; } + +declare module '@coinweb/contract-kit/dist/esm/operations/read' { + export * from '@coinweb/contract-kit/dist/types/operations/read'; +} diff --git a/packages/cwait/src/onchain/claims/index.ts b/packages/cwait/src/onchain/claims/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/packages/cwait/src/onchain/claims/result.ts b/packages/cwait/src/onchain/claims/result.ts new file mode 100644 index 0000000..b101dcf --- /dev/null +++ b/packages/cwait/src/onchain/claims/result.ts @@ -0,0 +1,34 @@ +import { + BlockFilter, + constructClaim, + constructClaimKey, + constructStore, + CwebStore, + GRead, + GStore, +} from '@coinweb/contract-kit'; +import { constructRead, CwebRead } from '@coinweb/contract-kit/dist/esm/operations/read'; + +import { ResolvedOp } from '../../types'; +import { context } from '../context'; + +export const constructResultClaimKey = (id: string) => constructClaimKey(['result'], [id]); + +export const constructResultClaim = (id: string, result: ResolvedOp[]) => + constructClaim(constructResultClaimKey(id), result, '0x0'); + +export const constructResultClaimStore = (id: string, result: ResolvedOp[]): GStore => + constructStore(constructResultClaim(id, result)); + +export const constructResultClaimRead = (id: string): GRead => + constructRead(context.issuer, constructResultClaimKey(id)); + +export const constructResultBlockFilter = (id: string): BlockFilter => { + const { first_part: first, second_part: second } = constructResultClaimKey(id); + + return { + issuer: context.issuer, + first, + second, + }; +}; diff --git a/packages/cwait/src/onchain/context/context.ts b/packages/cwait/src/onchain/context/context.ts index 69faa16..3401c69 100644 --- a/packages/cwait/src/onchain/context/context.ts +++ b/packages/cwait/src/onchain/context/context.ts @@ -1,4 +1,8 @@ -import { constructContractIssuer, Context, extractUser, getAuthenticated, getContractId } from '@coinweb/contract-kit'; +import { Context, extractContractArgs, extractUser, getMethodArguments, User } from '@coinweb/contract-kit'; +import { getCallParameters, getContractIssuer } from 'lib/onchain'; + +import { ResolvedOp, TypedClaim } from '../../types'; +import { isResolvedBlockOp, isResolvedReadOp } from '../utils'; let rawContext: Context | null = null; @@ -14,14 +18,92 @@ export const getRawContext = () => { return rawContext; }; +const getMethodArgs = () => { + return getMethodArguments(getRawContext()) as [ + methodName: string, + initialArgs?: unknown[], + resolvedOps?: ResolvedOp[], + caller?: User, + isChild?: boolean, + ]; +}; + +const extractResolvedOps = () => { + const resolvedOps = extractContractArgs(getRawContext().tx); + + const extractedOps: ResolvedOp[] = []; + + let i = 0; + + while (i < resolvedOps.length) { + const op = resolvedOps[i]; + + if (isResolvedBlockOp(op)) { + const { first } = op.BlockOp.blocks_on[0][0]; + + //Maybe it is needed to check more conditions here + if (Array.isArray(first) && first[0] === 'result') { + const nextAfterBlock = resolvedOps[i + 1]; + + if (!isResolvedReadOp(nextAfterBlock)) { + throw new Error('Wrong subcall result'); + } + + extractedOps.push( + { ChildOp: 0 }, + ...(nextAfterBlock.ReadOp.results[0].content as TypedClaim).body + ); + + i += 2; + continue; + } + } + + extractedOps.push(op); + i++; + } + + return extractedOps; +}; + export const context = { - get issuer() { - return constructContractIssuer(getContractId(getRawContext().tx)); + get isChild() { + return !!getMethodArgs()[4]; }, - get authenticated() { - return getAuthenticated(getRawContext().tx); + get ops() { + console.log('Parse ops in context'); + + const previousOps = getMethodArgs()[2] ?? []; + const resolvedOps = extractResolvedOps(); + + const allResolvedOps = [...previousOps, ...resolvedOps]; + + console.log('new ops >>>', JSON.stringify(resolvedOps)); + + return allResolvedOps; + }, + get methodName() { + return getMethodArguments(getRawContext())[0] as string; + }, + get initialArgs() { + return getMethodArgs()[1] ?? []; }, get user() { - return extractUser(getAuthenticated(getRawContext().tx)); + const { authInfo } = getCallParameters(getRawContext()); + const provided = getMethodArgs()[3]; + + const user = (authInfo && extractUser(authInfo)) ?? provided; + + if (!user) { + throw new Error('User not found'); + } + + return user; + }, + get issuer() { + return getContractIssuer(getRawContext()); + }, + get parentTxId() { + return getRawContext().call.txid; }, }; diff --git a/packages/cwait/src/onchain/executor.ts b/packages/cwait/src/onchain/executor.ts index 82035cc..f917eee 100644 --- a/packages/cwait/src/onchain/executor.ts +++ b/packages/cwait/src/onchain/executor.ts @@ -1,133 +1,56 @@ -import { - Context, - extractContractArgs, - NewTx, - getMethodArguments, - constructContinueTx, - constructContractRef, - constructDataUnverified, - isSelfCall, -} from '@coinweb/contract-kit'; -import { getCallParameters, queue } from 'lib/onchain'; - -import { ResolvedOp, Task } from '../types'; +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { Context, NewTx, getMethodArguments, isSelfCall } from '@coinweb/contract-kit'; import { context, getRawContext, setRawContext } from './context'; -import { getAwaitedOps } from './promisifiedOps/awaited'; import { pushResolvedOp } from './promisifiedOps/resolved'; +import { constructTx } from './utils'; let abortExecution: ((result: boolean) => void) | null = null; -const handleState = () => { - const ctx = getRawContext(); +export const executor = (method: (...args: any[]) => Promise) => { + return async (ctx: Context): Promise => { + console.log('executor-start >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>'); + setRawContext(ctx); + pushResolvedOp(context.ops); - const resolvedOps = extractContractArgs(ctx.tx); - const methodArgs = getMethodArguments(ctx) as [unknown, unknown[], ResolvedOp[]]; + if (getMethodArguments(getRawContext()).length > 2 && !isSelfCall(ctx)) { + throw new Error('Wrong contract call, check the call arguments'); + } - const initialArgs = methodArgs[1] ?? []; - const previousOps = methodArgs[2] ?? []; - const allResolvedOps = [...previousOps, ...resolvedOps]; + const execution = new Promise((resolve, reject) => { + abortExecution = resolve; - pushResolvedOp(allResolvedOps); + method(...context.initialArgs).then( + () => { + console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-resolved'); + resolve(true); + }, + (error) => { + console.log(' <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 0, + //@ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + os.setTimeout(() => { + 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 executor = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (method: (...args: any[]) => Promise) => - async (ctx: Context): Promise => { - console.log('executor-start'); - setRawContext(ctx); - - const { args, methodName, ops, isChildCall } = handleState(); - - if (isChildCall && !isSelfCall(ctx)) { - throw new Error('Only contract itself can call it'); - } - - const execution = new Promise((resolve, reject) => { - abortExecution = resolve; - - method(...args).then( - () => { - console.log('executor-resolved'); - resolve(true); - }, - () => { - console.log('executor-rejected'); - reject(); - } - ); - }); - - //@ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - os.setTimeout(() => { - abortExecution?.(false); - }, 0); - - const isFullyExecuted = await execution; - - console.log('executor-executed'); - - const { authInfo, availableCweb } = getCallParameters(ctx); - - const awaitedOps = getAwaitedOps(); - - if (!awaitedOps.length) { - return [constructContinueTx(ctx, [constructDataUnverified({ isFullyExecuted })]), ...queue.gateway.unlock(ctx)]; - } - - const callArgs: Task[] = []; - - if (!isFullyExecuted) { - callArgs.push(awaitedOps.pop()!); - } - - if (callArgs[0]?.isBatch) { - while (awaitedOps.at(-1)?.isBatch) { - callArgs.push(awaitedOps.pop()!); - } - } - - const callFee = 700n + BigInt(callArgs.length) * 100n; - const opTxFee = awaitedOps.length ? 100n + BigInt(awaitedOps.length) * 100n : 0n; - - return [ - constructContinueTx( - ctx, - awaitedOps.map(({ op }) => op), - [ - { - callInfo: { - ref: constructContractRef(context.issuer, []), - methodInfo: { - methodName, - methodArgs: [ - args, - [ - ...ops, - ...Array(awaitedOps.length).fill({ SlotOp: 0 } satisfies ResolvedOp), - ] satisfies ResolvedOp[], - isFullyExecuted, - callArgs, - awaitedOps, - ], - }, - contractInfo: { - providedCweb: availableCweb - callFee - opTxFee, - authenticated: authInfo, - }, - contractArgs: callArgs.map(({ op }) => op), - }, - }, - ] - ), - ]; - }; +export const stopExecution = () => { + console.log('stopExecution'); + abortExecution?.(false); +}; diff --git a/packages/cwait/src/onchain/global.ts b/packages/cwait/src/onchain/global.ts index 919289d..e28bf31 100644 --- a/packages/cwait/src/onchain/global.ts +++ b/packages/cwait/src/onchain/global.ts @@ -1,7 +1,9 @@ -import { markOpBatch } from './promisifiedOps/awaited'; +import { markTaskBatch } from './promisifiedOps/awaited'; export const opMarker = Symbol('opMarker'); +let batchId = 0; + const wrapPromiseUtil = // eslint-disable-next-line @typescript-eslint/no-explicit-any any>(nativePromiseUtil: T): T => @@ -14,7 +16,7 @@ const wrapPromiseUtil = }); if (argsWithMarker > 0) { - markOpBatch(argsWithMarker); + markTaskBatch(argsWithMarker, batchId++); } // eslint-disable-next-line @typescript-eslint/no-unsafe-return diff --git a/packages/cwait/src/onchain/promisifiedOps/awaited.ts b/packages/cwait/src/onchain/promisifiedOps/awaited.ts index 4b0a60c..23c1dbc 100644 --- a/packages/cwait/src/onchain/promisifiedOps/awaited.ts +++ b/packages/cwait/src/onchain/promisifiedOps/awaited.ts @@ -1,20 +1,17 @@ -import { PreparedOperation } from '@coinweb/contract-kit'; +import { PreparedOp, Task } from '../../types'; -import { Task } from '../../types'; -import { getStack } from '../utils'; +const awaitedTasks: Task[] = []; -const awaitedOps: Task[] = []; - -export const pushAwaitedOp = (op: PreparedOperation) => { - const stack = getStack({ skip: 2 }); - - awaitedOps.push({ op, stack, isBatch: false }); +export const pushAwaitedTask = (op: PreparedOp) => { + awaitedTasks.push({ op, batchId: -1 }); }; -export const getAwaitedOps = () => awaitedOps; +export const getAwaitedTasks = () => awaitedTasks; -export const markOpBatch = (count: number) => { +export const markTaskBatch = (count: number, batchId: number) => { for (let i = 1; i <= count; i++) { - awaitedOps.at(i * -1)!.isBatch = true; + awaitedTasks.at(i * -1)!.batchId = batchId; } }; + +export const getAwaitedTasksCount = () => awaitedTasks.length; diff --git a/packages/cwait/src/onchain/promisifiedOps/cwait.ts b/packages/cwait/src/onchain/promisifiedOps/cwait.ts new file mode 100644 index 0000000..6af33d5 --- /dev/null +++ b/packages/cwait/src/onchain/promisifiedOps/cwait.ts @@ -0,0 +1,82 @@ +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'; + +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) { + pushAwaitedTask({ + ExecOp: { + id: uuid(), + }, + }); + + const result = new Promise(() => null) as Promise & { [opMarker]: boolean }; + + result[opMarker] = true; + + return result as ReturnType; + } else { + if (isResolvedSlotOp(op)) { + console.log('cwait-slotOp'); + return new Promise(() => null); + } + + if (isResolvedExecOp(op)) { + console.log('cwait-execOp'); + + saveUsedOps(); + + const result = await asyncCallback(...args); + + const awaitedOps = getAwaitedTasks(); + + if (!awaitedOps.length) { + pushAwaitedTask(constructStore(constructResultClaim(op.ExecOp.id, getUsedOps()))); + + stopExecution(); + } + + return result; + } + + if (isResolvedChildOp(op)) { + return asyncCallback(...args); + } + + console.log('cwait-error'); + throw new Error('Exec operation not found'); + } + }) as TAsyncCallback; +}; diff --git a/packages/cwait/src/onchain/promisifiedOps/index.ts b/packages/cwait/src/onchain/promisifiedOps/index.ts index 854cc7e..672baa5 100644 --- a/packages/cwait/src/onchain/promisifiedOps/index.ts +++ b/packages/cwait/src/onchain/promisifiedOps/index.ts @@ -1,3 +1,4 @@ export * from './awaited'; -export * from './resolved'; +export * from './cwait'; export * from './ops'; +export * from './resolved'; diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/block.ts b/packages/cwait/src/onchain/promisifiedOps/ops/block.ts index 799460f..f85fce9 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/block.ts +++ b/packages/cwait/src/onchain/promisifiedOps/ops/block.ts @@ -2,7 +2,7 @@ import { BlockFilter, constructBlock, extractBlock } from '@coinweb/contract-kit import { opMarker } from '../../global'; import { isResolvedBlockOp, isResolvedSlotOp } from '../../utils'; -import { pushAwaitedOp } from '../awaited'; +import { pushAwaitedTask } from '../awaited'; import { shiftResolvedOp } from '../resolved'; export const blockOp = (filters: BlockFilter[]) => { @@ -14,7 +14,7 @@ export const blockOp = (filters: BlockFilter[]) => { const { op, isOp } = shiftResolvedOp(); if (!isOp) { - pushAwaitedOp(constructBlock(filters)); + pushAwaitedTask(constructBlock(filters)); opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts b/packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts index 640d5a2..7b223f6 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts +++ b/packages/cwait/src/onchain/promisifiedOps/ops/rangeRead.ts @@ -5,7 +5,7 @@ import { TypedClaim } from '../../../types'; import { context } from '../../context'; import { opMarker } from '../../global'; import { isResolvedReadOp, isResolvedSlotOp } from '../../utils'; -import { pushAwaitedOp } from '../awaited'; +import { pushAwaitedTask } from '../awaited'; import { shiftResolvedOp } from '../resolved'; export const rangeReadOp = ( @@ -21,7 +21,7 @@ export const rangeReadOp = ( const { op, isOp } = shiftResolvedOp(); if (!isOp) { - pushAwaitedOp(constructRangeRead(context.issuer, firstPart, range, maxCount)); + pushAwaitedTask(constructRangeRead(context.issuer, firstPart, range, maxCount)); opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/read.ts b/packages/cwait/src/onchain/promisifiedOps/ops/read.ts index 514ed42..0e58301 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/read.ts +++ b/packages/cwait/src/onchain/promisifiedOps/ops/read.ts @@ -4,7 +4,7 @@ import { TypedClaim } from '../../../types'; import { context } from '../../context'; import { opMarker } from '../../global'; import { isResolvedReadOp, isResolvedSlotOp } from '../../utils'; -import { pushAwaitedOp } from '../awaited'; +import { pushAwaitedTask } from '../awaited'; import { shiftResolvedOp } from '../resolved'; export const readOp = (key: ClaimKey) => { @@ -16,7 +16,7 @@ export const readOp = (key: ClaimKey) => { const { op, isOp } = shiftResolvedOp(); if (!isOp) { - pushAwaitedOp(constructRead(context.issuer, key)); + pushAwaitedTask(constructRead(context.issuer, key)); opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/store.ts b/packages/cwait/src/onchain/promisifiedOps/ops/store.ts index 0145685..15a5254 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/store.ts +++ b/packages/cwait/src/onchain/promisifiedOps/ops/store.ts @@ -3,7 +3,7 @@ import { extractStore } from '@coinweb/contract-kit/dist/esm/operations/store'; import { opMarker } from '../../global'; import { isResolvedSlotOp, isResolvedStoreOp } from '../../utils'; -import { pushAwaitedOp } from '../awaited'; +import { pushAwaitedTask } from '../awaited'; import { shiftResolvedOp } from '../resolved'; export const storeOp = (claim: Claim) => { @@ -15,7 +15,7 @@ export const storeOp = (claim: Claim) => { const { op, isOp } = shiftResolvedOp(); if (!isOp) { - pushAwaitedOp(constructStore(claim)); + pushAwaitedTask(constructStore(claim)); opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { diff --git a/packages/cwait/src/onchain/promisifiedOps/ops/take.ts b/packages/cwait/src/onchain/promisifiedOps/ops/take.ts index 5c9c507..20041bd 100644 --- a/packages/cwait/src/onchain/promisifiedOps/ops/take.ts +++ b/packages/cwait/src/onchain/promisifiedOps/ops/take.ts @@ -3,7 +3,7 @@ import { Claim, ClaimKey, constructTake, extractTake } from '@coinweb/contract-k import { TypedClaim } from '../../../types'; import { opMarker } from '../../global'; import { isResolvedSlotOp, isResolvedTakeOp } from '../../utils'; -import { pushAwaitedOp } from '../awaited'; +import { pushAwaitedTask } from '../awaited'; import { shiftResolvedOp } from '../resolved'; export const takeOp = (key: ClaimKey) => { @@ -15,7 +15,7 @@ export const takeOp = (key: ClaimKey) => { const { op, isOp } = shiftResolvedOp(); if (!isOp) { - pushAwaitedOp(constructTake(key)); + pushAwaitedTask(constructTake(key)); opMarkerValue = true; } else { if (isResolvedSlotOp(op)) { diff --git a/packages/cwait/src/onchain/promisifiedOps/resolved.ts b/packages/cwait/src/onchain/promisifiedOps/resolved.ts index c3189dd..b53ff78 100644 --- a/packages/cwait/src/onchain/promisifiedOps/resolved.ts +++ b/packages/cwait/src/onchain/promisifiedOps/resolved.ts @@ -2,6 +2,9 @@ import { ResolvedOp } from '../../types'; const resolvedOps: ResolvedOp[] = []; +let usedOps: ResolvedOp[] = []; +let isSavingUsed = false; + export const pushResolvedOp = (op: ResolvedOp | ResolvedOp[]) => { if (Array.isArray(op)) { resolvedOps.push(...op); @@ -10,11 +13,11 @@ export const pushResolvedOp = (op: ResolvedOp | ResolvedOp[]) => { } }; -export const shiftResolvedOp = () => - ({ +export const shiftResolvedOp = () => { + const result = { isOp: resolvedOps.length > 0, op: resolvedOps.shift(), - }) as + } as | { isOp: true; op: ResolvedOp | null; @@ -23,3 +26,17 @@ export const shiftResolvedOp = () => isOp: false; op: undefined; }; + + if (isSavingUsed && result.op) { + usedOps.push(result.op); + } + + return result; +}; + +export const getUsedOps = () => usedOps; + +export const saveUsedOps = () => { + usedOps = []; + isSavingUsed = true; +}; diff --git a/packages/cwait/src/onchain/utils/constructTx/constructTx.ts b/packages/cwait/src/onchain/utils/constructTx/constructTx.ts new file mode 100644 index 0000000..a719045 --- /dev/null +++ b/packages/cwait/src/onchain/utils/constructTx/constructTx.ts @@ -0,0 +1,9 @@ +import { prepareTx } from './prepareTxs'; + +export const constructTx = (isFullyExecuted: boolean) => { + const { calls } = prepareTx(isFullyExecuted); + + const { tx } = prepareTx(isFullyExecuted, calls); + + return tx; +}; diff --git a/packages/cwait/src/onchain/utils/constructTx/index.ts b/packages/cwait/src/onchain/utils/constructTx/index.ts new file mode 100644 index 0000000..889f301 --- /dev/null +++ b/packages/cwait/src/onchain/utils/constructTx/index.ts @@ -0,0 +1 @@ +export * from './constructTx'; diff --git a/packages/cwait/src/onchain/utils/constructTx/prepareInThreadTxs.ts b/packages/cwait/src/onchain/utils/constructTx/prepareInThreadTxs.ts new file mode 100644 index 0000000..51d0ebd --- /dev/null +++ b/packages/cwait/src/onchain/utils/constructTx/prepareInThreadTxs.ts @@ -0,0 +1,132 @@ +import { + constructBlock, + constructContinueTx, + constructContractRef, + FullCallInfo, + NewTx, + PreparedOperation, +} 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 { constructResultBlockFilter, constructResultClaimRead } from '../../claims/result'; +import { context, getRawContext } from '../../context'; +import { isPreparedBlockOp, isPreparedExecOp } from '../typeGuards'; + +export const prepareInThreadTxs = ({ + cwebPerCall, + outThread, + outThreadFee, + tasks, +}: { + tasks: Task[]; + cwebPerCall: bigint; + outThread: number; + outThreadFee: bigint; +}) => { + let txFee = 0n; + let callsPrepared = 0; + const resolvedSlotOps = new Array(outThread).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)]), constructResultClaimRead(id)); + txFee += 200n; + + childCalls.push({ + callInfo: { + ref: constructContractRef(context.issuer, []), + methodInfo: { + methodName: context.methodName, + methodArgs: [ + context.initialArgs, + [...resolvedChildOps, { ExecOp: { id } }], + context.user, + 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 latestCallArg = callArgs.at(-1)!; + + if ('StoreOp' in latestCallArg && (latestCallArg.StoreOp.key.first_part as [string])[0] === 'result') { + if (callArgs.length > 1) { + throw new Error('Unexpected count of result ops'); + } + + returnTxs.push(constructContinueTx(getRawContext(), [latestCallArg])); + } else { + txFee += 700n + outThreadFee; + callsPrepared++; + + returnTxs.push( + constructContinueTx( + getRawContext(), + [], + [ + { + callInfo: { + ref: constructContractRef(context.issuer, []), + methodInfo: { + methodName: context.methodName, + methodArgs: [ + context.initialArgs, + [...context.ops, ...resolvedSlotOps], + context.user, + false, + ] satisfies ExecutorMethodArgs, + }, + contractInfo: { + providedCweb: cwebPerCall - txFee, + authenticated: null, + }, + contractArgs: 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 new file mode 100644 index 0000000..d75d85e --- /dev/null +++ b/packages/cwait/src/onchain/utils/constructTx/prepareOutThreadTxs.ts @@ -0,0 +1,77 @@ +import { constructContinueTx, constructContractRef, FullCallInfo, 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 }) => { + 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, + true, + ] 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) + ); + + 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 new file mode 100644 index 0000000..b89b48c --- /dev/null +++ b/packages/cwait/src/onchain/utils/constructTx/prepareTxs.ts @@ -0,0 +1,41 @@ +import { constructContinueTx, constructDataUnverified } from '@coinweb/contract-kit'; +import { getCallParameters } from 'lib/onchain'; + +import { 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) => { + console.log('Calls Count: ', callsCount); + + const ctx = getRawContext(); + const awaitedTasks = getAwaitedTasks(); + + console.log('Awaited Tasks: ', JSON.stringify(awaitedTasks)); + + if (!awaitedTasks.length) { + return { tx: [constructContinueTx(ctx, [constructDataUnverified({ isFullyExecuted })])], calls: 0 }; + } + + const { inThreadTasks, outThreadTasks } = splitTasks(awaitedTasks, isFullyExecuted); + + const { availableCweb } = getCallParameters(ctx); + 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, + outThread: outThreadTasks.length, + outThreadFee, + }); + + return { tx: [...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 new file mode 100644 index 0000000..ee90bf1 --- /dev/null +++ b/packages/cwait/src/onchain/utils/constructTx/splitTasks.ts @@ -0,0 +1,48 @@ +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 e7b425c..a5e7e5f 100644 --- a/packages/cwait/src/onchain/utils/index.ts +++ b/packages/cwait/src/onchain/utils/index.ts @@ -1,2 +1,4 @@ export * from './callstack'; +export * from './constructTx'; export * from './typeGuards'; +export * from './uuid'; diff --git a/packages/cwait/src/onchain/utils/typeGuards.ts b/packages/cwait/src/onchain/utils/typeGuards.ts index 9df5655..6a5bfa3 100644 --- a/packages/cwait/src/onchain/utils/typeGuards.ts +++ b/packages/cwait/src/onchain/utils/typeGuards.ts @@ -4,47 +4,93 @@ import { isResolvedRead, isResolvedStore, isResolvedTake, + ResolvedOperation, ResolvedRead, ResolvedTake, } from '@coinweb/contract-kit'; +import { CwebBlock } from '@coinweb/contract-kit/dist/esm/operations/block'; import { CwebCallRefResolved, isResolvedCall } from '@coinweb/contract-kit/dist/esm/operations/call'; import { ResolvedBlock } from '@coinweb/contract-kit/dist/types/operations/block'; import { GBlock, GCall, GRead, GStore, GTake } from '@coinweb/contract-kit/dist/types/operations/generics'; -import { ResolvedOp, SlotOp } from '../../types'; +import { PreparedExecOp, PreparedOp, ResolvedChildOp, ResolvedExecOp, ResolvedOp, ResolvedSlotOp } from '../../types'; + +export const isResolvedSlotOp = (op?: ResolvedOp | null): op is ResolvedSlotOp => { + if (op && 'SlotOp' in op) { + console.log('isResolvedSlotOp >>> ', JSON.stringify(op)); + } -export const isResolvedSlotOp = (op?: ResolvedOp | null): op is SlotOp => { - console.log('isResolvedSlotOp', JSON.stringify(op)); - console.log('isResolvedSlotOp', !!(op && 'SlotOp' in 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 isResolvedBlockOp = (op?: ResolvedOp | null): op is GBlock => { - console.log('isResolvedBlockOp', JSON.stringify(op)); - console.log('isResolvedBlockOp', !!(op && !isResolvedSlotOp(op) && isResolvedBlock(op))); - return !!(op && !isResolvedSlotOp(op) && isResolvedBlock(op)); + 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 => { - console.log('isResolvedStoreOp', JSON.stringify(op)); - console.log('isResolvedStoreOp', !!(op && !isResolvedSlotOp(op) && isResolvedStore(op))); - return !!(op && !isResolvedSlotOp(op) && isResolvedStore(op)); + 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 => { - console.log('isResolvedCallOp', JSON.stringify(op)); - console.log('isResolvedCallOp', !!(op && !isResolvedSlotOp(op) && isResolvedCall(op))); - return !!(op && !isResolvedSlotOp(op) && isResolvedCall(op)); + 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 => { - console.log('isResolvedTakeOp', JSON.stringify(op)); - console.log('isResolvedTakeOp', !!(op && !isResolvedSlotOp(op) && isResolvedTake(op))); - return !!(op && !isResolvedSlotOp(op) && isResolvedTake(op)); + 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 => { - console.log('isResolvedReadOp', JSON.stringify(op)); - console.log('isResolvedReadOp', !!(op && !isResolvedSlotOp(op) && isResolvedRead(op))); - return !!(op && !isResolvedSlotOp(op) && isResolvedRead(op)); + 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); }; diff --git a/packages/cwait/src/onchain/utils/uuid.ts b/packages/cwait/src/onchain/utils/uuid.ts new file mode 100644 index 0000000..9921fef --- /dev/null +++ b/packages/cwait/src/onchain/utils/uuid.ts @@ -0,0 +1,12 @@ +import { context } from '../context'; + +let next = 0n; + +export const uuid = () => { + const parentTxId = context.parentTxId; + + 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 027e0bc..d9c7b22 100644 --- a/packages/cwait/src/types.ts +++ b/packages/cwait/src/types.ts @@ -1,4 +1,4 @@ -import { Claim, ClaimKey, OrdJson, PreparedOperation, ResolvedOperation } from '@coinweb/contract-kit'; +import { Claim, ClaimKey, OrdJson, PreparedOperation, ResolvedOperation, User } from '@coinweb/contract-kit'; export type HexBigInt = `0x${string}`; @@ -8,14 +8,38 @@ export type TypedClaim