From dea416f46ba6e1eecff902865bfae1970e3ac74e Mon Sep 17 00:00:00 2001 From: nhenin Date: Fri, 15 Dec 2023 11:49:29 +0100 Subject: [PATCH] Atomic Swap Example --- packages/language/examples/src/atomicSwap.ts | 390 ++++++++++++++++++ .../examples/src/contract-one-notify.ts | 3 +- packages/language/examples/src/index.ts | 11 +- .../examples/src/swaps/swap-token-token.ts | 71 ---- packages/language/examples/src/vesting.ts | 2 +- packages/language/examples/typedoc.json | 7 +- 6 files changed, 406 insertions(+), 78 deletions(-) create mode 100644 packages/language/examples/src/atomicSwap.ts delete mode 100644 packages/language/examples/src/swaps/swap-token-token.ts diff --git a/packages/language/examples/src/atomicSwap.ts b/packages/language/examples/src/atomicSwap.ts new file mode 100644 index 00000000..9a59616f --- /dev/null +++ b/packages/language/examples/src/atomicSwap.ts @@ -0,0 +1,390 @@ +import { + Contract, + close, + TokenValue, + Timeout, + Party, + PayeeParty, + Input, + MarloweState, + IChoice, + IDeposit, + INotify, + Address, + datetoTimeout, +} from "@marlowe.io/language-core-v1"; +import * as G from "@marlowe.io/language-core-v1/guards"; +import { Deposit } from "@marlowe.io/language-core-v1/next"; + +/** + * Marlowe Example : Atomic Swap + * + * The Atomic Swap Contract is a Marlowe Contract allowing 2 participants (A Seller and a Buyer) to atomically exchange + * some tokens A against some token B. + * @packageDocumentation + */ + +type IReduce = void; +const iReduce: void = undefined; + +/* #region Scheme */ + +export type Scheme = { + participants: { + seller: Address; + buyer: Party; + }; + offer: { + deadline: Timeout; + asset: TokenValue; + }; + ask: { + deadline: Timeout; + asset: TokenValue; + }; + swapConfirmation: { + deadline: Timeout; + }; +}; + +/* #endregion */ + +/* #region State */ +export type State = + | WaitingSellerOffer + | NoSellerOfferInTime + | WaitingForAnswer + | WaitingForSwapConfirmation + | Closed; + +export type WaitingSellerOffer = { + typeName: "WaitingSellerOffer"; + scheme: Scheme; + action: ProvisionOffer; +}; + +export type NoSellerOfferInTime = { + typeName: "NoSellerOfferInTime"; + scheme: Scheme; + action: RetrieveMinimumLovelaceAdded; +}; + +export type WaitingForAnswer = { + typeName: "WaitingForAnswer"; + scheme: Scheme; + actions: [Swap, Retract]; +}; + +export type WaitingForSwapConfirmation = { + typeName: "WaitingForSwapConfirmation"; + scheme: Scheme; + action: ConfirmSwap; +}; + +export type Closed = { + typeName: "Closed"; + scheme: Scheme; + reason: CloseReason; +}; + +/* #region Action */ +export type Action = + /* When Contract Created (timed out > NoOfferProvisionnedOnTime) */ + | ProvisionOffer // > OfferProvisionned + /* When NoOfferProvisionnedOnTime (timed out > no timeout (need to be reduced to be closed))*/ + | RetrieveMinimumLovelaceAdded // > closed + /* When OfferProvisionned (timed out > NotNotifiedOnTime) */ + | Retract // > closed + | Swap // > Swapped + /* When Swapped (timed out > NotNotifiedOnTime) */ + | ConfirmSwap; // > closed + +export type ActionParticipant = "buyer" | "seller" | "anybody"; + +export type RetrieveMinimumLovelaceAdded = { + typeName: "RetrieveMinimumLovelaceAdded"; + owner: ActionParticipant; + input: IReduce; +}; + +export type ProvisionOffer = { + typeName: "ProvisionOffer"; + owner: ActionParticipant; + input: IDeposit; +}; + +export type Swap = { + typeName: "Swap"; + owner: ActionParticipant; + input: IDeposit; +}; + +export type ConfirmSwap = { + typeName: "ConfirmSwap"; + owner: ActionParticipant; + input: INotify; +}; + +export type Retract = { + typeName: "Retract"; + owner: ActionParticipant; + input: IChoice; +}; + +/* #endregion */ + +/* #region Close Reason */ +export type CloseReason = + | NoOfferProvisionnedOnTime + | SellerRetracted + | NotAnsweredOnTime + | Swapped + | NotNotifiedOnTime; + +export type NoOfferProvisionnedOnTime = { + typeName: "NoOfferProvisionnedOnTime"; +}; +export type SellerRetracted = { typeName: "SellerRetracted" }; +export type NotAnsweredOnTime = { typeName: "NotAnsweredOnTime" }; +export type NotNotifiedOnTime = { typeName: "NotNotifiedOnTime" }; +export type Swapped = { typeName: "Swapped" }; + +/* #endregion */ + +export class UnexpectedSwapContractState extends Error { + public type = "UnexpectedSwapContractState" as const; + public scheme: Scheme; + public state?: MarloweState; + constructor(scheme: Scheme, state?: MarloweState) { + super("Swap Contract / Unexpected State"); + this.scheme = scheme; + this.state = state; + } +} + +/* #endregion */ +export const getState = ( + scheme: Scheme, + inputHistory: Input[], + state?: MarloweState +): State => { + /* #region Closed State */ + if (state === null) { + // The Contract is closed when the State is null + if (inputHistory.length === 0) { + // Offer Provision Deadline has passed and there is one reduced applied to close the contract + return { + typeName: "Closed", + scheme: scheme, + reason: { typeName: "NoOfferProvisionnedOnTime" }, + }; + } + if (inputHistory.length === 1) { + return { + typeName: "Closed", + scheme: scheme, + reason: { typeName: "NotAnsweredOnTime" }, + }; + } + if (inputHistory.length === 2) { + const isRetracted = + 1 === + inputHistory + .filter((input) => G.IChoice.is(input)) + .map((input) => input as IChoice) + .filter((choice) => choice.for_choice_id.choice_name === "retract") + .length; + const nbDeposits = inputHistory.filter((input) => + G.IDeposit.is(input) + ).length; + if (isRetracted && nbDeposits === 1) { + return { + typeName: "Closed", + scheme: scheme, + reason: { typeName: "SellerRetracted" }, + }; + } + if (nbDeposits === 2) { + return { + typeName: "Closed", + scheme: scheme, + reason: { typeName: "NotNotifiedOnTime" }, + }; + } + } + if (inputHistory.length === 3) { + const nbDeposits = inputHistory.filter((input) => + G.IDeposit.is(input) + ).length; + const nbNotify = inputHistory.filter((input) => + G.INotify.is(input) + ).length; + if (nbDeposits === 2 && nbNotify === 1) { + return { + typeName: "Closed", + scheme: scheme, + reason: { typeName: "Swapped" }, + }; + } + } + } + /* #endregion */ + + const now: Timeout = datetoTimeout(new Date()); + + if (inputHistory.length === 0) { + if (now < scheme.offer.deadline) { + const offerInput: IDeposit = { + input_from_party: scheme.participants.seller, + that_deposits: scheme.offer.asset.amount, + of_token: scheme.offer.asset.token, + into_account: scheme.participants.seller, + }; + return { + typeName: "WaitingSellerOffer", + scheme, + action: { + typeName: "ProvisionOffer", + owner: "seller", + input: offerInput, + }, + }; + } else { + return { + typeName: "NoSellerOfferInTime", + scheme, + action: { + typeName: "RetrieveMinimumLovelaceAdded", + owner: "anybody", + input: iReduce, + }, + }; + } + } + + if (inputHistory.length === 1) { + if (now < scheme.ask.deadline) { + const askInput: IDeposit = { + input_from_party: scheme.participants.buyer, + that_deposits: scheme.ask.asset.amount, + of_token: scheme.ask.asset.token, + into_account: scheme.participants.buyer, + }; + const retractInput: IChoice = { + for_choice_id: { + choice_name: "retract", + choice_owner: scheme.participants.seller, + }, + input_that_chooses_num: 0n, + }; + return { + typeName: "WaitingForAnswer", + scheme: scheme, + actions: [ + { + typeName: "Swap", + owner: "buyer", + input: askInput, + }, + { + typeName: "Retract", + owner: "seller", + input: retractInput, + }, + ], + }; + } else { + // Closed (handled upstream) + } + } + + if (inputHistory.length === 2) { + const nbDeposits = inputHistory.filter((input) => + G.IDeposit.is(input) + ).length; + if (nbDeposits === 2 && now < scheme.swapConfirmation.deadline) { + return { + typeName: "WaitingForSwapConfirmation", + scheme: scheme, + action: { + typeName: "ConfirmSwap", + owner: "anybody", + input: "input_notify", + }, + }; + } else { + // Closed (handled upstream) + } + } + + throw new UnexpectedSwapContractState(scheme, state); +}; +export function mkAtomicSwap(scheme: Scheme): Contract { + const mkOffer = (ask: Contract): Contract => { + const depositOffer = { + party: scheme.participants.seller, + deposits: scheme.offer.asset.amount, + of_token: scheme.offer.asset.token, + into_account: scheme.participants.seller, + }; + + return { + when: [{ case: depositOffer, then: ask }], + timeout: scheme.offer.deadline, + timeout_continuation: close, + }; + }; + + const mkAsk = (confirmSwap: Contract): Contract => { + const asPayee = (party: Party): PayeeParty => ({ party: party }); + const depositAsk = { + party: scheme.participants.buyer, + deposits: scheme.ask.asset.amount, + of_token: scheme.ask.asset.token, + into_account: scheme.participants.buyer, + }; + const chooseToRetract = { + choose_between: [{ from: 0n, to: 0n }], + for_choice: { + choice_name: "retract", + choice_owner: scheme.participants.seller, + }, + }; + return { + when: [ + { + case: depositAsk, + then: { + pay: scheme.offer.asset.amount, + token: scheme.offer.asset.token, + from_account: scheme.participants.seller, + to: asPayee(scheme.participants.buyer), + then: { + pay: scheme.ask.asset.amount, + token: scheme.ask.asset.token, + from_account: scheme.participants.buyer, + to: asPayee(scheme.participants.seller), + then: confirmSwap, + }, + }, + }, + { + case: chooseToRetract, + then: close, + }, + ], + timeout: scheme.ask.deadline, + timeout_continuation: close, + }; + }; + + const mkSwapConfirmation = (): Contract => { + return { + when: [{ case: { notify_if: true }, then: close }], + timeout: scheme.swapConfirmation.deadline, + timeout_continuation: close, + }; + }; + + return mkOffer(mkAsk(mkSwapConfirmation())); +} diff --git a/packages/language/examples/src/contract-one-notify.ts b/packages/language/examples/src/contract-one-notify.ts index 569c8769..38eb3d46 100644 --- a/packages/language/examples/src/contract-one-notify.ts +++ b/packages/language/examples/src/contract-one-notify.ts @@ -1,9 +1,8 @@ -/* eslint-disable sort-keys-fix/sort-keys-fix */ - import { Contract, close, Timeout } from "@marlowe.io/language-core-v1"; /** * Marlowe Example : A contract with One Step (one true notify) + * @packageDocumentation */ export const oneNotifyTrue: (notifyTimeout: Timeout) => Contract = ( diff --git a/packages/language/examples/src/index.ts b/packages/language/examples/src/index.ts index 75dd690d..e80e01a1 100644 --- a/packages/language/examples/src/index.ts +++ b/packages/language/examples/src/index.ts @@ -1,6 +1,11 @@ -export * as SwapADAToken from "./swaps/swap-token-token.js"; -export { oneNotifyTrue } from "./contract-one-notify.js"; +/** + * This is the package doc + * @packageDocumentation + */ + export * as Vesting from "./vesting.js"; +export * as Swap from "./atomicSwap.js"; +export * as Survey from "./survey.js"; +export { oneNotifyTrue } from "./contract-one-notify.js"; export { escrow } from "./playground-v1/escrow.js"; export { escrowWithCollateral } from "./playground-v1/escrow-with-collateral.js"; -export { survey, verifySurvey } from "./survey.js"; diff --git a/packages/language/examples/src/swaps/swap-token-token.ts b/packages/language/examples/src/swaps/swap-token-token.ts deleted file mode 100644 index ca052ea9..00000000 --- a/packages/language/examples/src/swaps/swap-token-token.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - Contract, - close, - role, - TokenValue, - Timeout, -} from "@marlowe.io/language-core-v1"; - -/** - * Marlowe Example : Swap - * Description : - * Takes Tokens A from one party and tokens B from another party, and it swaps them atomically. - */ - -export type SwapRequest = { provider: SwapParty; swapper: SwapParty }; - -export type SwapParty = { - roleName: string; - depositTimeout: Timeout; - value: TokenValue; -}; - -export const mkSwapContract: (request: SwapRequest) => Contract = ( - request -) => ({ - when: [ - { - case: { - party: role(request.provider.roleName), - deposits: request.provider.value.amount, - of_token: request.provider.value.token, - into_account: role(request.provider.roleName), - }, - then: { - when: [ - { - case: { - party: role(request.swapper.roleName), - deposits: request.swapper.value.amount, - of_token: request.swapper.value.token, - into_account: role(request.swapper.roleName), - }, - then: { - pay: request.provider.value.amount, - token: request.provider.value.token, - from_account: role(request.provider.roleName), - to: { party: role(request.swapper.roleName) }, - then: { - pay: request.swapper.value.amount, - token: request.swapper.value.token, - from_account: role(request.swapper.roleName), - to: { party: role(request.provider.roleName) }, - then: close, - }, - }, - }, - ], - timeout: request.swapper.depositTimeout, - timeout_continuation: { - pay: request.provider.value.amount, - token: request.provider.value.token, - from_account: role(request.provider.roleName), - to: { party: role(request.provider.roleName) }, - then: close, - }, - }, - }, - ], - timeout: request.provider.depositTimeout, - timeout_continuation: close, -}); diff --git a/packages/language/examples/src/vesting.ts b/packages/language/examples/src/vesting.ts index d1b7f667..32cea566 100644 --- a/packages/language/examples/src/vesting.ts +++ b/packages/language/examples/src/vesting.ts @@ -19,7 +19,7 @@ * * 8. The Provider may cancel the contract during the first vesting period. * 9. The Provider may not cancel the contract after all funds have been vested. - * @packageDocumentation + * @packageDocumentation */ import { diff --git a/packages/language/examples/typedoc.json b/packages/language/examples/typedoc.json index 6156dbd9..31d9af79 100644 --- a/packages/language/examples/typedoc.json +++ b/packages/language/examples/typedoc.json @@ -1,6 +1,11 @@ { "entryPointStrategy": "expand", - "entryPoints": ["./src/vesting.ts", "./src/swaps/swap-token-token.ts"], + "entryPoints": [ + "./src/index.ts", + "./src/vesting.ts", + "./src/swap.ts", + "./src/survey.ts" + ], "tsconfig": "./src/tsconfig.json", "categorizeByGroup": false }