feat: add fee management

This commit is contained in:
Alex 2025-04-23 12:38:29 +03:00
parent 732ca0f292
commit 27cd1f50b6
16 changed files with 312 additions and 90 deletions

View File

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

View File

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

View File

@ -3,16 +3,18 @@ import {
constructClaim,
constructClaimKey,
constructStore,
constructTake,
CwebStore,
GRead,
GStore,
constructRead,
} 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 resultKey = '_result';
export const constructResultClaimKey = (id: string) => constructClaimKey([resultKey], [id]);
export const constructResultClaim = (id: string, result: ResolvedOp[]) =>
constructClaim(constructResultClaimKey(id), result, '0x0');
@ -20,8 +22,9 @@ export const constructResultClaim = (id: string, result: ResolvedOp[]) =>
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 constructResultClaimRead = (id: string) => constructRead(context.issuer, constructResultClaimKey(id));
export const constructResultClaimTake = (id: string) => constructTake(constructResultClaimKey(id));
export const constructResultBlockFilter = (id: string): BlockFilter => {
const { first_part: first, second_part: second } = constructResultClaimKey(id);

View File

@ -1,8 +1,11 @@
import { Context, extractContractArgs, extractUser, getMethodArguments, User } from '@coinweb/contract-kit';
import { Context, extractUser, getMethodArguments } from '@coinweb/contract-kit';
import { getCallParameters, getContractIssuer } from 'lib/onchain';
import { ResolvedOp, TypedClaim } from '../../types';
import { isResolvedBlockOp, isResolvedReadOp } from '../utils';
import { ExecutorMethodArgs } from '../../types';
import { uuid } from '../utils';
import { extractFunds } from './extractFunds';
import { extractOps } from './extractOps';
let rawContext: Context | null = null;
@ -18,70 +21,50 @@ export const getRawContext = () => {
return rawContext;
};
const getMethodArgs = () => {
return getMethodArguments(getRawContext()) as [
methodName: string,
initialArgs?: unknown[],
resolvedOps?: ResolvedOp[],
caller?: User,
isChild?: boolean,
];
export const getMethodArgs = () => {
return getMethodArguments(getRawContext()) as [methodName: string, ...ExecutorMethodArgs];
};
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;
};
let thisId: string | undefined;
export const context = {
get isChild() {
return !!getMethodArgs()[4];
return !!getMethodArgs()[5];
},
get thisId() {
thisId = thisId ?? getMethodArgs()[4] ?? uuid();
return thisId;
},
get parentId() {
return getMethodArgs()[5];
},
get ops() {
console.log('Parse ops in context');
const previousOps = getMethodArgs()[2] ?? [];
const resolvedOps = extractResolvedOps();
const { extractedOps } = extractOps();
const allResolvedOps = [...previousOps, ...resolvedOps];
const allResolvedOps = [...previousOps, ...extractedOps];
console.log('new ops >>>', JSON.stringify(resolvedOps));
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,
};
},
get methodName() {
return getMethodArguments(getRawContext())[0] as string;
},
@ -106,4 +89,7 @@ export const context = {
get parentTxId() {
return getRawContext().call.txid;
},
get needSaveResult() {
return getMethodArgs()[6] ?? false;
},
};

View File

@ -0,0 +1,34 @@
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),
};
};

View File

@ -0,0 +1,58 @@
import { extractContractArgs } from '@coinweb/contract-kit';
import { ResolvedOp, TypedClaim } from '../../types';
import { resultKey } from '../claims/result';
import { isResolvedBlockOp, isResolvedReadOp, isResolvedTakeOp } from '../utils';
import { getRawContext } from './context';
export const extractOps = () => {
const contractArgs = extractContractArgs(getRawContext().tx);
if (!contractArgs.length) {
return {
extractedOps: [],
fundsOps: [],
};
}
const [fundsOp, ...resolvedOps] = contractArgs;
if (!isResolvedReadOp(fundsOp)) {
throw new Error('Wrong funds claims');
}
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] === resultKey) {
const nextAfterBlock = resolvedOps[i + 1];
if (!isResolvedTakeOp(nextAfterBlock)) {
throw new Error('Wrong subcall result');
}
extractedOps.push({ ChildOp: 0 }, ...(nextAfterBlock.TakeOp.result as TypedClaim<ResolvedOp[]>).body);
i += 2;
continue;
}
}
extractedOps.push(op);
i++;
}
return {
fundsOp,
extractedOps,
};
};

View File

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

View File

@ -76,7 +76,7 @@ export const cwait = <TAsyncCallback extends (...args: any[]) => Promise<unknown
}
console.log('cwait-error');
throw new Error('Exec operation not found');
throw new Error('Read operation not found');
}
}) as TAsyncCallback;
};

View File

@ -0,0 +1,3 @@
let batchId = 0;
export const getBatchId = () => batchId++;

View File

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

