diff --git a/packages/cwait/src/onchain/context/context.ts b/packages/cwait/src/onchain/context/context.ts index 273a71a..a056d0e 100644 --- a/packages/cwait/src/onchain/context/context.ts +++ b/packages/cwait/src/onchain/context/context.ts @@ -1,16 +1,39 @@ -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 { pushAwaitedOp } from '../../../dist/onchain/ops'; +import { ExecutorMethodArgs, ResolvedOp } from '../../types'; import { uuid } from '../utils'; -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 +48,106 @@ 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, + saveResult, + takenFundsIds = [], + execOpsIndexes = [], + ] = getMethodArgs(); + + initialContext.isChild = !!parentId; + initialContext.thisId = thisId ?? uuid(); + initialContext.parentId = parentId; + initialContext.methodName = methodName; + initialContext.initialArgs = initialArgs ?? []; + initialContext.needSaveResult = saveResult ?? 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); + console.log('Take Ops: ', fundsTakeOps); + + initialContext.funds = { + availableCweb: availableCweb, + storedCweb: storedCweb, + takeOps: fundsTakeOps, + }; + + if (executionOpsTakeOp) { + pushAwaitedOp(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..d2d3d86 100644 --- a/packages/cwait/src/onchain/context/extractOps.ts +++ b/packages/cwait/src/onchain/context/extractOps.ts @@ -1,18 +1,51 @@ -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 { mutexBlockLockKey, mutexBlockUnlockKey, mutexExecOpsKey, MutexExecOpsResult } 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 +56,9 @@ export const extractOps = () => { } const extractedOps: ResolvedOp[] = []; + let executedOps: MutexExecOpsResult = []; + let executionOpsProfit: bigint = 0n; + let executionOpsTakeOp: GTake | null = null; let i = 0; @@ -34,15 +70,69 @@ export const extractOps = () => { //Maybe it is needed to check more conditions here if (Array.isArray(first) && first[0] === resultKey) { - const nextAfterBlock = resolvedOps[i + 1]; + 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: 0 }, ...(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({ SlotOp: { ok: true } }); + + i += 2; + continue; + } + case mutexBlockUnlockKey: { + const nextAfterBlock = resolvedOps[i + 1]; + + if (!isResolvedTakeOp(nextAfterBlock)) { + throw new Error('Wrong mutex unlock result'); + } + + extractedOps.push({ SlotOp: { ok: true } }); + + 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'); + } + + 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 +141,38 @@ export const extractOps = () => { i++; } + const allOps: ResolvedOp[] = []; + + for (let i = 0; i < resolvedOps.length + executedOps.length; i++) { + if (execOpsIndexes.includes(i)) { + const op = executedOps.shift(); + + if (!op) { + throw new Error('Wrong mutex exec result'); + } + + if (op.ok) { + allOps.push(op.resolved); + } else { + allOps.push({ SlotOp: { ok: false, error: op.error } }); + } + } else { + const op = resolvedOps.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 index 9532673..eb8a1dd 100644 --- a/packages/cwait/src/onchain/executor.ts +++ b/packages/cwait/src/onchain/executor.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Context, NewTx, getMethodArguments, isSelfCall } from '@coinweb/contract-kit'; -import { context, getRawContext, setRawContext } from './context'; +import { context, getRawContext, handleContext } from './context'; import { pushResolvedOp } from './promisifiedOps/resolved'; import { constructTx } from './utils'; @@ -10,13 +10,17 @@ 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); + 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) { + return constructTx(false); + } + const execution = new Promise((resolve, reject) => { abortExecution = resolve; diff --git a/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts b/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts index af1d981..7b75ff1 100644 --- a/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts +++ b/packages/cwait/src/onchain/mutex/calls/constructExecOpsCall.ts @@ -4,7 +4,7 @@ import { CwebTake } from '@coinweb/contract-kit/dist/types/operations/take'; import { PreparedExtendedStoreOp } from '../../../types'; import { constructMutexExecOpsBlock, - constructMutexExecOpsClaimTake, + constructMutexExecOpsClaimRead, constructMutexLockClaimRangeRead, } from '../claims'; import { execOpsMethodName } from '../methods'; @@ -65,6 +65,8 @@ export const constructExecOpsCall = ({ contractArgs: [constructMutexLockClaimRangeRead(issuer)], }, fee: providedCweb + contractArgsFee, - ops: execId ? ([constructMutexExecOpsBlock(execId, issuer), constructMutexExecOpsClaimTake(execId)] as const) : [], + ops: execId + ? ([constructMutexExecOpsBlock(execId, issuer), constructMutexExecOpsClaimRead(issuer, execId)] as const) + : [], }; }; diff --git a/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts b/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts index e3b4d47..a3cd931 100644 --- a/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts +++ b/packages/cwait/src/onchain/mutex/claims/mutexExecOps.ts @@ -3,6 +3,7 @@ import { constructBlock, constructClaim, constructClaimKey, + constructRead, constructStore, constructTake, ContractIssuer, @@ -26,6 +27,9 @@ export const constructMutexExecOpsClaimStore = ( 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 => { diff --git a/packages/cwait/src/onchain/mutex/index.ts b/packages/cwait/src/onchain/mutex/index.ts index 004e773..793cef3 100644 --- a/packages/cwait/src/onchain/mutex/index.ts +++ b/packages/cwait/src/onchain/mutex/index.ts @@ -10,8 +10,10 @@ import { saveExecOpResult, saveExecOpResultMethodName, unlockMethodName, + waitMethodName, } from './methods'; import { preReadExecTakeOps } from './methods/preReadExecTakeOps'; +import { wait } from './methods/wait'; export * from './claims'; export * from './types'; @@ -24,4 +26,5 @@ export const mutexMethods = { [execOpsMethodName]: execOps, [preReadExecTakeOpsMethodName]: preReadExecTakeOps, [saveExecOpResultMethodName]: saveExecOpResult, + [waitMethodName]: wait, }; diff --git a/packages/cwait/src/onchain/mutex/methods/execLock.ts b/packages/cwait/src/onchain/mutex/methods/execLock.ts index 3f79a2f..4d8d5ff 100644 --- a/packages/cwait/src/onchain/mutex/methods/execLock.ts +++ b/packages/cwait/src/onchain/mutex/methods/execLock.ts @@ -13,7 +13,7 @@ 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'; @@ -74,7 +74,7 @@ export const mutexExecLock = (context: Context) => { ref: constructContractRef(issuer, []), methodInfo: { methodName: notifyLockMethodName, - methodArgs: [(lockCandidate.key.second_part as [number, string])[1]], + methodArgs: [0, (lockCandidate.key.second_part as [number, string])[1]] satisfies MutexWaitNotifyArgs, }, contractInfo: { providedCweb: 200n, diff --git a/packages/cwait/src/onchain/mutex/methods/names.ts b/packages/cwait/src/onchain/mutex/methods/names.ts index ce0c305..823cf34 100644 --- a/packages/cwait/src/onchain/mutex/methods/names.ts +++ b/packages/cwait/src/onchain/mutex/methods/names.ts @@ -5,3 +5,4 @@ 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/wait.ts b/packages/cwait/src/onchain/mutex/methods/wait.ts new file mode 100644 index 0000000..af8d489 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/wait.ts @@ -0,0 +1,61 @@ +import { constructContinueTx, constructContractRef, Context } from '@coinweb/contract-kit'; +import { getContractArguments, getContractIssuer } from 'lib/onchain'; + +import { waitSteps } from '../settings'; +import { MutexNotifyLockArgs, MutexWaitNotifyArgs } from '../types'; + +import { notifyLockMethodName } from './names'; + +export const wait = (context: Context) => { + const issuer = getContractIssuer(context); + const [step, lockId] = getContractArguments(context); + + //Including the notifyLock method as step + if (step + 1 < waitSteps) { + return [ + constructContinueTx( + context, + [], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: notifyLockMethodName, + methodArgs: [step + 1, lockId] satisfies MutexWaitNotifyArgs, + }, + contractInfo: { + providedCweb: 200n, + authenticated: null, + }, + contractArgs: [], + }, + }, + ] + ), + ]; + } + + return [ + constructContinueTx( + context, + [], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: notifyLockMethodName, + methodArgs: [lockId] satisfies MutexNotifyLockArgs, + }, + contractInfo: { + providedCweb: 200n, + 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 7116b84..245e8af 100644 --- a/packages/cwait/src/onchain/mutex/types.ts +++ b/packages/cwait/src/onchain/mutex/types.ts @@ -24,6 +24,8 @@ export type MutexUnlockArgs = [lockId: string, timestamp: number, notify?: boole export type MutexExecOpArgs = [ops: (GTake | GStore)[], processId: string, execId?: string]; +export type MutexWaitNotifyArgs = [step: number, lockId: string]; + export type MutexNotifyLockArgs = [lockId: string]; export type MutexPreReadTakeOpsArgs = [execId: string, unavailableIndexes: number[], ops: PreparedOperation[]];