import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, fillIn, typeIn, click, settled, waitFor, find, } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import Layer1TestWeb3Strategy from '@cardstack/web-client/utils/web3-strategies/test-layer1'; import Layer2TestWeb3Strategy from '@cardstack/web-client/utils/web3-strategies/test-layer2'; import { WorkflowSession } from '@cardstack/web-client/models/workflow'; import BN from 'bn.js'; import { toWei } from 'web3-utils'; import sinon from 'sinon'; import { TransactionReceipt } from 'web3-core'; import { RelayTokensOptions } from '@cardstack/web-client/utils/web3-strategies/types'; import RSVP, { defer } from 'rsvp'; let layer1Service: Layer1TestWeb3Strategy; let session: WorkflowSession; module( 'Integration | Component | card-pay/deposit-workflow/transaction-amount', function (hooks) { setupRenderingTest(hooks); hooks.beforeEach(async function () { let layer2Service = this.owner.lookup('service:layer2-network'); let layer2Strategy = layer2Service.strategy as Layer2TestWeb3Strategy; // Simulate being connected on layer 2 -- prereq to converting to USD let layer2AccountAddress = '0x182619c6Ea074C053eF3f1e1eF81Ec8De6Eb6E44'; layer2Strategy.test__simulateAccountsChanged([layer2AccountAddress]); session = new WorkflowSession(); session.setValue('depositSourceToken', 'DAI'); layer1Service = this.owner.lookup('service:layer1-network').strategy; const startDaiAmountString = '5.111111111111111110'; const startDaiAmount = toWei(startDaiAmountString); layer1Service.test__simulateBalances({ defaultToken: new BN('0'), dai: new BN(startDaiAmount), card: new BN('0'), }); this.setProperties({ session, }); }); test('it can go through with unlock and deposit', async function (assert) { let completed = false; this.set('onComplete', () => { completed = true; }); await render(hbs` `); const daiToSend = '5'; await fillIn('[data-test-token-amount-input]', daiToSend); assert.dom('[data-test-deposit-button]').isDisabled(); assert.dom('[data-test-unlock-button]').isNotDisabled(); await click('[data-test-unlock-button]'); layer1Service.test__simulateUnlockTxnHash(); await waitFor('[data-test-unlock-etherscan-button]'); assert .dom('[data-test-unlock-etherscan-button]') .containsText('View on Etherscan') .hasAttribute('href', /.+/); layer1Service.test__simulateUnlock(); await settled(); assert.dom('[data-test-deposit-button]').isEnabled(); await click('[data-test-deposit-button]'); assert.dom('[data-test-deposit-button]').containsText('Depositing'); layer1Service.test__simulateDepositTxnHash(); await waitFor('[data-test-deposit-etherscan-button]'); assert .dom('[data-test-deposit-etherscan-button]') .containsText('View on Etherscan') .hasAttribute('href', /.+/); layer1Service.test__simulateDeposit(); await settled(); assert .dom('[data-test-deposit-success-message]') .containsText('Deposited'); assert.ok(completed); }); test('its deposit step can be retried if there is no deposit transaction hash', async function (assert) { session.setValue('unlockTxnHash', 'unlockTokensTxnHash'); session.setValue('depositSourceToken', 'DAI'); session.setValue('depositedAmount', new BN(toWei('5'))); let relayTokensSpy = sinon.spy(layer1Service, 'relayTokens'); await render(hbs` `); await layer1Service.test__simulateUnlock(); await click('[data-test-deposit-button]'); assert.dom('[data-test-deposit-button]').containsText('Depositing'); assert.ok(relayTokensSpy.calledOnce); await new Promise((resolve) => setTimeout(() => resolve(true), 500)); assert.dom('[data-test-deposit-retry-button]').isVisible(); assert .dom('[data-test-deposit-retry-button]') .containsText('Resend Signing Request'); assert.dom('[data-test-deposit-retry-button]').isEnabled(); assert .dom('[data-test-deposit-error-message]') .containsText( `If you haven't received a signing request, make sure to check your connected L1 test chain wallet. You can also try to click on "Resend Signing Request" below, or contact Cardstack Support.` ); await click('[data-test-deposit-retry-button]'); assert.ok(relayTokensSpy.calledTwice); }); test('its deposit step can not be retried if there is a deposit transaction hash', async function (assert) { session.setValue('unlockTxnHash', 'unlockTokensTxnHash'); session.setValue('depositSourceToken', 'DAI'); session.setValue('depositedAmount', new BN(toWei('5'))); await render(hbs` `); await layer1Service.test__simulateUnlock(); session.setValue('relayTokensTxnHash', 'relayTokensTxnHash'); await click('[data-test-deposit-button]'); await new Promise((resolve) => setTimeout(() => resolve(true), 500)); assert.dom('[data-test-deposit-retry-button]').doesNotExist(); }); test('It disables the unlock button when amount entered is more than balance (18-decimal floating point)', async function (assert) { const daiInBalance = '5.111111111111111110'; const moreDaiThanBalance = '5.111111111111111111'; await render(hbs` `); assert.dom('[data-test-unlock-button]').isDisabled(); await fillIn('[data-test-token-amount-input]', moreDaiThanBalance); assert.dom('[data-test-unlock-button]').isDisabled(); assert .dom('[data-test-boxel-input-error-message]') .containsText('Insufficient balance in your account'); await fillIn('[data-test-token-amount-input]', daiInBalance); assert.dom('[data-test-unlock-button]').isNotDisabled(); }); test('It accurately sends the amount to be handled by layer 1 (18-decimal floating point)', async function (assert) { const daiToSend = '5.111111111111111110'; const daiToSendInWei = '5111111111111111110'; await render(hbs` `); let approveSpy = sinon.spy(layer1Service, 'approve'); await fillIn('[data-test-token-amount-input]', daiToSend); assert.dom('[data-test-unlock-button]').isNotDisabled(); await click('[data-test-unlock-button]'); assert.ok( approveSpy.calledWith(new BN(daiToSendInWei), 'DAI'), 'The amount that the approve call is made with matches the amount shown in the UI' ); }); test('It does not accept invalid values and ignores invalid characters typed in', async function (assert) { const startDaiString = '55.111111111111111111'; const validAmount = '2'; const invalidDai1 = '4.1111111111111111112'; /* more than 18 decimal places */ const invalidDai2 = '1 1'; /* has space */ const invalidDai3 = ' 1'; /* has space */ const invalidDai4 = '12/'; /* invalid char */ let startDaiAmount = toWei(startDaiString); await render(hbs` `); layer1Service.test__simulateBalances({ defaultToken: new BN('0'), dai: new BN(startDaiAmount), card: new BN('0'), }); await settled(); assert.dom('[data-test-token-amount-input]').hasValue(''); assert.dom('[data-test-unlock-button]').isDisabled(); await fillIn('[data-test-token-amount-input]', invalidDai1); assert.dom('[data-test-token-amount-input]').hasValue(invalidDai1); assert.dom('[data-test-unlock-button]').isDisabled(); await fillIn('[data-test-token-amount-input]', validAmount); assert.dom('[data-test-token-amount-input]').hasValue(validAmount); assert.dom('[data-test-unlock-button]').isNotDisabled(); await fillIn('[data-test-token-amount-input]', invalidDai2); assert.dom('[data-test-token-amount-input]').hasValue(validAmount); assert.dom('[data-test-unlock-button]').isNotDisabled(); await fillIn('[data-test-token-amount-input]', ''); assert.dom('[data-test-token-amount-input]').hasValue(''); assert.dom('[data-test-unlock-button]').isDisabled(); await typeIn('[data-test-token-amount-input]', invalidDai2); assert.dom('[data-test-token-amount-input]').hasValue('11'); assert.dom('[data-test-unlock-button]').isNotDisabled(); await fillIn('[data-test-token-amount-input]', ''); await typeIn('[data-test-token-amount-input]', invalidDai3); assert.dom('[data-test-token-amount-input]').hasValue('1'); assert.dom('[data-test-unlock-button]').isNotDisabled(); await fillIn('[data-test-token-amount-input]', ''); await typeIn('[data-test-token-amount-input]', invalidDai4); assert.dom('[data-test-token-amount-input]').hasValue('12'); assert.dom('[data-test-unlock-button]').isNotDisabled(); await fillIn('[data-test-token-amount-input]', startDaiString); assert.dom('[data-test-token-amount-input]').hasValue(startDaiString); assert.dom('[data-test-unlock-button]').isNotDisabled(); }); test('it rejects a value of zero', async function (assert) { await render(hbs` `); await fillIn('input', '0'); assert.dom('input').hasAria('invalid', 'true'); assert .dom('[data-test-boxel-input-error-message]') .containsText('Amount must be above 0.00 DAI'); }); module('it can resolve retries correctly', function (hooks) { let attempt1: RSVP.Deferred; let attempt2: RSVP.Deferred; let isComplete = false; let onComplete: Function; hooks.beforeEach(async function () { session.setValue('unlockTxnHash', 'unlockTokensTxnHash'); session.setValue('depositSourceToken', 'DAI'); session.setValue('depositedAmount', new BN(toWei('5'))); isComplete = false; onComplete = () => (isComplete = true); this.set('onComplete', onComplete); let relayTokensStub = sinon.stub(layer1Service, 'relayTokens'); let attempts = 0; // resolving attempt1 will cause the transaction hash to be resolved with // "attempt 1 hash", and attempt2 with "attempt 2 hash" attempt1 = defer(); attempt2 = defer(); relayTokensStub.callsFake(async function ( _token: string, _destinationAddress: string, _amountInWei: BN, options: RelayTokensOptions ) { attempts += 1; let currentAttempt = attempts; let hash: string; if (currentAttempt === 1) { hash = 'attempt 1 hash'; await attempt1.promise; } else if (currentAttempt === 2) { hash = 'attempt 2 hash'; await attempt2.promise; } options.onTxnHash?.(hash!); return { blockNumber: 0, } as TransactionReceipt; }); await render(hbs` `); await layer1Service.test__simulateUnlock(); await click('[data-test-deposit-button]'); await waitFor('[data-test-deposit-retry-button]'); await click('[data-test-deposit-retry-button]'); }); test('it resets if we reject both attempts', async function (assert) { attempt1.reject(); attempt2.reject(); await settled(); assert.notOk(isComplete, 'onComplete was not called'); assert .dom('[data-test-deposit-error-message]') .containsText( 'There was a problem initiating the bridging of your tokens to the L2 test chain. This may be due to a network issue, or perhaps you canceled the request in your wallet. Please try again if you want to continue with this workflow, or contact Cardstack support.' ); }); test('it completes successfully if we reject the first attempt and resolve the second', async function (assert) { attempt1.reject(); attempt2.resolve(); await settled(); assert.ok(isComplete, 'onComplete was called'); assert.ok( find('[data-test-deposit-etherscan-button]') ?.getAttribute('href') ?.includes('attempt 2 hash') ); }); test('it completes successfully if we resolve the first attempt and reject the second', async function (assert) { attempt1.resolve(); attempt2.reject(); await settled(); assert.ok(isComplete, 'onComplete was called'); assert.ok( find('[data-test-deposit-etherscan-button]') ?.getAttribute('href') ?.includes('attempt 1 hash') ); }); test('it completes successfully if we resolve both attempts', async function (assert) { attempt1.resolve(); attempt2.resolve(); await settled(); assert.ok(isComplete, 'onComplete was called'); assert.ok( find('[data-test-deposit-etherscan-button]') ?.getAttribute('href') ?.includes('attempt 1 hash') ); }); }); } );