feat: add cwait callstack

This commit is contained in:
Alex 2025-04-22 17:34:28 +03:00
parent bb6d584ed2
commit 732ca0f292
31 changed files with 763 additions and 201 deletions

View File

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

View File

@ -1,5 +1,5 @@
import { constructClaim } from '@coinweb/contract-kit'; 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 { TypedClaim } from '../../../lib/dist/shared/types';
import { AddWordArgs, createWordKey, WordClaimBody } from '../offchain/shared'; import { AddWordArgs, createWordKey, WordClaimBody } from '../offchain/shared';
@ -18,28 +18,37 @@ function hashCode(str: string): string {
return `${(hash < 0 ? hash * -1 : hash).toString(16)}`; 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); const id = hashCode(word);
console.log('await storeOp');
await storeOp(constructClaim(createWordKey(id), { word }, '0x0')); await storeOp(constructClaim(createWordKey(id), { word }, '0x0'));
console.log('await extraLogic');
const wordClaim = await extraLogic(id); const wordClaim = await extraLogic(id);
const newWord1 = (wordClaim?.body.word ?? '') + '!'; const newWord1 = (wordClaim?.body.word ?? '') + '_1';
const newId1 = hashCode(newWord1); const newId1 = hashCode(newWord1);
const newWord2 = (wordClaim?.body.word ?? '') + '!!'; const newWord2 = (wordClaim?.body.word ?? '') + '_2';
const newId2 = hashCode(newWord2); const newId2 = hashCode(newWord2);
const newWord3 = (wordClaim?.body.word ?? '') + '!!!'; const newWord3 = (wordClaim?.body.word ?? '') + '_3';
const newId3 = hashCode(newWord3); 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([ await Promise.all([
storeOp(constructClaim(createWordKey(newId2), { word: newWord2 }, '0x0')), storeOp(constructClaim(createWordKey(newId2), { word: wordClaim?.body.word + '_in_promise_all' }, '0x0')),
storeOp(constructClaim(createWordKey(newId3), { word: newWord3 }, '0x0')), storeOp(constructClaim(createWordKey(newId3), { word: 'WIN' }, '0x0')),
]); ]);
console.log('free readOp');
readOp<TypedClaim<WordClaimBody>>(createWordKey(newId3)); readOp<TypedClaim<WordClaimBody>>(createWordKey(newId3));
};
console.log('addWord END');
});

View File

@ -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 { selfRegisterHandler } from '@coinweb/self-register';
import { addMethodHandler, ContractHandlers, executeHandler, executor } from 'cwait'; import { addMethodHandler, ContractHandlers, executeHandler, executor } from 'cwait';
import { queue } from 'lib/onchain';
import { PUBLIC_METHODS } from '../offchain/shared'; import { PUBLIC_METHODS } from '../offchain/shared';
@ -14,8 +13,6 @@ const createModule = (): ContractHandlers => {
addMethodHandler(module, SELF_REGISTER_HANDLER_NAME, selfRegisterHandler); addMethodHandler(module, SELF_REGISTER_HANDLER_NAME, selfRegisterHandler);
queue.applyQueue(module as CKContractHandlers, [PUBLIC_METHODS.ADD_WORD]);
return module; return module;
}; };

View File

@ -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'; 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<TypedClaim<WordClaimBody>>(createWordKey(id)); const result = await readOp<TypedClaim<WordClaimBody>>(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);
});

View File

@ -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<TypedClaim<WordClaimBody>>(createWordKey(id));
console.log('extraLogic2 END');
return result;
});

View File

@ -17,3 +17,7 @@ declare module '@coinweb/contract-kit/dist/esm/operations/block' {
declare module '@coinweb/contract-kit/dist/esm/operations/call' { declare module '@coinweb/contract-kit/dist/esm/operations/call' {
export * from '@coinweb/contract-kit/dist/types/operations/call'; export * from '@coinweb/contract-kit/dist/types/operations/call';
} }
declare module '@coinweb/contract-kit/dist/esm/operations/read' {
export * from '@coinweb/contract-kit/dist/types/operations/read';
}

View File

@ -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<CwebStore> =>
constructStore(constructResultClaim(id, result));
export const constructResultClaimRead = (id: string): GRead<CwebRead> =>
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,
};
};

