diff --git a/e2e/artifacts/wallets/no-pure-ada-utxo/mnemonic b/e2e/artifacts/wallets/no-pure-ada-utxo/mnemonic new file mode 100644 index 00000000..eb89b126 --- /dev/null +++ b/e2e/artifacts/wallets/no-pure-ada-utxo/mnemonic @@ -0,0 +1 @@ +flame build shy opera chef educate crane high acquire season negative syrup senior lecture false float dwarf quit fancy couple lizard grief buffalo bundle diff --git a/e2e/artifacts/wallets/no-pure-ada-utxo/testnet.bech32 b/e2e/artifacts/wallets/no-pure-ada-utxo/testnet.bech32 new file mode 100644 index 00000000..05a55d67 --- /dev/null +++ b/e2e/artifacts/wallets/no-pure-ada-utxo/testnet.bech32 @@ -0,0 +1 @@ +addr_test1qp7thfxtpw5yj2jmej823cgnh5937cv38e7a32k3ece8aerv6hy3dvev273nppcz3qu8dwk92p5atmkyzm9p8lvmv5rqnc06ms diff --git a/e2e/src/features/apply-inputs.feature b/e2e/src/features/apply-inputs.feature index 721390c5..06a5ebbb 100644 --- a/e2e/src/features/apply-inputs.feature +++ b/e2e/src/features/apply-inputs.feature @@ -190,9 +190,24 @@ Feature: As a user, I would like to apply an input on a current contract Given I use alice lace browser Then I should see "Complete" status of the "escrow" contract - @submission-failure-with-empty-wallet + @contract-creation-failure-with-empty-wallet Scenario Outline: Creating a deposit with a wallet - Given I use alice browser + Given I use empty lace browser + And I am on the "home" page + When I authorize the app + + When I click the "button" with "Create a contract" text + And I generate Escrow contract with "empty" as a buyer and "empty" as a seller and "empty" as a mediator and call it "escrow" + + And I enter the json of the contract "escrow" into the "contract-input" field + When I click the "button" with "Submit contract" text And sign the transaction + Then I should see error toast + # The actual error: + # An error occured during contract submission: (ServerApiError ApiError { message: "CoinSelectionFailed \"Insufficient lovelace available for coin selection: valueFromList [(AdaAssetId,8766894)] required, but valueFromList [] available.\"", error: CoinSelectionFailed, details: {"contents":"Insufficient lovelace available for coin selection: valueFromList [(AdaAssetId,8766894)] required, but valueFromList [] available.","tag":"CoinSelectionFailed"} })# + + @input-application-failure-with-empty-wallet + Scenario Outline: Creating a deposit with a wallet + Given I use alice lace browser And I am on the "home" page When I authorize the app @@ -206,7 +221,7 @@ Feature: As a user, I would like to apply an input on a current contract Then I should see "Syncing" status of the "escrow" contract Then I should see "Awaiting other party" status of the "escrow" contract - Given I use empty browser + Given I use empty lace browser And I am on the "home" page When I authorize the app Then I should see "Advance" status of the "escrow" contract @@ -214,7 +229,143 @@ Feature: As a user, I would like to apply an input on a current contract When I start advancing "escrow" contract And I click the "button" with "Advance contract" text Then I should see error toast + # The actual error: + # An error occured during contract submission: (ServerApiError ApiError { error: CoinSelectionFailed, message: "CoinSelectionFailed \"No collateral found in [(TxIn \\\"0c338171c1d7a1a7228b60e8ed8a3d168b2fd74853e0cd4bb6fe85aec69786bd\\\" (TxIx 1),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraBabbage) (ShelleyAddress Testnet (ScriptHashObj (ScriptHash \\\"d85fa9bc2bdfd97d5ebdbc5e3fc66f7476213c40c21b73b41257f09d\\\")) StakeRefNull)) (TxOutValue MultiAssetInBabbageEra (valueFromList [(AdaAssetId,2000000)])) (TxOutDatumInline ReferenceTxInsScriptsInlineDatumsInBabbageEra (HashableScriptData \\\"\\\\216y\\\\159\\\\216y\\\\159@\\\\255\\\\216y (...)"" }) - Examples: - | wallet_name | - | lace | + @contract-creation-failure-with-no-pure-ada-utxo + Scenario Outline: Creating a deposit with a wallet + Given I use no-pure-ada-utxo lace browser + And I am on the "home" page + When I authorize the app + + When I click the "button" with "Create a contract" text + And I generate Escrow contract with "empty" as a buyer and "empty" as a seller and "empty" as a mediator and call it "escrow" + + And I enter the json of the contract "escrow" into the "contract-input" field + When I click the "button" with "Submit contract" text And sign the transaction + Then I should see error toast + # The actual error: + # An error occured during contract submission: (ServerApiError ApiError { message: "CoinSelectionFailed \"Insufficient lovelace available for coin selection: valueFromList [(AdaAssetId,9340124)] required, but valueFromList [(AdaAssetId,5172320),(AssetId \\\"0122e5079749e36fc703335621430e65017231726166e319a32ed831\\\" \\\"Other provider\\\",4),(AssetId \\\"32dada46a8f74bc884627f1258f52777b9a95bd6d0a492ccaabd6d99\\\" \\\"Mediator\\\",4),(AssetId \\\"58a4d8a00bf87cdc21af08c0fbbe0c8d239ea428386df0b1494cfd97\\\" \\\"Withdrawer\\\",4)] available.\"", error: CoinSelectionFailed, details: {"contents":"Insufficient lovelace available for coin selection: valueFromList [(AdaAssetId,9340124)] required, but valueFromList [(AdaAssetId,5172320),(AssetId \"0122e5079749e36fc703335621430e65017231726166e319a32ed831\" \"Other provider\",4),(AssetId \"32dada46a8f74bc884627f1258f52777b9a95bd6d0a492ccaabd6d99\" \"Mediator\",4),(AssetId \"58a4d8a00bf87cdc21af08c0fbbe0c8d239ea428386df0b1494cfd97\" \"Withdrawer\",4)] available.","tag":"CoinSelectionFailed"} })# + + @submission-failure-of-input-application-for-closed-contract + Scenario Outline: User tries to apply the input on a closed contract + Given I use alice lace browser + Given I am on the "home" page + When I authorize the app + + When I click the "button" with "Create a contract" text + And I generate "DoubleDeposit" contract with "alice" as a first depositor and "bob" as a second depositor and call it "double-deposit" + And I enter the json of the contract "double-deposit" into the "contract-input" field + + When I click the "button" with "Submit contract" text And sign the transaction + Then I can see "double-deposit" contract id in the first row in the table + + Then I should see success toast and close it + Then I should see "Advance" status of the "double-deposit" contract + # We leave this user on the input application page + When I start advancing "double-deposit" contract + + # and switch to the second user + Given I use bob lace browser + Given I am on the "home" page + When I authorize the app + Then I should see "Advance" status of the "double-deposit" contract + When I start advancing "double-deposit" contract + And I click the "checkbox" with "Deposit 2 ₳" text + When I click the "button" with "Advance contract" text And sign the transaction + Then I should see "Syncing" status of the "double-deposit" contract + Then I should see "Complete" status of the "double-deposit" contract + + # Now we switch back to the first user + Given I use alice lace browser + When I click the "button" with "Advance contract" text + # FAIULRE + Then I should see error toast + # The actual error: + # An error occured during contract submission: (ServerApiError ApiError { message: "LoadMarloweContextErrorNotFound", error: LoadMarloweContextErrorNotFound, details: {"tag":"LoadMarloweContextErrorNotFound"} })# + + @submission-failure-of-exclusive-input-application + Scenario Outline: Two users try to apply the inputs which are exclusive + Given I use alice lace browser + Given I am on the "home" page + When I authorize the app + + When I click the "button" with "Create a contract" text + And I generate "DoubleDepositAndNotify" contract with "alice" as a first depositor and "bob" as a second depositor and call it "double-deposit" + And I enter the json of the contract "double-deposit" into the "contract-input" field + + When I click the "button" with "Submit contract" text And sign the transaction + Then I can see "double-deposit" contract id in the first row in the table + Then I should see success toast and close it + + Then I should see "Advance" status of the "double-deposit" contract + # We leave this user on the input application page + When I start advancing "double-deposit" contract + + # and switch to the second user + Given I use bob lace browser + Given I am on the "home" page + When I authorize the app + Then I should see "Advance" status of the "double-deposit" contract + When I start advancing "double-deposit" contract + And I click the "checkbox" with "Deposit 2 ₳" text + When I click the "button" with "Advance contract" text And sign the transaction + Then I should see success toast and close it + Then I should see "Syncing" status of the "double-deposit" contract + Then I should see "Advance" status of the "double-deposit" contract + + # Now we switch back to the first user + Given I use alice lace browser + When I click the "button" with "Advance contract" text + # FAIULRE + Then I should see error toast + # The actual error: + # An error occured during contract submission: (ServerApiError ApiError { message: "MarloweComputeTransactionFailed \"TEApplyNoMatchError\"", error: MarloweComputeTransactionFailed, details: {"contents":"TEApplyNoMatchError","tag":"MarloweComputeTransactionFailed"} })# + + # This artificial scenario is actually trying to test a behavior and error reporting + # of submission of an invalid transaction. + # * We ask the Runtime to create a transaction and keep it + # * We submit a colliding transaction + # * We try to submit the kept transaction + @input-application-failure-of-expired-transaction + Scenario Outline: Two users try to apply the inputs which are exclusive. The second submission is done using old transaction + Given I use alice lace browser + Given I am on the "home" page + When I authorize the app + + When I click the "button" with "Create a contract" text + And I generate "DoubleDepositAndNotify" contract with "alice" as a first depositor and "bob" as a second depositor and call it "double-deposit" + And I enter the json of the contract "double-deposit" into the "contract-input" field + + When I click the "button" with "Submit contract" text And sign the transaction + Then I can see "double-deposit" contract id in the first row in the table + Then I should see success toast and close it + + Then I should see "Advance" status of the "double-deposit" contract + # We leave this user on the input application page + When I start advancing "double-deposit" contract + When I click the "button" with "Advance contract" text And grab wallet popup + + # and switch to the second user + Given I use bob lace browser + Given I am on the "home" page + When I authorize the app + Then I should see "Advance" status of the "double-deposit" contract + When I start advancing "double-deposit" contract + And I click the "checkbox" with "Deposit 2 ₳" text + When I click the "button" with "Advance contract" text And sign the transaction + Then I should see success toast and close it + Then I should see "Syncing" status of the "double-deposit" contract + Then I should see "Advance" status of the "double-deposit" contract + + # Now we switch back to the first user + Given I switch to "alice" lace browser and finally sign the transaction + Then I should see error toast + # The actual error: + # Status Code: 403 + # { + # "details": null, + # "errorCode": "SubmissionError", + # "message": "SubmitFailed \"TxValidationErrorInMode (ShelleyTxValidationError ShelleyBasedEraBabbage (ApplyTxError [UtxowFailure (AlonzoInBabbageUtxowPredFailure (NonOutputSupplimentaryDatums (fromList [SafeHash \\\"68534c553c71d5435372939ddcb3befe73f51d4036c6651d8ad122be55b45085\\\"]) (fromList [SafeHash \\\"14a0df629b39faa3b76b8c2aaaa59d7cbfe9e5f297a1f785b7e9660c37f1c53f\\\"]))),UtxowFailure (AlonzoInBabbageUtxowPredFailure (ExtraRedeemers [RdmrPtr Spend 1])),UtxowFailure (AlonzoInBabbageUtxowPredFailure (PPViewHashesDontMatch (SJust (SafeHash \\\"1557169995ed8cb99308b23de184cc46e8ea43a41024df2bba978ac76906584f\\\")) (SJust (SafeHash \\\"27641f03212d3ab79d4c420a630e6db8586edb40fb1d8eadfbb42b3303ef6e79\\\")))),UtxowFailure (UtxoFailure (AlonzoInBabbageUtxoPredFailure (ValueNotConservedUTxO (MaryValue 7790191 (MultiAsset (fromList []))) (MaryValue 9790191 (MultiAsset (fromList [])))))),UtxowFailure (UtxoFailure (AlonzoInBabbageUtxoPredFailure (BadInputsUTxO (fromList [TxIn (TxId {unTxId = SafeHash \\\"eea2f67cce5ccb720897f10365d9c8bee81c4c8af3192cdc27bdf598dc173b5f\\\"}) (TxIx 1)]))))])) BabbageEraInCardanoMode\"" + # } + # diff --git a/e2e/src/step-definitions/app.ts b/e2e/src/step-definitions/app.ts index 52e9525c..7acf1a0a 100644 --- a/e2e/src/step-definitions/app.ts +++ b/e2e/src/step-definitions/app.ts @@ -8,7 +8,16 @@ When( /^I should see (error|success) toast$/, async function(this: ScenarioWorld, toastType: ToastType) { const { page } = this.getScreen(); - await waitForTestIdVisible(page, "toast-" + toastType + "-msg"); + await waitForTestIdVisible(page, "toast-" + toastType + "-msg", 600000); } ) +When( + /^I should see (error|success) toast and close it$/, + async function(this: ScenarioWorld, toastType: ToastType) { + const { page } = this.getScreen(); + const toast = await waitForTestIdVisible(page, "toast-" + toastType + "-msg", 60000); + const closeButton = toast.locator(".btn-close"); + await closeButton.click(); + } +) diff --git a/e2e/src/step-definitions/contracts.ts b/e2e/src/step-definitions/contracts.ts index a820203e..bfabb22e 100644 --- a/e2e/src/step-definitions/contracts.ts +++ b/e2e/src/step-definitions/contracts.ts @@ -121,4 +121,28 @@ When( } ); - +// And I generate "SimpleNotify" and call it "notify" +When( + /^I generate "([^"]*)" and call it "([^"]*)"$/, + async function(this: ScenarioWorld, contractName: string, contractNickname: string) { + const walletAddress = await this.getWalletAddress(); + let contract: Contract; + switch (contractName) { + case "SimpleDeposit": + contract = mkSimpleDeposit(walletAddress); + break; + case "SimpleChoice": + contract = mkSimpleChoice(walletAddress); + break; + case "TimedOutSimpleChoice": + contract = mkTimedOutSimpleChoice(walletAddress); + break; + case "SimpleNotify": + contract = mkSimpleNotify(); + break; + default: + throw new Error("Unknown contract type: " + contractName); + } + this.setContractInfo(contractNickname, { contract: contract, contractId: undefined }); + } +); diff --git a/e2e/src/step-definitions/contracts/doubleDeposit.ts b/e2e/src/step-definitions/contracts/doubleDeposit.ts new file mode 100644 index 00000000..4bae7a52 --- /dev/null +++ b/e2e/src/step-definitions/contracts/doubleDeposit.ts @@ -0,0 +1,46 @@ +import { When } from '@cucumber/cucumber'; +import { ScenarioWorld } from '../world.js'; +import { + Contract, + datetoTimeout, +} from "@marlowe.io/language-core-v1"; +import { Bech32 } from '../../cardano.js'; + + +const mkDoubleDeposit = (address1: Bech32, address2: Bech32): Contract => { + const twentyMinutesInMilliseconds = 20 * 60 * 1000; + const inTwentyMinutes = datetoTimeout(new Date(Date.now() + twentyMinutesInMilliseconds)); + return { + timeout: inTwentyMinutes, + timeout_continuation: "close", + when: [ + { case: { + party: {address: address1.toString()}, + deposits: 1000000n, + of_token: { currency_symbol: "", token_name: "" }, + into_account: {address: address1.toString()} + }, + then: "close", + }, + { case: { + party: {address: address2.toString()}, + deposits: 2000000n, + of_token: { currency_symbol: "", token_name: "" }, + into_account: {address: address2.toString()} + }, + then: "close", + }, + ] + }; +} + +When( + /^I generate "DoubleDeposit" contract with "([^"]*)" as a first depositor and "([^"]*)" as a second depositor and call it "([^"]*)"$/, + async function(this: ScenarioWorld, first: string, second: string, contractNickname: string) { + const firstAddress = await this.getWalletAddress(first); + const secondAddress = await this.getWalletAddress(second); + const contract = mkDoubleDeposit(firstAddress, secondAddress); + this.setContractInfo(contractNickname, { contract: contract, contractId: undefined }); + } +); + diff --git a/e2e/src/step-definitions/contracts/doubleDepositAndNotify.ts b/e2e/src/step-definitions/contracts/doubleDepositAndNotify.ts new file mode 100644 index 00000000..e86e82bd --- /dev/null +++ b/e2e/src/step-definitions/contracts/doubleDepositAndNotify.ts @@ -0,0 +1,59 @@ +import { When } from '@cucumber/cucumber'; +import { ScenarioWorld } from '../world.js'; +import { + Contract, + datetoTimeout, +} from "@marlowe.io/language-core-v1"; +import { Bech32 } from '../../cardano.js'; + + +const mkDoubleDepositAndNotify = (address1: Bech32, address2: Bech32): Contract => { + const twentyMinutesInMilliseconds = 20 * 60 * 1000; + const inTwentyMinutes = datetoTimeout(new Date(Date.now() + twentyMinutesInMilliseconds)); + const notifyContinuation:Contract = { + timeout: inTwentyMinutes, + timeout_continuation: "close", + "when": [ + { + "then": "close", + "case": { + "notify_if": true + } + } + ], + }; + + return { + timeout: inTwentyMinutes, + timeout_continuation: "close", + when: [ + { case: { + party: {address: address1.toString()}, + deposits: 1000000n, + of_token: { currency_symbol: "", token_name: "" }, + into_account: {address: address1.toString()} + }, + then: notifyContinuation, + }, + { case: { + party: {address: address2.toString()}, + deposits: 2000000n, + of_token: { currency_symbol: "", token_name: "" }, + into_account: {address: address2.toString()} + }, + then: notifyContinuation, + }, + ] + }; +} + +When( + /^I generate "DoubleDepositAndNotify" contract with "([^"]*)" as a first depositor and "([^"]*)" as a second depositor and call it "([^"]*)"$/, + async function(this: ScenarioWorld, first: string, second: string, contractNickname: string) { + const firstAddress = await this.getWalletAddress(first); + const secondAddress = await this.getWalletAddress(second); + const contract = mkDoubleDepositAndNotify(firstAddress, secondAddress); + this.setContractInfo(contractNickname, { contract: contract, contractId: undefined }); + } +); + diff --git a/e2e/src/step-definitions/hooks.ts b/e2e/src/step-definitions/hooks.ts index cd42be55..60bfccf7 100644 --- a/e2e/src/step-definitions/hooks.ts +++ b/e2e/src/step-definitions/hooks.ts @@ -26,6 +26,7 @@ After(async function(this: ScenarioWorld, scenario) { if (scenarioStatus === 'FAILED' && screen) { const page = screen.page; + await page.evaluate(() => window.scrollTo(0, 0)) const screenshot = await page.screenshot({ path: `${env('SCREENSHOT_PATH')}${scenario.pickle.name}.png` }); diff --git a/e2e/src/step-definitions/popup.ts b/e2e/src/step-definitions/popup.ts new file mode 100644 index 00000000..a68814d6 --- /dev/null +++ b/e2e/src/step-definitions/popup.ts @@ -0,0 +1,7 @@ +import { Page } from "playwright"; + +export const grabPopup = async function (page: Page, triggerPopup: () => Promise): Promise { + const popupPromise:Promise = new Promise(resolve => page.context().once('page', resolve)); + await triggerPopup(); + return await popupPromise; +} diff --git a/e2e/src/step-definitions/wallets.ts b/e2e/src/step-definitions/wallets.ts index e5cac184..1215f6b5 100644 --- a/e2e/src/step-definitions/wallets.ts +++ b/e2e/src/step-definitions/wallets.ts @@ -4,6 +4,7 @@ import { AccessibilityRole, waitForRoleVisible } from "../support/wait-for-behav import * as nami from "./wallets/nami.js"; import * as lace from "./wallets/lace.js"; import { Page } from 'playwright'; +import { WalletPopup } from './wallets/walletPopup.js'; When( /^I use ([^ ]*) (nami|lace) browser$/, @@ -67,12 +68,13 @@ When( const locator = await waitForRoleVisible(page, role, name); await locator.click(); } + const walletPopup = await WalletPopup.fromTrigger(page, triggerSign); switch (wallet.type) { case 'nami': - await nami.signTx(page, triggerSign); + await nami.signTx(walletPopup); break; case 'lace': - await lace.signTx(page, triggerSign); + await lace.signTx(walletPopup); break; default: throw new Error('Unknown wallet type'); @@ -80,3 +82,46 @@ When( } ); +When( + /^I click the "([^"]*)" with "([^"]*)" text And grab wallet popup$/, + async function(this: ScenarioWorld, role: AccessibilityRole, name: string) { + const { page } = this.getScreen(); + const triggerSign = async () => { + const locator = await waitForRoleVisible(page, role, name); + await locator.click(); + } + const walletPopup = await WalletPopup.fromTrigger(page, triggerSign); + this.setWalletPopup(walletPopup); + } +); + +When( + /^I switch to "([^ ]*)" (nami|lace) browser and finally sign the transaction$/, + async function(this: ScenarioWorld, walletName: string, walletType: string) { + const { screens } = this; + switch (walletType) { + case 'nami': + this.screen = await screens('nami', walletName); + break; + case 'lace': + this.screen = await screens('lace', walletName); + break; + default: + throw new Error('Unknown wallet type'); + } + const walletPopup:WalletPopup|undefined = this.getScreen().walletPopup; + if(walletPopup === undefined) { + throw new Error("Wallet popup was probably already closed"); + } + + switch (walletType) { + case 'nami': + await nami.signTx(walletPopup); + break; + case 'lace': + await lace.signTx(walletPopup); + break; + default: + throw new Error('Unknown wallet type'); + } +}); diff --git a/e2e/src/step-definitions/wallets/lace.ts b/e2e/src/step-definitions/wallets/lace.ts index 299596f9..9d64f100 100644 --- a/e2e/src/step-definitions/wallets/lace.ts +++ b/e2e/src/step-definitions/wallets/lace.ts @@ -2,6 +2,8 @@ import { Locator, Page } from 'playwright'; import { waitFor, waitForRoleVisible, waitForSelectorVisible, waitForTestIdVisible } from "../../support/wait-for-behavior.js"; import { inputValue } from '../../support/html-behavior.js'; import { Bech32 } from '../../cardano.js'; +import { grabPopup } from '../popup.js'; +import { WalletPopup } from './walletPopup.js'; var SPENDING_PASSWORD: string = "Runner test"; @@ -117,47 +119,47 @@ export const authorizeApp = async function (page: Page, triggerAuthorization: () const grantAccess:Promise = (async () => { const page = await walletPopupPromise; await page.reload(); - await waitFor(async ():Promise => { - const locator = page.getByRole("button", { name: "Authorize", exact: true }); - const result = await locator.isVisible(); - if (result) { - await locator.click(); - return result; - } - return true - }, { label: "Authorize button" }); - - await waitFor(async() => { - const locator = page.getByRole("button", { name: "Always", exact: true }); - const result = await locator.isVisible(); - if (result) { - await locator.click(); - return result; - } - }, { label: "Always button" }); + let locator: Locator; + + locator = await waitForRoleVisible(page, "button", "Authorize"); + await locator.click(); + + locator = await waitForRoleVisible(page, "button", "Always"); + await locator.click(); })(); + // Playwright `waitFor` doesn't support aborting the promise + // so the loosing one will run till the timeout and the result + // will be ignored. await Promise.any([isAuthorizedCheck(page), grantAccess]); await isAuthorizedCheck(page); } -export const signTx = async (page: Page, triggerSign: () => Promise): Promise => { +export const signTx = async (walletPopupWrapper: WalletPopup): Promise => { var locator: Locator; - const walletPopupPromise:Promise = new Promise(resolve => page.context().once('page', resolve)); - await triggerSign(); - const walletPopup = await walletPopupPromise; - await walletPopup.reload(); - + let possibleWalletPopup:Page|undefined = walletPopupWrapper.getPage(); + if(possibleWalletPopup === undefined) { + throw new Error("Wallet popup was probably already closed"); + } + let walletPopup:Page = possibleWalletPopup; locator = await waitForRoleVisible(walletPopup, "button", "Confirm"); await locator.click(); locator = await waitForTestIdVisible(walletPopup, "password-input"); await inputValue(locator, SPENDING_PASSWORD); - locator = await waitForRoleVisible(walletPopup, "button", "Confirm"); - await locator.click(); + const confirm = async function() { + locator = await waitForRoleVisible(walletPopup, "button", "Confirm"); + await locator.click(); + } + + await confirm(); + // Sometimes we have to double confirm or we will stuck on the popup + locator = await waitForRoleVisible(walletPopup, "button", "Close").catch(async () => { + await confirm().catch(async () => { return }); + return await waitForRoleVisible(walletPopup, "button", "Close"); + }); - locator = await waitForRoleVisible(walletPopup, "button", "Close"); await locator.click(); } diff --git a/e2e/src/step-definitions/wallets/nami.ts b/e2e/src/step-definitions/wallets/nami.ts index 1fa1b381..85929079 100644 --- a/e2e/src/step-definitions/wallets/nami.ts +++ b/e2e/src/step-definitions/wallets/nami.ts @@ -3,6 +3,7 @@ import { waitFor, waitForRoleVisible } from "../../support/wait-for-behavior.js" import { inputValue } from '../../support/html-behavior.js'; import { sleep } from '../../promise.js'; import { Bech32 } from '../../cardano.js'; +import { WalletPopup } from './walletPopup.js'; var SPENDING_PASSWORD: string = "Runner test"; @@ -118,12 +119,13 @@ export const authorizeApp = async function (page: Page, triggerAuthorization: () await isAuthorized(); } -export const signTx = async (page: Page, triggerSign: () => Promise): Promise => { +export const signTx = async (walletPopupWrapper: WalletPopup): Promise => { var locator: Locator; - const walletPopupPromise:Promise = new Promise(resolve => page.context().once('page', resolve)); - await triggerSign(); - const walletPopup = await walletPopupPromise; - await walletPopup.reload(); + let possibleWalletPopup:Page|undefined = walletPopupWrapper.getPage(); + if(possibleWalletPopup === undefined) { + throw new Error("Wallet popup was probably already closed"); + } + let walletPopup:Page = possibleWalletPopup; locator = await waitForRoleVisible(walletPopup, "button", "Sign"); await locator.click(); diff --git a/e2e/src/step-definitions/wallets/walletPopup.ts b/e2e/src/step-definitions/wallets/walletPopup.ts new file mode 100644 index 00000000..a9473534 --- /dev/null +++ b/e2e/src/step-definitions/wallets/walletPopup.ts @@ -0,0 +1,26 @@ +import { Page } from 'playwright'; +import { grabPopup } from '../popup.js'; + +// TODO: move the module level api which we have +// here and turn this into an interface. +export class WalletPopup { + private page: Page; + + constructor(page: Page) { + this.page = page; + } + + public getPage(): Page | undefined { + if(!this.page.isClosed()) { + return this.page; + } + } + + public static async fromTrigger(page: Page, triggerPopup: () => Promise): Promise { + const walletPopup:Page = await grabPopup(page, triggerPopup); + await walletPopup.reload(); + return new WalletPopup(walletPopup); + } +} + + diff --git a/e2e/src/step-definitions/world.ts b/e2e/src/step-definitions/world.ts index ef3ec027..647091c7 100644 --- a/e2e/src/step-definitions/world.ts +++ b/e2e/src/step-definitions/world.ts @@ -13,11 +13,13 @@ import { GlobalConfig } from '../env/global.js'; import GlobalStateManager from "../support/globalStateManager.js"; import { Bech32, ContractId } from '../cardano.js'; import { Contract } from '@marlowe.io/language-core-v1'; +import { WalletPopup } from './wallets/walletPopup.js'; export type WalletType = "nami" | "lace"; type Screen = { page: Page + walletPopup: WalletPopup | undefined, // TODO: Hide contect and expose only page related methods context: BrowserContext, // getDefaultPage: () => Promise @@ -89,6 +91,10 @@ export class ScenarioWorld extends World { this.contracts[contractName] = contractInfo; } + public setWalletPopup(walletPopup: WalletPopup): void { + this.getScreen().walletPopup = walletPopup; + } + public async getWalletAddress(walletName?:string): Promise { // Just check if there is any wallet configured in the cache for a given user and grab the address if(walletName === undefined) { @@ -169,7 +175,7 @@ export class ScenarioWorld extends World { throw new Error("Wallet configuration failed - wallet address is not the same as expected: given " + address + " != expected " + expectedAddress); } const wallet = { address, name: walletName, mnemonic, url: walletURL, type: walletType }; - const screen = { context, page, wallet }; + const screen = { context, page, wallet, walletPopup: undefined }; cache[walletType][walletName] = screen; return screen; } diff --git a/nix/gen/spago-packages.nix b/nix/gen/spago-packages.nix index 24e6f7e3..7f91ad66 100644 --- a/nix/gen/spago-packages.nix +++ b/nix/gen/spago-packages.nix @@ -187,11 +187,11 @@ let "cardano-multiplatform-lib" = pkgs.stdenv.mkDerivation { name = "cardano-multiplatform-lib"; - version = "3929298b6a650f15d9d270b91b4415b9b5457601"; + version = "v0.0.2"; src = pkgs.fetchgit { url = "https://github.com/input-output-hk/purescript-cardano-multiplatform-lib.git"; - rev = "3929298b6a650f15d9d270b91b4415b9b5457601"; - sha256 = "1dm2gg1m3yallck3x6akzjkh7b2fyd1biwr9wgyb5ylb7nwqqxx4"; + rev = "d06bdedf43a46f27484c040e7f30f213bebe2396"; + sha256 = "1yqiy7n8giahsx59nsbzfbs5clwnw96bpj5yv575fxjw1cmfa691"; }; phases = "installPhase"; installPhase = "ln -s $src $out"; @@ -883,11 +883,11 @@ let "marlowe-runtime-client" = pkgs.stdenv.mkDerivation { name = "marlowe-runtime-client"; - version = "v0.3.2"; + version = "v0.3.7"; src = pkgs.fetchgit { url = "https://github.com/input-output-hk/purescript-marlowe-runtime-client.git"; - rev = "3c9b5af0adad6bd86233e015785dade783f968d8"; - sha256 = "0mr8dfgd7gskbar4l31xfiq05ls4m7xbblksj5mjbc9f784hiwfv"; + rev = "fb84dd40df1b261effb87db4440ae70ad381886c"; + sha256 = "03j5cc4rvsa3k2hm7sbhbmpy2ri1qzkh38ppb4g72a0aqhkik5dx"; }; phases = "installPhase"; installPhase = "ln -s $src $out"; diff --git a/packages.dhall b/packages.dhall index 78dc9507..d4b6d5a0 100644 --- a/packages.dhall +++ b/packages.dhall @@ -289,7 +289,7 @@ in upstream , "web-encoding" ] "https://github.com/input-output-hk/purescript-cardano-multiplatform-lib.git" - "3929298b6a650f15d9d270b91b4415b9b5457601" + "v0.0.2" with cardano-wallet-client = mkPackage @@ -322,7 +322,6 @@ in upstream ] "https://github.com/input-output-hk/purescript-cardano-wallet-client.git" "v0.1.1" - -- with marlowe-runtime-client = ../purescript-marlowe-runtime-client/spago.dhall as Location with marlowe-runtime-client = mkPackage [ "aff" @@ -416,7 +415,7 @@ in upstream , "web-html" ] "https://github.com/input-output-hk/purescript-marlowe-runtime-client.git" - "v0.3.2" + "v0.3.7" with errors = mkPackage diff --git a/src/Component/ApplyInputs.purs b/src/Component/ApplyInputs.purs index 1a5f0f81..78e0ad3a 100644 --- a/src/Component/ApplyInputs.purs +++ b/src/Component/ApplyInputs.purs @@ -17,7 +17,6 @@ import Component.Types.ContractInfo as ContractInfo import Component.Widgets (backToContractListLink, link, marlowePreview, marloweStatePreview) import Component.Widgets as Widgets import Contrib.Data.FunctorWithIndex (mapWithIndexFlipped) -import Contrib.Fetch (FetchError) import Contrib.Polyform.FormSpecBuilder (evalBuilder') import Contrib.Polyform.FormSpecs.StatefulFormSpec as StatefulFormSpec import Contrib.Polyform.FormSpecs.StatelessFormSpec as StatelessFormSpec @@ -98,7 +97,11 @@ create contractData serverUrl contractsEndpoint = do post' serverUrl contractsEndpoint req -submit :: CborHex TransactionWitnessSetObject -> ServerURL -> TransactionEndpoint -> Aff (Either FetchError Unit) +submit + :: CborHex TransactionWitnessSetObject + -> ServerURL + -> TransactionEndpoint + -> Aff (Either (ClientError String) Unit) submit witnesses serverUrl contractEndpoint = do let textEnvelope = toTextEnvelope witnesses "" diff --git a/src/Component/ApplyInputs/Machine.purs b/src/Component/ApplyInputs/Machine.purs index bd7cec61..e4cb45b4 100644 --- a/src/Component/ApplyInputs/Machine.purs +++ b/src/Component/ApplyInputs/Machine.purs @@ -477,8 +477,9 @@ requestToAffAction = case _ of SubmitTxRequest { txWitnessSet, createTxResponse, serverURL } -> do let action = submit txWitnessSet serverURL createTxResponse.links.transaction >>= case _ of - Right _ -> do + Right response -> do n <- liftEffect $ now + pure $ SubmitTxSucceeded n Left err -> pure $ SubmitTxFailed $ show err action `catchError` (pure <<< SubmitTxFailed <<< show) @@ -516,7 +517,7 @@ submit :: CborHex TransactionWitnessSetObject -> ServerURL -> TransactionEndpoint - -> Aff (Either FetchError Unit) + -> Aff (Either (ClientError String) Unit) submit witnesses serverUrl transactionEndpoint = do let textEnvelope = toTextEnvelope witnesses "" diff --git a/src/Component/ContractList.purs b/src/Component/ContractList.purs index 006eba6b..cfdc5b38 100644 --- a/src/Component/ContractList.purs +++ b/src/Component/ContractList.purs @@ -3,8 +3,7 @@ module Component.ContractList where import Prelude import Cardano as Cardano -import CardanoMultiplatformLib (CborHex, bech32ToString) -import CardanoMultiplatformLib.Transaction (TransactionWitnessSetObject) +import CardanoMultiplatformLib (bech32ToString) import Component.ApplyInputs as ApplyInputs import Component.ApplyInputs.Machine (mkEnvironment) import Component.ApplyInputs.Machine as ApplyInputs.Machine @@ -14,7 +13,7 @@ import Component.ContractTemplates.Escrow as Escrow import Component.ContractTemplates.Swap as Swap import Component.CreateContract (runnerTag) import Component.CreateContract as CreateContract -import Component.InputHelper (addressesInContract, canInput, rolesInContract) +import Component.InputHelper (addressesInContract, addressesInState, allInputs, canInput, rolesInContract, rolesInState) import Component.Types (ContractInfo(..), ContractJsonString, MessageContent(..), MessageHub(..), MkComponentM, Page(..), WalletInfo) import Component.Types.ContractInfo (ContractStatus(..), MarloweInfo(..), SomeContractInfo(..), contractStatusMarloweInfo, contractStatusTransactionsHeadersWithEndpoints) import Component.Types.ContractInfo as ContractInfo @@ -22,7 +21,6 @@ import Component.Widget.Table (orderingHeader) as Table import Component.Widgets (buttonOutlinedInactive, buttonOutlinedPrimary, buttonOutlinedWithdraw) import Component.Withdrawals as Withdrawals import Contrib.Data.JSDate (toLocaleDateString, toLocaleTimeString) as JSDate -import Contrib.Fetch (FetchError) import Contrib.Polyform.FormSpecBuilder (evalBuilder') import Contrib.Polyform.FormSpecs.StatelessFormSpec (renderFormSpec) import Contrib.React.Svg (loadingSpinnerLogo) @@ -40,7 +38,7 @@ import Data.Array.NonEmpty (NonEmptyArray) import Data.Array.NonEmpty as NonEmptyArray import Data.DateTime.Instant (Instant, unInstant) import Data.DateTime.Instant as Instant -import Data.Either (Either, hush) +import Data.Either (Either(..), hush) import Data.Foldable (fold, for_, or) import Data.FormURLEncoded.Query (FieldId(..), Query) import Data.Function (on) @@ -58,14 +56,14 @@ import Data.Time.Duration as Duration import Data.Tuple (fst, snd) import Data.Tuple.Nested (type (/\)) import Effect (Effect) -import Effect.Aff (Aff, delay, launchAff_) +import Effect.Aff (delay, launchAff_) import Effect.Class (liftEffect) import Effect.Now as Now import Foreign.Object as Object +import Language.Marlowe.Core.V1.Semantics (emptyState) as V1 +import Language.Marlowe.Core.V1.Semantics.Types (Action(..), Address, Case(..), Contract(..), CurrencySymbol, Environment, Party(..), State) as V1 import Language.Marlowe.Core.V1.Semantics.Types (Contract) -import Language.Marlowe.Core.V1.Semantics.Types as V1 -import Marlowe.Runtime.Web.Client (put') -import Marlowe.Runtime.Web.Types (Payout(..), PutTransactionRequest(..), ServerURL, Tags(..), TransactionEndpoint, TransactionsEndpoint, TxOutRef, toTextEnvelope, txOutRefToString) +import Marlowe.Runtime.Web.Types (Payout(..), Tags(..), TransactionsEndpoint, TxOutRef, txOutRefToString) import Marlowe.Runtime.Web.Types as Runtime import Polyform.Validator (liftFnM) import Promise.Aff as Promise @@ -130,14 +128,6 @@ data OrderBy derive instance Eq OrderBy -submit :: CborHex TransactionWitnessSetObject -> ServerURL -> TransactionEndpoint -> Aff (Either FetchError Unit) -submit witnesses serverUrl transactionEndpoint = do - let - textEnvelope = toTextEnvelope witnesses "" - - req = PutTransactionRequest textEnvelope - put' serverUrl transactionEndpoint req - data ContractTemplate = Escrow | Swap | ContractForDifferencesWithOracle derive instance Eq ContractTemplate @@ -606,14 +596,15 @@ walletParties :: WalletContext -> Maybe V1.CurrencySymbol -> V1.Contract + -> V1.State -> Either String (Array V1.Party) -walletParties walletContext@(WalletContext { changeAddress, usedAddresses }) possibleCurrencySymbol contract = do +walletParties walletContext@(WalletContext { changeAddress, usedAddresses }) possibleCurrencySymbol contract state = do roleParties <- case possibleCurrencySymbol of Nothing -> pure [] Just currencySymbol -> case Cardano.policyIdFromHexString currencySymbol of Just policyId -> do let - contractRoles = rolesInContract contract + contractRoles = Array.nub $ rolesInContract contract <> rolesInState state WalletContext { balance: Cardano.Value balanceValue } = walletContext assetsIds2Role = contractRoles <#> \tokenName -> do let @@ -626,7 +617,7 @@ walletParties walletContext@(WalletContext { changeAddress, usedAddresses }) pos let contractAddresses :: Array V1.Address - contractAddresses = addressesInContract contract + contractAddresses = Array.nub $ addressesInContract contract <> addressesInState state addressParties :: Array V1.Party addressParties = @@ -674,11 +665,28 @@ contractActionsContext env walletContext submittedPayouts sc@(SyncedConractInfo let MarloweInfo { initialContract, currencySymbol, currentContract, state: currentState, unclaimedPayouts } = marloweInfo - possiblyParties = hush $ walletParties walletContext currencySymbol initialContract + possiblyParties = hush $ walletParties walletContext currencySymbol initialContract (fromMaybe V1.emptyState currentState) possiblyCanWithdraw = do parties <- possiblyParties payouts <- NonEmptyArray.fromArray $ remainingPayouts contractId parties submittedPayouts unclaimedPayouts pure $ CanWithdraw payouts + + -- FIXME: + -- This is redunant and repetitive. We introduced this extra check because the `possiblyCanAdvance` + -- short circuits on non party user. In the future we would like to handle unrelated contracts as well. + -- Improve these two checks. + possiblyAnyoneCanAdvance = do + contract <- currentContract + state <- currentState + let + canAdvance = CanAdvance + { contract, state, transactionsEndpoint: endpoints.transactions, initialContract } + case allInputs env state contract of + Left _ -> pure canAdvance + Right { notifies } -> + if not $ Array.null notifies then pure canAdvance + else Nothing + possiblyCanAdvance = do parties <- possiblyParties contract <- currentContract @@ -690,7 +698,7 @@ contractActionsContext env walletContext submittedPayouts sc@(SyncedConractInfo possiblyActions :: Maybe (These CanAdvance CanWithdraw) possiblyActions = maybeThese - possiblyCanAdvance + (possiblyCanAdvance <|> possiblyAnyoneCanAdvance) possiblyCanWithdraw case possiblyActions of diff --git a/src/Component/CreateContract/Machine.purs b/src/Component/CreateContract/Machine.purs index bc4b8d51..6616cd07 100644 --- a/src/Component/CreateContract/Machine.purs +++ b/src/Component/CreateContract/Machine.purs @@ -7,7 +7,6 @@ import CardanoMultiplatformLib as CardanoMultiplatformLib import CardanoMultiplatformLib.Transaction (TransactionObject, TransactionWitnessSetObject) import Component.InputHelper (rolesInContract) import Component.Types (WalletInfo(..)) -import Contrib.Fetch (FetchError) import Control.Monad.Error.Class (catchError) import Data.Array as Array import Data.Array.NonEmpty (NonEmptyArray) @@ -305,7 +304,7 @@ submit :: CborHex TransactionWitnessSetObject -> ServerURL -> ContractEndpoint - -> Aff (Either FetchError Unit) + -> Aff (Either (ClientError String) Unit) submit witnesses serverUrl contractEndpoint = do let textEnvelope = toTextEnvelope witnesses "" diff --git a/src/Component/InputHelper.purs b/src/Component/InputHelper.purs index a485e683..b9905253 100644 --- a/src/Component/InputHelper.purs +++ b/src/Component/InputHelper.purs @@ -14,15 +14,17 @@ import Data.Either (Either(..)) import Data.Foldable (foldM) import Data.List (List) import Data.List as List +import Data.Map as Map import Data.Maybe (Maybe(..)) +import Data.Set as Set import Data.Time.Duration (Milliseconds(..), negateDuration) +import Data.Tuple (fst) import Data.Tuple.Nested (type (/\), (/\)) import Language.Marlowe.Core.V1.Folds (MapStep(..), foldMapContract) import Language.Marlowe.Core.V1.Semantics (applyCases, evalObservation, reduceContractStep) as V1 import Language.Marlowe.Core.V1.Semantics (evalObservation, evalValue, reduceContractUntilQuiescent) import Language.Marlowe.Core.V1.Semantics.Types (AccountId, Action(..), Bound, Case(..), ChoiceId(..), Contract(..), Environment(..), Observation(..), Party(..), Payee(..), State, TimeInterval(..), Token, TokenName, Value(..), Address) -import Language.Marlowe.Core.V1.Semantics.Types (ApplyResult(..), Contract, Environment(..), Input(..), InputContent(..), ReduceResult(..), ReduceStepResult(..), State, TimeInterval(..)) as V1 -import Language.Marlowe.Core.V1.Semantics.Types (ApplyResult(..), Contract, Environment(..), Input(..), InputContent(..), ReduceResult(..), ReduceStepResult(..), State, TimeInterval(..)) as V1 +import Language.Marlowe.Core.V1.Semantics.Types (ApplyResult(..), Contract, Environment(..), Input(..), InputContent(..), ReduceResult(..), ReduceStepResult(..), State(..), TimeInterval(..)) as V1 data DepositInput = DepositInput AccountId Party Token BigInt.BigInt (Maybe Contract) @@ -207,6 +209,34 @@ addressesInContract = Array.nub <<< foldMapContract addressesContract (Let _ _ _) = [] addressesContract (Assert _ _) = [] +rolesInState :: V1.State -> Array TokenName +rolesInState (V1.State { accounts, choices }) = do + let + choiceParties = Set.map choiceIdToParty $ Map.keys choices + accountParties = Set.map fst (Map.keys accounts) + Array.catMaybes $ Set.toUnfoldable $ Set.map roleFromParty $ choiceParties `Set.union` accountParties + where + choiceIdToParty :: ChoiceId -> Party + choiceIdToParty (ChoiceId _ party) = party + + roleFromParty :: Party -> Maybe TokenName + roleFromParty (Role t) = Just t + roleFromParty _ = Nothing + +addressesInState :: V1.State -> Array Address +addressesInState (V1.State { accounts, choices }) = do + let + choiceParties = Set.map choiceIdToParty $ Map.keys choices + accountParties = Set.map fst (Map.keys accounts) + Array.catMaybes $ Set.toUnfoldable $ Set.map addressFromParty $ choiceParties `Set.union` accountParties + where + choiceIdToParty :: ChoiceId -> Party + choiceIdToParty (ChoiceId _ party) = party + + addressFromParty :: Party -> Maybe Address + addressFromParty (Address t) = Just t + addressFromParty _ = Nothing + data ExecutionBranch = WhenBranch (Maybe Int) | IfBranch Boolean diff --git a/src/Component/Withdrawals.purs b/src/Component/Withdrawals.purs index c4d87ec3..4206181c 100644 --- a/src/Component/Withdrawals.purs +++ b/src/Component/Withdrawals.purs @@ -34,7 +34,7 @@ import Effect.Aff (Aff, launchAff_) import Effect.Class (liftEffect) import JS.Unsafe.Stringify (unsafeStringify) import Language.Marlowe.Core.V1.Semantics.Types (Ada(..)) as V1 -import Marlowe.Runtime.Web.Client (post', put') +import Marlowe.Runtime.Web.Client (ClientError, post', put') import Marlowe.Runtime.Web.Types (Payout(..), PostWithdrawalsRequest(..), PostWithdrawalsResponseContent(..), PutWithdrawalRequest(..), Runtime(Runtime), ServerURL, TextEnvelope(..), TxOutRef, WithdrawalEndpoint, WithdrawalsEndpoint, toTextEnvelope) import Polyform.Validator (liftFn) import React.Basic (fragment) @@ -192,7 +192,7 @@ submit :: CborHex TransactionWitnessSetObject -> ServerURL -> WithdrawalEndpoint - -> Aff (Either FetchError Unit) + -> Aff (Either (ClientError String) Unit) submit witnesses serverUrl contractEndpoint = do let textEnvelope = toTextEnvelope witnesses ""