View File

@ -4,30 +4,59 @@ import {
constructContractRef,
FullCallInfo,
NewTx,
passCwebFrom,
PreparedOperation,
sendCwebInterface,
} from '@coinweb/contract-kit';
import { CwebBlock } from '@coinweb/contract-kit/dist/esm/operations/block';
import { GBlock } from '@coinweb/contract-kit/dist/types/operations/generics';
import { ExecutorMethodArgs, ResolvedOp, ResolvedSlotOp, Task } from '../../../types';
import { constructResultBlockFilter, constructResultClaimRead } from '../../claims/result';
import { constructFundsClaimRangRead, constructFundsClaimStore } from '../../claims/funds';
import { constructResultBlockFilter, constructResultClaimTake, resultKey } from '../../claims/result';
import { context, getRawContext } from '../../context';
import { isPreparedBlockOp, isPreparedExecOp } from '../typeGuards';
export const prepareInThreadTxs = ({
cwebPerCall,
outThread,
outThreadTasksCount,
outThreadFee,
tasks,
}: {
tasks: Task[];
cwebPerCall: bigint;
outThread: number;
outThreadTasksCount: number;
outThreadFee: bigint;
}) => {
}): {
txs: NewTx[];
calls: number;
} => {
if (!tasks.length) {
const { constructSendCweb } = sendCwebInterface();
const { availableCweb, takeOps, storedCweb } = context.funds;
const { user } = context;
const restOfAvailableCweb = availableCweb - outThreadFee;
const restOfCweb = restOfAvailableCweb + storedCweb - BigInt(takeOps.length) * 100n - 3000n;
const txs =
restOfCweb > 0n
? [
constructContinueTx(getRawContext(), [
passCwebFrom(context.issuer, restOfAvailableCweb),
...takeOps,
...constructSendCweb(restOfCweb, user, null),
]),
]
: [];
return { txs, calls: 0 };
}
let txFee = 0n;
let callsPrepared = 0;
const resolvedSlotOps = new Array(outThread).fill({ SlotOp: 0 }) satisfies ResolvedSlotOp[];
const resolvedSlotOps = new Array(outThreadTasksCount).fill({ SlotOp: 0 }) satisfies ResolvedSlotOp[];
const resolvedChildOps: ResolvedOp[] = [...context.ops, ...resolvedSlotOps];
@ -47,7 +76,7 @@ export const prepareInThreadTxs = ({
console.log('Child call info');
const id = task.op.ExecOp.id;
callArgs.push(constructBlock([constructResultBlockFilter(id)]), constructResultClaimRead(id));
callArgs.push(constructBlock([constructResultBlockFilter(id)]), constructResultClaimTake(id));
txFee += 200n;
childCalls.push({
@ -59,6 +88,8 @@ export const prepareInThreadTxs = ({
context.initialArgs,
[...resolvedChildOps, { ExecOp: { id } }],
context.user,
id,
context.thisId,
true,
] satisfies ExecutorMethodArgs,
},
@ -82,22 +113,47 @@ export const prepareInThreadTxs = ({
const returnTxs: NewTx[] = [];
if (callArgs.length) {
const { storedCweb, takeOps } = context.funds;
const latestCallArg = callArgs.at(-1)!;
if ('StoreOp' in latestCallArg && (latestCallArg.StoreOp.key.first_part as [string])[0] === 'result') {
if ('StoreOp' in latestCallArg && (latestCallArg.StoreOp.key.first_part as [string])[0] === resultKey) {
//SAVE RESULT CLAIMS
console.log('SAVE RESULT CLAIMS', JSON.stringify(callArgs));
if (callArgs.length > 1) {
throw new Error('Unexpected count of result ops');
}
returnTxs.push(constructContinueTx(getRawContext(), [latestCallArg]));
const resultOps = [];
if (context.needSaveResult) {
resultOps.push(...callArgs);
}
if (context.parentId) {
const { availableCweb, storedCweb, takeOps } = context.funds;
const cwebToStore = availableCweb + storedCweb - BigInt(takeOps.length) * 100n - 500n;
resultOps.push(
constructFundsClaimStore(context.parentId, cwebToStore),
...takeOps,
passCwebFrom(context.issuer, availableCweb)
);
}
if (resultOps.length) {
returnTxs.push(constructContinueTx(getRawContext(), resultOps));
}
} else {
txFee += 700n + outThreadFee;
txFee += 800n + outThreadFee + BigInt(takeOps.length) * 100n;
callsPrepared++;
returnTxs.push(
constructContinueTx(
getRawContext(),
[],
[passCwebFrom(context.issuer, cwebPerCall - outThreadFee), ...takeOps],
[
{
callInfo: {
@ -108,14 +164,17 @@ export const prepareInThreadTxs = ({
context.initialArgs,
[...context.ops, ...resolvedSlotOps],
context.user,
false,
context.thisId,
context.parentId,
context.needSaveResult,
takeOps.map((op) => (op.TakeOp.key.second_part as [string])[0]),
] satisfies ExecutorMethodArgs,
},
contractInfo: {
providedCweb: cwebPerCall - txFee,
providedCweb: cwebPerCall - txFee + storedCweb,
authenticated: null,
},
contractArgs: callArgs,
contractArgs: [constructFundsClaimRangRead(context.thisId), ...callArgs],
},
},
]

View File

@ -1,10 +1,26 @@
import { constructContinueTx, constructContractRef, FullCallInfo, PreparedOperation } from '@coinweb/contract-kit';
import {
constructContinueTx,
constructContractRef,
FullCallInfo,
NewTx,
PreparedOperation,
} from '@coinweb/contract-kit';
import { ExecutorMethodArgs, Task } from '../../../types';
import { context, getRawContext } from '../../context';
import { isPreparedExecOp } from '../typeGuards';
export const prepareOutThreadTxs = ({ tasks, cwebPerCall }: { tasks: Task[]; cwebPerCall: bigint }) => {
export const prepareOutThreadTxs = ({
tasks,
cwebPerCall,
}: {
tasks: Task[];
cwebPerCall: bigint;
}): {
txs: NewTx[];
fee: bigint;
calls: number;
} => {
const siblingCallResolvedOps = [...context.ops];
const siblingTxInfo: {
@ -32,7 +48,9 @@ export const prepareOutThreadTxs = ({ tasks, cwebPerCall }: { tasks: Task[]; cwe
context.initialArgs,
[...siblingCallResolvedOps, { ExecOp: { id } }],
context.user,
true,
id,
context.parentId ?? context.thisId,
false,
] satisfies ExecutorMethodArgs,
},
contractInfo: {
@ -73,5 +91,7 @@ export const prepareOutThreadTxs = ({ tasks, cwebPerCall }: { tasks: Task[]; cwe
constructContinueTx(getRawContext(), ops, calls)
);
txFee += BigInt(siblingTxs.length) * 100n;
return { txs: siblingTxs, fee: txFee, calls: callsPrepared };
};

View File

@ -1,28 +1,48 @@
import { constructContinueTx, constructDataUnverified } from '@coinweb/contract-kit';
import { getCallParameters } from 'lib/onchain';
import { constructContinueTx, NewTx, passCwebFrom, sendCwebInterface } from '@coinweb/contract-kit';
import { getRawContext } from '../../context';
import { context, getRawContext } from '../../context';
import { getAwaitedTasks } from '../../promisifiedOps';
import { prepareInThreadTxs } from './prepareInThreadTxs';
import { prepareOutThreadTxs } from './prepareOutThreadTxs';
import { splitTasks } from './splitTasks';
export const prepareTx = (isFullyExecuted: boolean, callsCount?: number) => {
export const prepareTx = (isFullyExecuted: boolean, callsCount?: number): { txs: NewTx[]; calls: 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 };
if (context.isChild) {
return { txs: [], calls: 0 };
}
const { constructSendCweb } = sendCwebInterface();
const { availableCweb, takeOps, storedCweb } = context.funds;
const { user } = context;
const restOfCweb = availableCweb + storedCweb - BigInt(takeOps.length) * 100n - 3000n;
const txs =
restOfCweb > 0n
? [
constructContinueTx(getRawContext(), [
passCwebFrom(context.issuer, availableCweb),
...takeOps,
...constructSendCweb(restOfCweb, user, null),
]),
]
: [];
return { txs, calls: 0 };
}
const { inThreadTasks, outThreadTasks } = splitTasks(awaitedTasks, isFullyExecuted);
const { availableCweb } = getCallParameters(ctx);
const { availableCweb } = context.funds;
const cwebPerCall = availableCweb / BigInt(callsCount || 1);
const {
txs: outThreadTxs,
@ -33,9 +53,9 @@ export const prepareTx = (isFullyExecuted: boolean, callsCount?: number) => {
const { txs: inThreadTxs, calls: inThreadCallsPrepared } = prepareInThreadTxs({
tasks: inThreadTasks,
cwebPerCall,
outThread: outThreadTasks.length,
outThreadTasksCount: outThreadTasks.length,
outThreadFee,
});
return { tx: [...inThreadTxs, ...outThreadTxs], calls: inThreadCallsPrepared + outThreadCallsPrepared };
return { txs: [...inThreadTxs, ...outThreadTxs], calls: inThreadCallsPrepared + outThreadCallsPrepared };
};

View File

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

View File

@ -41,5 +41,8 @@ export type ExecutorMethodArgs = [
initialArgs?: unknown[],
resolvedOps?: ResolvedOp[],
caller?: User,
isChild?: boolean,
thisId?: string,
parentId?: string,
saveResult?: boolean,
takenFundsIds?: string[],
];

View File

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