View File

@ -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; let rawContext: Context | null = null;
@ -14,14 +18,92 @@ export const getRawContext = () => {
return rawContext; 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<ResolvedOp[]>).body
);
i += 2;
continue;
}
}
extractedOps.push(op);
i++;
}
return extractedOps;
};
export const context = { export const context = {
get issuer() { get isChild() {
return constructContractIssuer(getContractId(getRawContext().tx)); return !!getMethodArgs()[4];
}, },
get authenticated() { get ops() {
return getAuthenticated(getRawContext().tx); 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() { 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;
}, },
}; };

View File

@ -1,67 +1,34 @@
import { /* eslint-disable @typescript-eslint/no-explicit-any */
Context, import { Context, NewTx, getMethodArguments, isSelfCall } from '@coinweb/contract-kit';
extractContractArgs,
NewTx,
getMethodArguments,
constructContinueTx,
constructContractRef,
constructDataUnverified,
isSelfCall,
} from '@coinweb/contract-kit';
import { getCallParameters, queue } from 'lib/onchain';
import { ResolvedOp, Task } from '../types';
import { context, getRawContext, setRawContext } from './context'; import { context, getRawContext, setRawContext } from './context';
import { getAwaitedOps } from './promisifiedOps/awaited';
import { pushResolvedOp } from './promisifiedOps/resolved'; import { pushResolvedOp } from './promisifiedOps/resolved';
import { constructTx } from './utils';
let abortExecution: ((result: boolean) => void) | null = null; let abortExecution: ((result: boolean) => void) | null = null;
const handleState = () => { export const executor = (method: (...args: any[]) => Promise<void>) => {
const ctx = getRawContext(); return async (ctx: Context): Promise<NewTx[]> => {
console.log('executor-start >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
const resolvedOps = extractContractArgs(ctx.tx);
const methodArgs = getMethodArguments(ctx) as [unknown, unknown[], ResolvedOp[]];
const initialArgs = methodArgs[1] ?? [];
const previousOps = methodArgs[2] ?? [];
const allResolvedOps = [...previousOps, ...resolvedOps];
pushResolvedOp(allResolvedOps);
return {
args: initialArgs,
methodName: methodArgs[0] as string,
ops: allResolvedOps,
isChildCall: previousOps.length > 0,
};
};
export const executor =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(method: (...args: any[]) => Promise<void>) =>
async (ctx: Context): Promise<NewTx[]> => {
console.log('executor-start');
setRawContext(ctx); setRawContext(ctx);
pushResolvedOp(context.ops);
const { args, methodName, ops, isChildCall } = handleState(); if (getMethodArguments(getRawContext()).length > 2 && !isSelfCall(ctx)) {
throw new Error('Wrong contract call, check the call arguments');
if (isChildCall && !isSelfCall(ctx)) {
throw new Error('Only contract itself can call it');
} }
const execution = new Promise<boolean>((resolve, reject) => { const execution = new Promise<boolean>((resolve, reject) => {
abortExecution = resolve; abortExecution = resolve;
method(...args).then( method(...context.initialArgs).then(
() => { () => {
console.log('executor-resolved'); console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-resolved');
resolve(true); resolve(true);
}, },
() => { (error) => {
console.log('executor-rejected'); console.log(' <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<executor-rejected');
reject(); console.log(error);
reject(error);
} }
); );
}); });
@ -72,62 +39,18 @@ export const executor =
abortExecution?.(false); abortExecution?.(false);
}, 0); }, 0);
try {
const isFullyExecuted = await execution; const isFullyExecuted = await execution;
return constructTx(isFullyExecuted);
console.log('executor-executed'); } catch (error) {
console.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< executor-error');
const { authInfo, availableCweb } = getCallParameters(ctx); console.log((error as Error).message);
throw error;
const awaitedOps = getAwaitedOps();
if (!awaitedOps.length) {
return [constructContinueTx(ctx, [constructDataUnverified({ isFullyExecuted })]), ...queue.gateway.unlock(ctx)];
} }
};
const callArgs: Task[] = []; };
if (!isFullyExecuted) { export const stopExecution = () => {
callArgs.push(awaitedOps.pop()!); console.log('stopExecution');
} abortExecution?.(false);
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),
},
},
]
),
];
}; };

