diff --git a/packages/contract.cm/src/onchain/contract.ts b/packages/contract.cm/src/onchain/contract.ts index 1b5d48d..7cad51e 100644 --- a/packages/contract.cm/src/onchain/contract.ts +++ b/packages/contract.cm/src/onchain/contract.ts @@ -5,12 +5,18 @@ import { addMethodHandler, ContractHandlers, executeHandler, executor } from 'cw import { PUBLIC_METHODS } from '../offchain/shared'; import { addWord } from './addWord'; +import { mutexLock } from './mutex/lock'; +import { mutexUnlock, mutexUnlockExec } from './mutex/unlock'; const createModule = (): ContractHandlers => { const module: ContractHandlers = { handlers: {} }; addMethodHandler(module, PUBLIC_METHODS.ADD_WORD, executor(addWord)); + addMethodHandler(module, '_lock', mutexLock); + addMethodHandler(module, '_unlock', mutexUnlock); + addMethodHandler(module, '_unlock_exec', mutexUnlockExec); + addMethodHandler(module, SELF_REGISTER_HANDLER_NAME, selfRegisterHandler); return module; diff --git a/packages/contract.cm/src/onchain/mutex/access.ts b/packages/contract.cm/src/onchain/mutex/access.ts new file mode 100644 index 0000000..c308620 --- /dev/null +++ b/packages/contract.cm/src/onchain/mutex/access.ts @@ -0,0 +1 @@ +export const mutexAccess = () => {}; diff --git a/packages/contract.cm/src/onchain/mutex/lock.ts b/packages/contract.cm/src/onchain/mutex/lock.ts new file mode 100644 index 0000000..a4d2b64 --- /dev/null +++ b/packages/contract.cm/src/onchain/mutex/lock.ts @@ -0,0 +1,43 @@ +import { + constructClaim, + constructClaimKey, + constructContinueTx, + constructContractIssuer, + constructContractRef, + constructStore, + Context, + extractContractInfo, + getContractId, +} from '@coinweb/contract-kit'; + +export const mutexLock = (context: Context) => { + const { providedCweb } = extractContractInfo(context.tx); + const issuer = constructContractIssuer(getContractId(context.tx)); + + return [ + constructContinueTx( + context, + [ + constructStore( + constructClaim(constructClaimKey('mutex', context.call.txid.slice(0, 8)), { word: 'CLASH' }, '0x0') + ), + ], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: '_unlock', + methodArgs: [], + }, + contractInfo: { + providedCweb: providedCweb ? providedCweb - 1000n : 1000n, + authenticated: null, + }, + contractArgs: [], + }, + }, + ] + ), + ]; +}; diff --git a/packages/contract.cm/src/onchain/mutex/unlock.ts b/packages/contract.cm/src/onchain/mutex/unlock.ts new file mode 100644 index 0000000..2f3ccdb --- /dev/null +++ b/packages/contract.cm/src/onchain/mutex/unlock.ts @@ -0,0 +1,59 @@ +import { + constructContinueTx, + constructContractIssuer, + constructContractRef, + constructRangeRead, + constructTake, + Context, + extractContractArgs, + extractContractInfo, + extractRead, + getContractId, +} from '@coinweb/contract-kit'; + +export const mutexUnlock = (context: Context) => { + const { providedCweb } = extractContractInfo(context.tx); + const issuer = constructContractIssuer(getContractId(context.tx)); + return [ + constructContinueTx( + context, + [], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: '_unlock_exec', + methodArgs: [], + }, + contractInfo: { + providedCweb: (providedCweb ? providedCweb - 1000n : 1000n) / 2n, + authenticated: null, + }, + contractArgs: [constructRangeRead(issuer, 'mutex', {}, 10000)], + }, + }, + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: '_unlock', + methodArgs: [], + }, + contractInfo: { + providedCweb: (providedCweb ? providedCweb - 1000n : 1000n) / 2n, + authenticated: null, + }, + contractArgs: [], + }, + }, + ] + ), + ]; +}; + +export const mutexUnlockExec = (context: Context) => { + const claim = extractContractArgs(context.tx)[0]!; + const take = extractRead(claim)![0]; + return [constructContinueTx(context, [constructTake(take.content.key)])]; +}; diff --git a/packages/cwait/src/onchain/mutex/claims/access.ts b/packages/cwait/src/onchain/mutex/claims/access.ts new file mode 100644 index 0000000..3c1c9ae --- /dev/null +++ b/packages/cwait/src/onchain/mutex/claims/access.ts @@ -0,0 +1,26 @@ +import { + constructClaim, + constructClaimKey, + constructStore, + constructTake, + CwebStore, + GStore, +} from '@coinweb/contract-kit'; + +import { MutexAccessResult, MutexAccessStatus } from '../types'; + +export const mutexAccessKey = 'mutex_access'; + +export const constructMutexAccessFirstPart = () => [mutexAccessKey]; + +export const constructMutexAccessClaimKey = (uniqueId: string) => + constructClaimKey(constructMutexAccessFirstPart(), [uniqueId]); + +export const constructMutexAccessClaim = (uniqueId: string, status: MutexAccessStatus) => + constructClaim(constructMutexAccessClaimKey(uniqueId), { status } satisfies MutexAccessResult, '0x0'); + +export const constructMutexAccessClaimStore = (uniqueId: string, status: MutexAccessStatus): GStore => + constructStore(constructMutexAccessClaim(uniqueId, status)); + +export const constructMutexAccessClaimTake = (uniqueId: string) => + constructTake(constructMutexAccessClaimKey(uniqueId)); diff --git a/packages/cwait/src/onchain/mutex/claims/index.ts b/packages/cwait/src/onchain/mutex/claims/index.ts new file mode 100644 index 0000000..ffb10d3 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/claims/index.ts @@ -0,0 +1,2 @@ +export * from './access'; +export * from './lock'; diff --git a/packages/cwait/src/onchain/mutex/claims/lock.ts b/packages/cwait/src/onchain/mutex/claims/lock.ts new file mode 100644 index 0000000..c307665 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/claims/lock.ts @@ -0,0 +1,70 @@ +import { + constructClaim, + constructClaimKey, + constructContractIssuer, + constructRangeRead, + constructStore, + Context, + getContractId, + GRead, +} from '@coinweb/contract-kit'; +import { CwebRead } from '@coinweb/contract-kit/dist/esm/operations/read'; +import { toHex } from 'lib/shared'; + +import { rangeReadLimit } from '../settings'; +import { LockedKey, MutexLockState } from '../types'; + +export const mutexLockKey = 'mutex_lock'; + +export const constructMutexLockFirstPart = () => [mutexLockKey]; + +export const constructMutexLockClaimKey = (lockId: string, timestamp: number) => + constructClaimKey(constructMutexLockFirstPart(), [timestamp, lockId]); + +export const constructMutexLockClaim = ({ + fee, + keys, + locked, + lockId, + timestamp, + processId, +}: { + lockId: string; + timestamp: number; + locked: boolean; + keys: LockedKey[]; + fee: bigint; + processId: string; +}) => + constructClaim( + constructMutexLockClaimKey(lockId, timestamp), + { locked, keys, processId } satisfies MutexLockState, + toHex(fee) + ); + +export const constructMutexLockClaimStore = ({ + fee, + keys, + locked, + lockId, + timestamp, + processId, +}: { + fee: bigint; + keys: LockedKey[]; + locked: boolean; + lockId: string; + timestamp: number; + processId: string; +}) => constructStore(constructMutexLockClaim({ fee, keys, locked, lockId, timestamp, processId })); + +export const constructMutexLockClaimRangeRead = (context: Context): GRead => + constructRangeRead( + constructContractIssuer(getContractId(context.tx)), + constructMutexLockFirstPart(), + {}, + rangeReadLimit + ); + +// export const constructMutexLockClaimTake = (lockId: string, timestamp: number) => +// constructTake(constructMutexLockClaimKey(lockId, timestamp)); diff --git a/packages/cwait/src/onchain/mutex/index.ts b/packages/cwait/src/onchain/mutex/index.ts new file mode 100644 index 0000000..c5a0114 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/index.ts @@ -0,0 +1,20 @@ +import { + execLockMethodName, + getAccessMethodName, + lockMethodName, + mutexExecLock, + mutexGetAccess, + mutexLock, + mutexUnlock, + unlockMethodName, +} from './methods'; + +export * from './claims'; +export * from './types'; + +export const mutexMethods = { + [execLockMethodName]: mutexExecLock, + [getAccessMethodName]: mutexGetAccess, + [lockMethodName]: mutexLock, + [unlockMethodName]: mutexUnlock, +}; diff --git a/packages/cwait/src/onchain/mutex/methods/execLock.ts b/packages/cwait/src/onchain/mutex/methods/execLock.ts new file mode 100644 index 0000000..0284cf8 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/execLock.ts @@ -0,0 +1,75 @@ +import { + constructClaim, + constructContinueTx, + constructContractRef, + constructStore, + constructTake, + Context, + extractContractArgs, + extractRead, + passCwebFrom, +} from '@coinweb/contract-kit'; +import { getCallParameters, getContractIssuer } from 'lib/onchain'; +import { toHex, TypedClaim } from 'lib/shared'; + +import { lockFee } from '../settings'; +import { MutexLockState } from '../types'; +import { isMatchLockKeys } from '../utils'; + +import { lockMethodName } from './names'; + +export const mutexExecLock = (context: Context) => { + const { availableCweb } = getCallParameters(context); + const issuer = getContractIssuer(context); + + const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map( + ({ content }) => content as TypedClaim + ); + + if (!lockQueue) { + throw new Error('No lock queue found'); + } + + const alreadyLockedKeys = lockQueue.filter(({ body }) => body.locked).map(({ key }) => key); + + const lockCandidate = lockQueue.find( + ({ body, key }) => !body.locked && !alreadyLockedKeys.some((lockedKey) => isMatchLockKeys(lockedKey, key)) + ); + + if (!lockCandidate) { + return []; + } + + return [ + constructContinueTx( + context, + [ + passCwebFrom(issuer, availableCweb), + constructTake(lockCandidate.key), + constructStore( + constructClaim( + lockCandidate.key, + { ...lockCandidate.body, locked: true }, + toHex(BigInt(lockCandidate.fees_stored) - lockFee) + ) + ), + ], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: lockMethodName, + methodArgs: [], + }, + contractInfo: { + providedCweb: lockFee, + authenticated: null, + }, + contractArgs: [], + }, + }, + ] + ), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/methods/getAccess.ts b/packages/cwait/src/onchain/mutex/methods/getAccess.ts new file mode 100644 index 0000000..28d2bdd --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/getAccess.ts @@ -0,0 +1,32 @@ +import { constructContinueTx, Context, extractContractArgs, extractRead } from '@coinweb/contract-kit'; +import { getContractArguments } from 'lib/onchain'; +import { TypedClaim } from 'lib/shared'; + +import { constructMutexAccessClaimStore } from '../claims'; +import { MutexAccessStatus, MutexGetAccessArgs, MutexLockState } from '../types'; +import { isMatchLockKeys } from '../utils'; + +export const mutexGetAccess = (context: Context) => { + const [claimKey, processId, uniqueId] = getContractArguments(context); + + const lockQueue = extractRead(extractContractArgs(context.tx)[0])?.map( + ({ content }) => content as TypedClaim + ); + + if (!lockQueue) { + throw new Error('No lock queue found'); + } + + const isLockedByOtherProcess = lockQueue.some( + ({ body }) => body.locked && body.processId !== processId && body.keys.some((key) => isMatchLockKeys(key, claimKey)) + ); + + return [ + constructContinueTx(context, [ + constructMutexAccessClaimStore( + uniqueId, + isLockedByOtherProcess ? MutexAccessStatus.DENIED : MutexAccessStatus.GRANTED + ), + ]), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/methods/index.ts b/packages/cwait/src/onchain/mutex/methods/index.ts new file mode 100644 index 0000000..287d9f5 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/index.ts @@ -0,0 +1,5 @@ +export * from './execLock'; +export * from './getAccess'; +export * from './lock'; +export * from './names'; +export * from './unlock'; diff --git a/packages/cwait/src/onchain/mutex/methods/lock.ts b/packages/cwait/src/onchain/mutex/methods/lock.ts new file mode 100644 index 0000000..0a31048 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/lock.ts @@ -0,0 +1,34 @@ +import { constructContinueTx, constructContractRef, Context } from '@coinweb/contract-kit'; +import { getCallParameters, getContractIssuer } from 'lib/onchain'; + +import { constructMutexLockClaimRangeRead } from '../claims'; + +import { execLockMethodName } from './names'; + +export const mutexLock = (context: Context) => { + const { availableCweb } = getCallParameters(context); + const issuer = getContractIssuer(context); + + return [ + constructContinueTx( + context, + [], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: execLockMethodName, + methodArgs: [], + }, + contractInfo: { + providedCweb: availableCweb - 700n, + authenticated: null, + }, + contractArgs: [constructMutexLockClaimRangeRead(context)], + }, + }, + ] + ), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/methods/names.ts b/packages/cwait/src/onchain/mutex/methods/names.ts new file mode 100644 index 0000000..269ca95 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/names.ts @@ -0,0 +1,4 @@ +export const lockMethodName = '_mutex_lock'; +export const execLockMethodName = '_mutex_execLock'; +export const getAccessMethodName = '_mutex_get_access'; +export const unlockMethodName = '_mutex_unlock'; diff --git a/packages/cwait/src/onchain/mutex/methods/unlock.ts b/packages/cwait/src/onchain/mutex/methods/unlock.ts new file mode 100644 index 0000000..ec5167c --- /dev/null +++ b/packages/cwait/src/onchain/mutex/methods/unlock.ts @@ -0,0 +1,38 @@ +import { constructContinueTx, constructContractRef, constructTake, Context, passCwebFrom } from '@coinweb/contract-kit'; +import { getCallParameters, getContractArguments, getContractIssuer } from 'lib/onchain'; + +import { constructMutexLockClaimKey } from '../claims'; +import { lockFee } from '../settings'; +import { MutexUnlockArgs } from '../types'; + +import { lockMethodName } from './names'; + +export const mutexUnlock = (context: Context) => { + const { availableCweb } = getCallParameters(context); + const issuer = getContractIssuer(context); + + const [lockId, timestamp] = getContractArguments(context); + + return [ + constructContinueTx( + context, + [passCwebFrom(issuer, availableCweb), constructTake(constructMutexLockClaimKey(lockId, timestamp))], + [ + { + callInfo: { + ref: constructContractRef(issuer, []), + methodInfo: { + methodName: lockMethodName, + methodArgs: [], + }, + contractInfo: { + providedCweb: lockFee, + authenticated: null, + }, + contractArgs: [], + }, + }, + ] + ), + ]; +}; diff --git a/packages/cwait/src/onchain/mutex/settings.ts b/packages/cwait/src/onchain/mutex/settings.ts new file mode 100644 index 0000000..e277112 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/settings.ts @@ -0,0 +1,4 @@ +export const lockFee = 5000n; +export const unlockFee = 1000n + lockFee; + +export const rangeReadLimit = 10000; diff --git a/packages/cwait/src/onchain/mutex/types.ts b/packages/cwait/src/onchain/mutex/types.ts new file mode 100644 index 0000000..2aec5c9 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/types.ts @@ -0,0 +1,22 @@ +import { ClaimKey } from '@coinweb/contract-kit'; + +export type LockedKey = Omit & Partial>; + +export type MutexLockState = { + locked: boolean; + keys: LockedKey[]; + processId: string; +}; + +export enum MutexAccessStatus { + GRANTED = 'granted', + DENIED = 'denied', +} + +export type MutexAccessResult = { + status: MutexAccessStatus; +}; + +export type MutexGetAccessArgs = [claimKey: ClaimKey, processId: string, uniqueId: string]; + +export type MutexUnlockArgs = [lockId: string, timestamp: number]; diff --git a/packages/cwait/src/onchain/mutex/utils/index.ts b/packages/cwait/src/onchain/mutex/utils/index.ts new file mode 100644 index 0000000..f2fb770 --- /dev/null +++ b/packages/cwait/src/onchain/mutex/utils/index.ts @@ -0,0 +1 @@ +export * from './isMatchLockKeys'; diff --git a/packages/cwait/src/onchain/mutex/utils/isMatchLockKeys.ts b/packages/cwait/src/onchain/mutex/utils/isMatchLockKeys.ts new file mode 100644 index 0000000..28d35dc --- /dev/null +++ b/packages/cwait/src/onchain/mutex/utils/isMatchLockKeys.ts @@ -0,0 +1,9 @@ +import { LockedKey } from '../types'; + +export const isMatchLockKeys = (lockKey1: LockedKey, lockKey2: LockedKey) => { + if (!lockKey1.second_part || !lockKey2.second_part) { + return lockKey1.first_part === lockKey2.first_part; + } + + return lockKey1.first_part === lockKey2.first_part && lockKey1.second_part === lockKey2.second_part; +}; diff --git a/packages/ui/.env b/packages/ui/.env index 6a36b70..2964227 100644 --- a/packages/ui/.env +++ b/packages/ui/.env @@ -1,4 +1,4 @@ VITE_API_URL='https://api-cloud.coinweb.io/wallet' VITE_EXPLORER_URL='https://explorer.coinweb.io' -VITE_CONTRACT_ADDRESS="0x8c89cb42634cfe892290845d907af8ec2d482cead42afaef3ccc3cea4446fce5" +VITE_CONTRACT_ADDRESS="0xbf9ca1687e3441bed1afd495405778a9c06c2f5173829d47d9b87dfb150063d8"