diff --git a/packages/adapter/src/time.ts b/packages/adapter/src/time.ts index 6f332c67..c2dd05f1 100644 --- a/packages/adapter/src/time.ts +++ b/packages/adapter/src/time.ts @@ -8,9 +8,7 @@ export const ISO8601 = t.string; export type POSIXTime = t.TypeOf; export const POSIXTime = t.number; -export const datetoIso8601 = (date: Date): ISO8601 => - pipe(date, (date) => format(date, "yyyy-MM-dd'T'HH:mm:ss'Z'")); -export const datetoIso8601Bis = (date: Date): ISO8601 => pipe(date, formatISO); +export const datetoIso8601 = (date: Date): ISO8601 => date.toISOString(); // a minute in milliseconds export const MINUTES = 1000 * 60; diff --git a/packages/language/core/v1/src/contract.ts b/packages/language/core/v1/src/contract.ts index c2d24b3e..9507e44f 100644 --- a/packages/language/core/v1/src/contract.ts +++ b/packages/language/core/v1/src/contract.ts @@ -94,6 +94,9 @@ export const datetoTimeout = (date: Date): Timeout => (a) => a.valueOf() ); +export const timeoutToDate = (timeout: Timeout): Date => + new Date(Number(timeout)); + export type Contract = Close | Pay | If | When | Let | Assert; export const Contract: t.Type = t.recursion("Contract", () => diff --git a/packages/language/core/v1/src/index.ts b/packages/language/core/v1/src/index.ts index 20697509..dc74a786 100644 --- a/packages/language/core/v1/src/index.ts +++ b/packages/language/core/v1/src/index.ts @@ -2,6 +2,7 @@ export { Contract, + Case, Assert, Close, close, @@ -10,6 +11,7 @@ export { Pay, When, datetoTimeout, + timeoutToDate, Timeout, } from "./contract.js"; export { role, Party } from "./participants.js"; diff --git a/packages/language/core/v1/src/next/index.ts b/packages/language/core/v1/src/next/index.ts index 371de984..cc877d7e 100644 --- a/packages/language/core/v1/src/next/index.ts +++ b/packages/language/core/v1/src/next/index.ts @@ -1,9 +1,29 @@ import * as t from "io-ts/lib/index.js"; import { ApplicableInputs } from "./applicables/index.js"; -export { toInput } from "./applicables/canDeposit.js"; +import { isNone, none } from "fp-ts/lib/Option.js"; +export * as Deposit from "./applicables/canDeposit.js"; +export * as Choice from "./applicables/canChoose.js"; +export * as Notify from "./applicables/canNotify.js"; export type Next = t.TypeOf; export const Next = t.type({ can_reduce: t.boolean, applicable_inputs: ApplicableInputs, }); + +export const emptyApplicables = (next: Next) => { + return ( + next.applicable_inputs.choices.length === 0 && + next.applicable_inputs.deposits.length === 0 && + isNone(next.applicable_inputs.notify) + ); +}; + +export const noNext: Next = { + can_reduce: false, + applicable_inputs: { + deposits: [], + choices: [], + notify: none, + }, +}; diff --git a/packages/language/examples/package.json b/packages/language/examples/package.json index 1de44554..929d4afb 100644 --- a/packages/language/examples/package.json +++ b/packages/language/examples/package.json @@ -32,6 +32,8 @@ } }, "dependencies": { - "@marlowe.io/language-core-v1": "0.2.0-alpha-3" + "@marlowe.io/language-core-v1": "0.2.0-alpha-3", + "date-fns": "2.29.3", + "fp-ts": "^2.16.1" } } diff --git a/packages/language/examples/src/index.ts b/packages/language/examples/src/index.ts index 79590ad2..cad64dab 100644 --- a/packages/language/examples/src/index.ts +++ b/packages/language/examples/src/index.ts @@ -1,2 +1,3 @@ export * as SwapADAToken from "./swaps/swap-token-token.js"; export { oneNotifyTrue } from "./contract-one-notify.js"; +export * as Vesting from "./vesting.js"; diff --git a/packages/language/examples/src/vesting.ts b/packages/language/examples/src/vesting.ts new file mode 100644 index 00000000..2e251c68 --- /dev/null +++ b/packages/language/examples/src/vesting.ts @@ -0,0 +1,383 @@ +import { + Contract, + Case, + datetoTimeout, + timeoutToDate, + close, + TokenValue, + Timeout, + Party, + MarloweState, + Environment, + mkEnvironment, + Input, +} from "@marlowe.io/language-core-v1"; +import { + Choice, + Deposit, + Next, + emptyApplicables, + noNext, +} from "@marlowe.io/language-core-v1/next"; +import addMinutes from "date-fns/addMinutes"; +import subMinutes from "date-fns/subMinutes"; +import * as O from "fp-ts/lib/Option.js"; +import { pipe } from "fp-ts/lib/function.js"; + +/** + * Vesting Contract Example + * @description : a Token `Provider` block funds on a Marlowe Contract and allow a Token `Claimer` to retrieve them + * based on a Vesting Scheme. + * Cancel Policy : + * 1) At any Vesting Period, The Token Provider can cancel the vesting schedule, then all the downstream vested period will be canceled too. + * 2) Once a vested period is claimable by the Claimer, The Provider can't cancel the transaction. + * @param request Request For Creating a Vesting Marlowe Contract + */ +export const mkContract = function (request: VestingRequest): Contract { + const { + scheme: { numberOfPeriods }, + } = request; + if (numberOfPeriods < 1) + throw "The number of periods needs to be greater or equal to 1"; + + return initialEmployerDeposit(request, employeeDepositDistribution(request)); +}; + +/** + * Request For Creating a Vesting Marlowe Contract + */ +export type VestingRequest = { + /** + * The token and its amount to be vested by the provider + */ + tokenValue: TokenValue; + /** + * The party definition of the Token Provider (Role token or a Cardano Address) + */ + provider: Party; + /** + * The party definition of the Token Claimer (Role token or a Cardano Address) + */ + claimer: Party; + /** + * The vesting scheme definition between Token Claimer & Provider + */ + scheme: VestingScheme; +}; + +/** + * Frequency at which chunks of tokens will be released + */ +export type Frequency = + | "annually" + | "half-yearly" + | "quarterly" + | "monthly" + | "daily" + | "hourly"; + +/** + * Vesting Scheme Definition + */ +export type VestingScheme = { + /** + * Start of the vesting schedule + */ + start: Date; + /** + * Frequency at which chunks of tokens will be released + */ + frequency: Frequency; + /** + * Number of Periods the Provider wants, to release the totality of the tokens to the claimer. + */ + numberOfPeriods: bigint; +}; + +export type State = // Initial Deposit + + | { + name: "WaitingDepositByProvider"; + initialDepositDeadline: Date; + depositInput?: Input[]; + } + + // Initial Deposit Failed + | { + name: "NoDepositBeforeDeadline"; + initialDepositDeadline: Date; + payMinUtxoBackInput: Input[]; + } + + // Distribution of Initial Deposit + | { + name: "WithinVestingPeriod"; + currentPeriod: bigint; + periodInterval: [Date, Date]; + amountToWithdraw: bigint; + cancelInput?: Input[]; + withdrawInput?: Input[]; + } + // Vesting Last Period has elapsed + | { name: "VestingEnded"; withdrawInput: Input[] } + + // Closed Contract + | { name: "Closed" } + | { + unknownState: { + request: VestingRequest; + state: MarloweState; + next: Next; + }; + }; + +export const getState = async ( + request: VestingRequest, + stateOpt: O.Option, + getNext: (environement: Environment) => Promise +): Promise => { + const state = pipe( + stateOpt, + O.match( + () => null, + (a) => a + ) + ); + if (state == null) return { name: "Closed" }; + + const startTimeout: Timeout = datetoTimeout(new Date(request.scheme.start)); + const periodInMilliseconds: bigint = getPeriodInMilliseconds( + request.scheme.frequency + ); + // Employer needs to deposit before the first vesting period + const initialDepositDeadline: Timeout = startTimeout + periodInMilliseconds; + const now = datetoTimeout(new Date()); + const currentPeriod: bigint = + (now - startTimeout) / periodInMilliseconds + 1n; + const periodInterval: [Date, Date] = [ + timeoutToDate( + startTimeout + periodInMilliseconds * (currentPeriod - 1n) + 1n + ), + timeoutToDate(startTimeout + periodInMilliseconds * currentPeriod - 1n), + ]; + + if (now > initialDepositDeadline && state?.accounts.length === 1) + return { + name: "NoDepositBeforeDeadline", + initialDepositDeadline: timeoutToDate(initialDepositDeadline), + payMinUtxoBackInput: [], + }; + + const environment = mkEnvironment(periodInterval[0])(periodInterval[1]); + const next = await getNext(environment); + if (next.can_reduce && emptyApplicables(next) && state?.accounts.length > 1) + return { name: "VestingEnded", withdrawInput: [] }; + + // Initial Deposit Phase + if (state?.accounts.length == 1) { + const depositInput = + next.applicable_inputs.deposits.length == 1 + ? [Deposit.toInput(next.applicable_inputs.deposits[0])] + : undefined; + return { + name: "WaitingDepositByProvider", + initialDepositDeadline: timeoutToDate(initialDepositDeadline), + depositInput: depositInput, + }; + } + if ( + next.applicable_inputs.choices.length == 1 && + next.applicable_inputs.choices[0].for_choice.choice_name == "cancel" + ) + return { + name: "WithinVestingPeriod", + currentPeriod: currentPeriod, + periodInterval: periodInterval, + amountToWithdraw: 0n, + cancelInput: [Choice.toInput(next.applicable_inputs.choices[0])(1n)], + }; + if ( + next.applicable_inputs.choices.length == 1 && + next.applicable_inputs.choices[0].for_choice.choice_name == "withdraw" + ) + return { + name: "WithinVestingPeriod", + currentPeriod: currentPeriod, + periodInterval: periodInterval, + amountToWithdraw: + next.applicable_inputs.choices[0].can_choose_between[0].to, + withdrawInput: [ + Choice.toInput(next.applicable_inputs.choices[0])( + next.applicable_inputs.choices[0].can_choose_between[0].to + ), + ], + }; + if (next.applicable_inputs.choices.length == 2) + return { + name: "WithinVestingPeriod", + currentPeriod: currentPeriod, + periodInterval: periodInterval, + amountToWithdraw: + next.applicable_inputs.choices[0].can_choose_between[0].to, + cancelInput: [Choice.toInput(next.applicable_inputs.choices[1])(1n)], + withdrawInput: [ + Choice.toInput(next.applicable_inputs.choices[0])( + next.applicable_inputs.choices[0].can_choose_between[0].to + ), + ], + }; + return { unknownState: { request: request, state: state, next: next } }; +}; + +const getPeriodInMilliseconds = function (frequency: Frequency): bigint { + switch (frequency) { + case "annually": + return 2n * getPeriodInMilliseconds("half-yearly"); + case "half-yearly": + return 2n * getPeriodInMilliseconds("quarterly"); + case "quarterly": + return 3n * getPeriodInMilliseconds("monthly"); + case "monthly": + return 30n * getPeriodInMilliseconds("daily"); + case "daily": + return 24n * getPeriodInMilliseconds("hourly"); + case "hourly": + return 1n * 60n * 60n * 1000n; + } +}; + +const initialEmployerDeposit = function ( + request: VestingRequest, + continuation: Contract +): Contract { + const { + tokenValue, + provider, + scheme: { start, frequency, numberOfPeriods }, + } = request; + if (numberOfPeriods < 1) + throw "The number of periods needs to be greater or equal to 1"; + + const startTimeout: Timeout = datetoTimeout(start); + const periodInMilliseconds: bigint = getPeriodInMilliseconds(frequency); + // Employer needs to deposit before the first vesting period + const initialDepositDeadline: Timeout = startTimeout + periodInMilliseconds; + return { + when: [ + { + case: { + party: provider, + deposits: tokenValue.amount, + of_token: tokenValue.token, + into_account: provider, + }, + then: continuation, + }, + ], + timeout: initialDepositDeadline, + timeout_continuation: close, + }; +}; + +const employeeDepositDistribution = function ( + request: VestingRequest +): Contract { + return recursiveEmployeeDepositDistribution(request, 1n); +}; + +/** NOTE: Currently this logic presents the withdrawal and cancel for the last period, even though it doesn't make sense + * because there is nothing to cancel, and even if the employee does a partial withdrawal, they receive the balance in their account. + */ +const recursiveEmployeeDepositDistribution = function ( + request: VestingRequest, + periodIndex: bigint +): Contract { + const { + tokenValue, + claimer, + provider, + scheme: { start, frequency, numberOfPeriods }, + } = request; + + const vestingAmountPerPeriod = tokenValue.amount / BigInt(numberOfPeriods); + const startTimeout: Timeout = datetoTimeout(start); + // Employer needs to deposit before the first vesting period + const periodInMilliseconds = getPeriodInMilliseconds(frequency); + + const continuation: Contract = + periodIndex == numberOfPeriods + ? close + : recursiveEmployeeDepositDistribution(request, periodIndex + 1n); + const vestingDate = startTimeout + periodIndex * periodInMilliseconds; + const nextVestingDate = vestingDate + periodInMilliseconds; + + // On every period, we allow an employee to do a withdrawal. + const employeeWithdrawCase: Case = { + case: { + choose_between: [ + { + from: 1n, + to: periodIndex * vestingAmountPerPeriod, + }, + ], + for_choice: { + choice_name: "withdraw", + choice_owner: claimer, + }, + }, + then: { + pay: { + value_of_choice: { + choice_name: "withdraw", + choice_owner: claimer, + }, + }, + token: tokenValue.token, + from_account: claimer, + to: { + party: claimer, + }, + then: continuation, + }, + }; + + const employerCancelCase: Case = { + case: { + choose_between: [ + { + from: 1n, + to: 1n, + }, + ], + for_choice: { + choice_name: "cancel", + choice_owner: provider, + }, + }, + then: close, + }; + + // 1) Wait for the vesting period. + // 2) Release vested funds + // 3) Allow the provider to withdraw or to cancel future vesting periods + return { + when: [employerCancelCase], + timeout: vestingDate, + timeout_continuation: { + pay: vestingAmountPerPeriod, + token: tokenValue.token, + from_account: provider, + to: { + account: claimer, + }, + then: { + when: + periodIndex == numberOfPeriods + ? [employeeWithdrawCase] + : [employeeWithdrawCase, employerCancelCase], + timeout: nextVestingDate, + timeout_continuation: continuation, + }, + }, + }; +}; diff --git a/packages/runtime/client/rest/src/contract/next/endpoint.ts b/packages/runtime/client/rest/src/contract/next/endpoint.ts index bebb70df..c23e17b3 100644 --- a/packages/runtime/client/rest/src/contract/next/endpoint.ts +++ b/packages/runtime/client/rest/src/contract/next/endpoint.ts @@ -21,7 +21,7 @@ export const getViaAxios: (axiosInstance: AxiosInstance) => GET = pipe( HTTP.Get(axiosInstance)( contractNextEndpoint(contractId) + - `?validityStart=${environment.validityStart}&validityEnd=${environment.validityEnd}` + + `?validityStart=${environment.validityStart}&validityEnd=${environment.validityEnd}&` + stringify({ party: parties }, { indices: false }), { headers: { diff --git a/packages/runtime/lifecycle/src/api.ts b/packages/runtime/lifecycle/src/api.ts index b2c58c10..2a5d0598 100644 --- a/packages/runtime/lifecycle/src/api.ts +++ b/packages/runtime/lifecycle/src/api.ts @@ -11,7 +11,7 @@ import { } from "@marlowe.io/runtime-core"; import { RestDI, RolesConfig } from "@marlowe.io/runtime-rest-client"; import { ISO8601 } from "@marlowe.io/adapter/time"; -import { Contract, Input } from "@marlowe.io/language-core-v1"; +import { Contract, Environment, Input } from "@marlowe.io/language-core-v1"; import { Next } from "@marlowe.io/language-core-v1/next"; export type RuntimeLifecycle = { @@ -76,7 +76,10 @@ export interface ContractsAPI { * @param contractId Contract Id of a created contract * @throws DecodingError */ - getNextApplicabilityAndReducibility(contractId: ContractId): Promise; + getNextApplicabilityAndReducibility( + contractId: ContractId, + environement: Environment + ): Promise; } export type PayoutsDI = WalletDI & RestDI; diff --git a/packages/runtime/lifecycle/src/generic/contracts.ts b/packages/runtime/lifecycle/src/generic/contracts.ts index 20e0b127..5831a59e 100644 --- a/packages/runtime/lifecycle/src/generic/contracts.ts +++ b/packages/runtime/lifecycle/src/generic/contracts.ts @@ -1,7 +1,10 @@ import * as TE from "fp-ts/lib/TaskEither.js"; import { pipe } from "fp-ts/lib/function.js"; -import { mkEnvironment, Party } from "@marlowe.io/language-core-v1"; -import { addMinutes, subMinutes } from "date-fns"; +import { + Environment, + mkEnvironment, + Party, +} from "@marlowe.io/language-core-v1"; import { tryCatchDefault, unsafeTaskEither } from "@marlowe.io/adapter/fp-ts"; import { ApplyInputsRequest, @@ -25,7 +28,8 @@ import { import { FPTSRestAPI } from "@marlowe.io/runtime-rest-client"; import { DecodingError } from "@marlowe.io/adapter/codec"; import { TransactionTextEnvelope } from "@marlowe.io/runtime-rest-client/contract/transaction/endpoints/collection"; -import { Next } from "@marlowe.io/language-core-v1/next"; +import { Next, noNext } from "@marlowe.io/language-core-v1/next"; +import { isNone } from "fp-ts/lib/Option.js"; export function mkContractLifecycle( wallet: WalletAPI, @@ -63,20 +67,20 @@ const submitApplyInputsTx = const getNextApplicabilityAndReducibility = ({ wallet, rest }: ContractsDI) => - async (contractId: ContractId): Promise => { + async (contractId: ContractId, environement: Environment): Promise => { const contractDetails = await unsafeTaskEither( rest.contracts.contract.get(contractId) ); - const parties = await getParties(wallet)( - contractDetails.roleTokenMintingPolicyId - ); - return await unsafeTaskEither( - rest.contracts.contract.next(contractId)( - mkEnvironment(pipe(Date.now(), (date) => subMinutes(date, 15)))( - pipe(Date.now(), (date) => addMinutes(date, 15)) - ) - )(parties) - ); + if (isNone(contractDetails.currentContract)) { + return noNext; + } else { + const parties = await getParties(wallet)( + contractDetails.roleTokenMintingPolicyId + ); + return await unsafeTaskEither( + rest.contracts.contract.next(contractId)(environement)(parties) + ); + } }; const getParties: ( diff --git a/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts b/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts index ab6b59e8..f1c3e58e 100644 --- a/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/examples/swap.ada.token.e2e.spec.ts @@ -1,8 +1,7 @@ import { pipe } from "fp-ts/lib/function.js"; import { addDays } from "date-fns"; - -import { toInput } from "@marlowe.io/language-core-v1/next"; +import { Deposit } from "@marlowe.io/language-core-v1/next"; import * as Examples from "@marlowe.io/language-examples"; import { datetoTimeout, adaValue } from "@marlowe.io/language-core-v1"; @@ -72,7 +71,7 @@ describe("swap", () => { const txFirstTokensDeposited = await runtime( adaProvider ).contracts.applyInputs(contractId, { - inputs: [pipe(next.applicable_inputs.deposits[0], toInput)], + inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], }); await runtime(adaProvider).wallet.waitConfirmation( txFirstTokensDeposited @@ -83,7 +82,7 @@ describe("swap", () => { tokenProvider ).contracts.getNextApplicabilityAndReducibility(contractId); await runtime(tokenProvider).contracts.applyInputs(contractId, { - inputs: [pipe(next.applicable_inputs.deposits[0], toInput)], + inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], }); await runtime(tokenProvider).wallet.waitConfirmation( txFirstTokensDeposited diff --git a/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts b/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts index 91abfa1d..9eda2547 100644 --- a/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts +++ b/packages/runtime/lifecycle/test/generic/payouts.e2e.spec.ts @@ -3,7 +3,7 @@ import { addDays } from "date-fns"; import * as Examples from "@marlowe.io/language-examples"; import { datetoTimeout, adaValue } from "@marlowe.io/language-core-v1"; -import { Next, toInput } from "@marlowe.io/language-core-v1/next"; +import { Next, Deposit } from "@marlowe.io/language-core-v1/next"; import { mkFPTSRestClient } from "@marlowe.io/runtime-rest-client/index.js"; import { @@ -47,29 +47,39 @@ describe("Payouts", () => { }, }; const swapContract = Examples.SwapADAToken.mkSwapContract(swapRequest); - const contractId = await runtime(adaProvider).contracts.create({ + const [contractId, txCreatedContract] = await runtime( + adaProvider + ).contracts.createContract({ contract: swapContract, roles: { [swapRequest.provider.roleName]: adaProvider.address, [swapRequest.swapper.roleName]: tokenProvider.address, }, }); + await runtime(adaProvider).wallet.waitConfirmation(txCreatedContract); - // see [[apply-inputs-next-provider]], I think it would be clearer to separate the "what are the - // next possible inputs from the apply inputs call" - await runtime(adaProvider).contracts.applyInputs( - contractId, - (next: Next) => ({ - inputs: [pipe(next.applicable_inputs.deposits[0], toInput)], - }) - ); + // Applying the first Deposit + let next = await runtime( + adaProvider + ).contracts.getNextApplicabilityAndReducibility(contractId); + const txFirstTokensDeposited = await runtime( + adaProvider + ).contracts.applyInputs(contractId, { + inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], + }); + await runtime(adaProvider).wallet.waitConfirmation(txFirstTokensDeposited); - await runtime(tokenProvider).contracts.applyInputs( - contractId, - (next: Next) => ({ - inputs: [pipe(next.applicable_inputs.deposits[0], toInput)], - }) + // Applying the second Deposit + next = await runtime( + tokenProvider + ).contracts.getNextApplicabilityAndReducibility(contractId); + await runtime(tokenProvider).contracts.applyInputs(contractId, { + inputs: [pipe(next.applicable_inputs.deposits[0], Deposit.toInput)], + }); + await runtime(tokenProvider).wallet.waitConfirmation( + txFirstTokensDeposited ); + return { contractId, runtime, diff --git a/packages/runtime/lifecycle/test/jest.e2e.config.mjs b/packages/runtime/lifecycle/test/jest.e2e.config.mjs index 7c5f35a2..a5cc54b0 100644 --- a/packages/runtime/lifecycle/test/jest.e2e.config.mjs +++ b/packages/runtime/lifecycle/test/jest.e2e.config.mjs @@ -7,7 +7,7 @@ const moduleNameMapper = { '^@marlowe.io/adapter/(.*)$': relative('../../../adapter/dist/esm/$1.js'), '^@marlowe.io/language\\-core\\-v1$': relative("../../../language/core/v1/dist/esm/semantics/contract/index.js"), '^@marlowe.io/language\\-core\\-v1/next$': relative('../../../language/core/v1/dist/esm/semantics/next/index.js'), - '^@marlowe.io/language\\-core\\-v1/examples$': relative('../../../language/core/v1/dist/esm/examples/index.js'), + '^@marlowe.io/language\\-examples$': relative('../../../language/examples/dist/esm/index.js'), '^@marlowe.io/wallet/nodejs/(.*)$': relative('../../../wallet/dist/esm/nodejs/$1.js'), '^@marlowe.io/wallet/nodejs/index.js': relative('../../../dist/esm/wallet/nodejs/index.js'), '^@marlowe.io/runtime\\-core/(.*)$': relative('../../core/dist/esm/$1.js'), diff --git a/pocs/contract-example/vesting-flow.html b/pocs/contract-example/vesting-flow.html new file mode 100644 index 00000000..92977df6 --- /dev/null +++ b/pocs/contract-example/vesting-flow.html @@ -0,0 +1,260 @@ + + + + + + Vesting Flow + + + + +
+

Contract Examples > Vesting

+

1. Setup Environment

+ + + +
+ + +
+

1.1 Create Vesting contract

+ + +
+
+ + + +
+

1.2 Apply Inputs

+ + + +

Contracts waiting for an action

+
+
+
+

Console

+
+
+ + + diff --git a/pocs/vesting-flow.html b/pocs/vesting-flow.html deleted file mode 100644 index fe8bf797..00000000 --- a/pocs/vesting-flow.html +++ /dev/null @@ -1,272 +0,0 @@ - - - - - - Vesting Flow - - - - - -