View File

@ -1,7 +1,9 @@
import { markOpBatch } from './promisifiedOps/awaited'; import { markTaskBatch } from './promisifiedOps/awaited';
export const opMarker = Symbol('opMarker'); export const opMarker = Symbol('opMarker');
let batchId = 0;
const wrapPromiseUtil = const wrapPromiseUtil =
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
<T extends (values: any) => any>(nativePromiseUtil: T): T => <T extends (values: any) => any>(nativePromiseUtil: T): T =>
@ -14,7 +16,7 @@ const wrapPromiseUtil =
}); });
if (argsWithMarker > 0) { if (argsWithMarker > 0) {
markOpBatch(argsWithMarker); markTaskBatch(argsWithMarker, batchId++);
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return

View File

@ -1,20 +1,17 @@
import { PreparedOperation } from '@coinweb/contract-kit'; import { PreparedOp, Task } from '../../types';
import { Task } from '../../types'; const awaitedTasks: Task[] = [];
import { getStack } from '../utils';
const awaitedOps: Task[] = []; export const pushAwaitedTask = (op: PreparedOp) => {
awaitedTasks.push({ op, batchId: -1 });
export const pushAwaitedOp = (op: PreparedOperation) => {
const stack = getStack({ skip: 2 });
awaitedOps.push({ op, stack, isBatch: false });
}; };
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++) { for (let i = 1; i <= count; i++) {
awaitedOps.at(i * -1)!.isBatch = true; awaitedTasks.at(i * -1)!.batchId = batchId;
} }
}; };
export const getAwaitedTasksCount = () => awaitedTasks.length;

View File

@ -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 = <TAsyncCallback extends (...args: any[]) => Promise<unknown>>(asyncCallback: TAsyncCallback) => {
console.log('cwait: ', asyncCallback.name);
let isRoot = false;
return (async (...args: Parameters<TAsyncCallback>) => {
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<unknown> & { [opMarker]: boolean };
result[opMarker] = true;
return result as ReturnType<TAsyncCallback>;
} 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;
};

View File

@ -1,3 +1,4 @@
export * from './awaited'; export * from './awaited';
export * from './resolved'; export * from './cwait';
export * from './ops'; export * from './ops';
export * from './resolved';

View File

@ -2,7 +2,7 @@ import { BlockFilter, constructBlock, extractBlock } from '@coinweb/contract-kit
import { opMarker } from '../../global'; import { opMarker } from '../../global';
import { isResolvedBlockOp, isResolvedSlotOp } from '../../utils'; import { isResolvedBlockOp, isResolvedSlotOp } from '../../utils';
import { pushAwaitedOp } from '../awaited'; import { pushAwaitedTask } from '../awaited';
import { shiftResolvedOp } from '../resolved'; import { shiftResolvedOp } from '../resolved';
export const blockOp = (filters: BlockFilter[]) => { export const blockOp = (filters: BlockFilter[]) => {
@ -14,7 +14,7 @@ export const blockOp = (filters: BlockFilter[]) => {
const { op, isOp } = shiftResolvedOp(); const { op, isOp } = shiftResolvedOp();
if (!isOp) { if (!isOp) {
pushAwaitedOp(constructBlock(filters)); pushAwaitedTask(constructBlock(filters));
opMarkerValue = true; opMarkerValue = true;
} else { } else {
if (isResolvedSlotOp(op)) { if (isResolvedSlotOp(op)) {

View File

@ -5,7 +5,7 @@ import { TypedClaim } from '../../../types';
import { context } from '../../context'; import { context } from '../../context';
import { opMarker } from '../../global'; import { opMarker } from '../../global';
import { isResolvedReadOp, isResolvedSlotOp } from '../../utils'; import { isResolvedReadOp, isResolvedSlotOp } from '../../utils';
import { pushAwaitedOp } from '../awaited'; import { pushAwaitedTask } from '../awaited';
import { shiftResolvedOp } from '../resolved'; import { shiftResolvedOp } from '../resolved';
export const rangeReadOp = <TClaims extends Claim[] = TypedClaim[]>( export const rangeReadOp = <TClaims extends Claim[] = TypedClaim[]>(
@ -21,7 +21,7 @@ export const rangeReadOp = <TClaims extends Claim[] = TypedClaim[]>(
const { op, isOp } = shiftResolvedOp(); const { op, isOp } = shiftResolvedOp();
if (!isOp) { if (!isOp) {
pushAwaitedOp(constructRangeRead(context.issuer, firstPart, range, maxCount)); pushAwaitedTask(constructRangeRead(context.issuer, firstPart, range, maxCount));
opMarkerValue = true; opMarkerValue = true;
} else { } else {
if (isResolvedSlotOp(op)) { if (isResolvedSlotOp(op)) {

View File

@ -4,7 +4,7 @@ import { TypedClaim } from '../../../types';
import { context } from '../../context'; import { context } from '../../context';
import { opMarker } from '../../global'; import { opMarker } from '../../global';
import { isResolvedReadOp, isResolvedSlotOp } from '../../utils'; import { isResolvedReadOp, isResolvedSlotOp } from '../../utils';
import { pushAwaitedOp } from '../awaited'; import { pushAwaitedTask } from '../awaited';
import { shiftResolvedOp } from '../resolved'; import { shiftResolvedOp } from '../resolved';
export const readOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => { export const readOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => {
@ -16,7 +16,7 @@ export const readOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => {
const { op, isOp } = shiftResolvedOp(); const { op, isOp } = shiftResolvedOp();
if (!isOp) { if (!isOp) {
pushAwaitedOp(constructRead(context.issuer, key)); pushAwaitedTask(constructRead(context.issuer, key));
opMarkerValue = true; opMarkerValue = true;
} else { } else {
if (isResolvedSlotOp(op)) { if (isResolvedSlotOp(op)) {

View File

@ -3,7 +3,7 @@ import { extractStore } from '@coinweb/contract-kit/dist/esm/operations/store';
import { opMarker } from '../../global'; import { opMarker } from '../../global';
import { isResolvedSlotOp, isResolvedStoreOp } from '../../utils'; import { isResolvedSlotOp, isResolvedStoreOp } from '../../utils';
import { pushAwaitedOp } from '../awaited'; import { pushAwaitedTask } from '../awaited';
import { shiftResolvedOp } from '../resolved'; import { shiftResolvedOp } from '../resolved';
export const storeOp = (claim: Claim) => { export const storeOp = (claim: Claim) => {
@ -15,7 +15,7 @@ export const storeOp = (claim: Claim) => {
const { op, isOp } = shiftResolvedOp(); const { op, isOp } = shiftResolvedOp();
if (!isOp) { if (!isOp) {
pushAwaitedOp(constructStore(claim)); pushAwaitedTask(constructStore(claim));
opMarkerValue = true; opMarkerValue = true;
} else { } else {
if (isResolvedSlotOp(op)) { if (isResolvedSlotOp(op)) {

View File

@ -3,7 +3,7 @@ import { Claim, ClaimKey, constructTake, extractTake } from '@coinweb/contract-k
import { TypedClaim } from '../../../types'; import { TypedClaim } from '../../../types';
import { opMarker } from '../../global'; import { opMarker } from '../../global';
import { isResolvedSlotOp, isResolvedTakeOp } from '../../utils'; import { isResolvedSlotOp, isResolvedTakeOp } from '../../utils';
import { pushAwaitedOp } from '../awaited'; import { pushAwaitedTask } from '../awaited';
import { shiftResolvedOp } from '../resolved'; import { shiftResolvedOp } from '../resolved';
export const takeOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => { export const takeOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => {
@ -15,7 +15,7 @@ export const takeOp = <TClaim extends Claim = TypedClaim>(key: ClaimKey) => {
const { op, isOp } = shiftResolvedOp(); const { op, isOp } = shiftResolvedOp();
if (!isOp) { if (!isOp) {
pushAwaitedOp(constructTake(key)); pushAwaitedTask(constructTake(key));
opMarkerValue = true; opMarkerValue = true;
} else { } else {
if (isResolvedSlotOp(op)) { if (isResolvedSlotOp(op)) {

View File

@ -2,6 +2,9 @@ import { ResolvedOp } from '../../types';
const resolvedOps: ResolvedOp[] = []; const resolvedOps: ResolvedOp[] = [];
let usedOps: ResolvedOp[] = [];
let isSavingUsed = false;
export const pushResolvedOp = (op: ResolvedOp | ResolvedOp[]) => { export const pushResolvedOp = (op: ResolvedOp | ResolvedOp[]) => {
if (Array.isArray(op)) { if (Array.isArray(op)) {
resolvedOps.push(...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, isOp: resolvedOps.length > 0,
op: resolvedOps.shift(), op: resolvedOps.shift(),
}) as } as
| { | {
isOp: true; isOp: true;
op: ResolvedOp | null; op: ResolvedOp | null;
@ -23,3 +26,17 @@ export const shiftResolvedOp = () =>
isOp: false; isOp: false;
op: undefined; op: undefined;
}; };
if (isSavingUsed && result.op) {
usedOps.push(result.op);
}
return result;
};
export const getUsedOps = () => usedOps;
export const saveUsedOps = () => {
usedOps = [];
isSavingUsed = true;
};

View File

@ -0,0 +1,9 @@
import { prepareTx } from './prepareTxs';
export const constructTx = (isFullyExecuted: boolean) => {
const { calls } = prepareTx(isFullyExecuted);
const { tx } = prepareTx(isFullyExecuted, calls);
return tx;
};

View File

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

View File

@ -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<CwebBlock>; 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 };
};

View File

@ -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 };
};

View File

@ -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 };
};

View File

@ -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 };
};

View File

@ -1,2 +1,4 @@
export * from './callstack'; export * from './callstack';
export * from './constructTx';
export * from './typeGuards'; export * from './typeGuards';
export * from './uuid';

View File

@ -4,47 +4,93 @@ import {
isResolvedRead, isResolvedRead,
isResolvedStore, isResolvedStore,
isResolvedTake, isResolvedTake,
ResolvedOperation,
ResolvedRead, ResolvedRead,
ResolvedTake, ResolvedTake,
} from '@coinweb/contract-kit'; } 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 { CwebCallRefResolved, isResolvedCall } from '@coinweb/contract-kit/dist/esm/operations/call';
import { ResolvedBlock } from '@coinweb/contract-kit/dist/types/operations/block'; 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 { 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); 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<ResolvedBlock> => { export const isResolvedBlockOp = (op?: ResolvedOp | null): op is GBlock<ResolvedBlock> => {
console.log('isResolvedBlockOp', JSON.stringify(op)); if (op && 'BlockOp' in op) {
console.log('isResolvedBlockOp', !!(op && !isResolvedSlotOp(op) && isResolvedBlock(op))); console.log('isResolvedBlockOp >>> ', JSON.stringify(op));
return !!(op && !isResolvedSlotOp(op) && isResolvedBlock(op)); }
return isResolvedBlock(op as ResolvedOperation); //TODO: Fix contract-kit types
}; };
export const isResolvedStoreOp = (op?: ResolvedOp | null): op is GStore<CwebStore> => { export const isResolvedStoreOp = (op?: ResolvedOp | null): op is GStore<CwebStore> => {
console.log('isResolvedStoreOp', JSON.stringify(op)); if (op && 'StoreOp' in op) {
console.log('isResolvedStoreOp', !!(op && !isResolvedSlotOp(op) && isResolvedStore(op))); console.log('isResolvedStoreOp >>> ', JSON.stringify(op));
return !!(op && !isResolvedSlotOp(op) && isResolvedStore(op)); }
return isResolvedStore(op as ResolvedOperation); //TODO: Fix contract-kit types
}; };
export const isResolvedCallOp = (op?: ResolvedOp | null): op is GCall<CwebCallRefResolved> => { export const isResolvedCallOp = (op?: ResolvedOp | null): op is GCall<CwebCallRefResolved> => {
console.log('isResolvedCallOp', JSON.stringify(op)); if (op && 'CallOp' in op) {
console.log('isResolvedCallOp', !!(op && !isResolvedSlotOp(op) && isResolvedCall(op))); console.log('isResolvedCallOp >>> ', JSON.stringify(op));
return !!(op && !isResolvedSlotOp(op) && isResolvedCall(op)); }
return isResolvedCall(op as ResolvedOperation); //TODO: Fix contract-kit types
}; };
export const isResolvedTakeOp = (op?: ResolvedOp | null): op is GTake<ResolvedTake> => { export const isResolvedTakeOp = (op?: ResolvedOp | null): op is GTake<ResolvedTake> => {
console.log('isResolvedTakeOp', JSON.stringify(op)); if (op && 'TakeOp' in op) {
console.log('isResolvedTakeOp', !!(op && !isResolvedSlotOp(op) && isResolvedTake(op))); console.log('isResolvedTakeOp >>> ', JSON.stringify(op));
return !!(op && !isResolvedSlotOp(op) && isResolvedTake(op)); }
return isResolvedTake(op as ResolvedOperation); //TODO: Fix contract-kit types
}; };
export const isResolvedReadOp = (op?: ResolvedOp | null): op is GRead<ResolvedRead> => { export const isResolvedReadOp = (op?: ResolvedOp | null): op is GRead<ResolvedRead> => {
console.log('isResolvedReadOp', JSON.stringify(op)); if (op && 'ReadOp' in op) {
console.log('isResolvedReadOp', !!(op && !isResolvedSlotOp(op) && isResolvedRead(op))); console.log('isResolvedReadOp >>> ', JSON.stringify(op));
return !!(op && !isResolvedSlotOp(op) && isResolvedRead(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<CwebBlock> => {
if (op && 'BlockOp' in op) {
console.log('isPreparedBlockOp >>> ', JSON.stringify(op));
}
return !!(op && 'BlockOp' in op);
}; };

View File

@ -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;
};

View File

@ -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}`; export type HexBigInt = `0x${string}`;
@ -8,14 +8,38 @@ export type TypedClaim<TBody extends OrdJson = OrdJson, TKey extends ClaimKey =
key: TKey; key: TKey;
}; };
export type Task = { export type ResolvedSlotOp = {
op: PreparedOperation;
stack: string;
isBatch: boolean;
};
export type SlotOp = {
SlotOp: 0; SlotOp: 0;
}; };
export type ResolvedOp = ResolvedOperation | SlotOp; export type ResolvedExecOp = {
ExecOp: {
id: string;
};
};
export type ResolvedChildOp = {
ChildOp: 0;
};
export type ResolvedOp = ResolvedOperation | ResolvedSlotOp | ResolvedExecOp | ResolvedChildOp;
export type PreparedExecOp = {
ExecOp: {
id: string;
};
};
export type PreparedOp = PreparedOperation | PreparedExecOp;
export type Task = {
op: PreparedOp;
batchId: number;
};
export type ExecutorMethodArgs = [
initialArgs?: unknown[],
resolvedOps?: ResolvedOp[],
caller?: User,
isChild?: boolean,
];

View File

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