diff --git a/packages/widget/package.json b/packages/widget/package.json index c1cfd32c..9140ed42 100644 --- a/packages/widget/package.json +++ b/packages/widget/package.json @@ -27,7 +27,7 @@ }, "author": "Sygmaprotocol Product Team", "dependencies": { - "@buildwithsygma/sygma-sdk-core": "^2.6.0", + "@buildwithsygma/sygma-sdk-core": "^2.6.2", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.2", @@ -36,6 +36,8 @@ "@lit/reactive-element": "^2.0.3", "@polkadot/api": "^10.11.2", "@polkadot/extension-dapp": "^0.46.6", + "@polkadot/keyring": "^12.6.2", + "@polkadot/util": "^12.6.2", "ethers": "5.7.2", "events": "^3.3.0", "lit": "3.0.0" @@ -45,7 +47,7 @@ "@open-wc/testing-helpers": "^3.0.0", "eslint": "^8.48.0", "eslint-plugin-lit": "^1.9.1", - "jsdom": "^23.2.0", + "happy-dom": "^13.3.1", "lit-analyzer": "^2.0.3", "rollup-plugin-visualizer": "^5.9.2", "typescript": "5.2.2", diff --git a/packages/widget/src/components/address-input/address-input.ts b/packages/widget/src/components/address-input/address-input.ts new file mode 100644 index 00000000..cc2a01a9 --- /dev/null +++ b/packages/widget/src/components/address-input/address-input.ts @@ -0,0 +1,80 @@ +import { LitElement, html } from 'lit'; +import type { HTMLTemplateResult } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { Network } from '@buildwithsygma/sygma-sdk-core'; +import { when } from 'lit/directives/when.js'; +import { validateAddress } from '../../utils'; +import { styles } from './styles'; + +@customElement('sygma-address-input') +export class AddressInput extends LitElement { + static styles = styles; + @property({ + type: String + }) + address: string = ''; + + @property({ attribute: false }) + onAddressChange: (address: string) => void = () => {}; + + @property({ + type: String + }) + network: Network = Network.EVM; + + @state() + errorMessage: string | null = null; + + connectedCallback(): void { + super.connectedCallback(); + this.handleAddressChange(this.address); + } + + private handleAddressChange = (value: string): void => { + const trimedValue = value.trim(); + + if (this.errorMessage) { + this.errorMessage = null; + } + + if (!trimedValue) { + return; + } + + this.errorMessage = validateAddress(trimedValue, this.network); + + if (!this.errorMessage) { + void this.onAddressChange(trimedValue); + } + }; + + render(): HTMLTemplateResult { + return html`
+
+ + +
+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'sygma-address-input': AddressInput; + } +} diff --git a/packages/widget/src/components/address-input/index.ts b/packages/widget/src/components/address-input/index.ts new file mode 100644 index 00000000..3f29279d --- /dev/null +++ b/packages/widget/src/components/address-input/index.ts @@ -0,0 +1 @@ +export { AddressInput } from './address-input'; diff --git a/packages/widget/src/components/address-input/styles.ts b/packages/widget/src/components/address-input/styles.ts new file mode 100644 index 00000000..f5778c64 --- /dev/null +++ b/packages/widget/src/components/address-input/styles.ts @@ -0,0 +1,51 @@ +import { css } from 'lit'; + +export const styles = css` + .inputAddressSection { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.5rem; + min-height: 7.75rem; // TOO: remove this hardcoded value + } + + .inputAddressContainer { + display: flex; + flex-direction: column; + width: 100%; + min-height: 7.75rem; // TOO: remove this hardcoded value + gap: 0.5rem; + } + + .inputAddress { + border-radius: 1.5rem; + border: 0.063rem solid var(--zinc-200); + font-size: 0.875rem; + text-align: center; + resize: none; + box-sizing: border-box; + overflow: hidden; + padding: 1rem; + } + + .inputAddress:focus { + outline: none; + border: 0.063rem solid var(--zinc-200); + } + + .error { + border-color: red; + } + + .errorMessage { + color: red; + font-weight: 300; + font-size: 0.75rem; + } + + .labelContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + } +`; diff --git a/packages/widget/src/components/amount-selector/styles.ts b/packages/widget/src/components/amount-selector/styles.ts index c893ab35..f60e2615 100644 --- a/packages/widget/src/components/amount-selector/styles.ts +++ b/packages/widget/src/components/amount-selector/styles.ts @@ -15,7 +15,7 @@ export const styles = css` display: flex; width: 100%; justify-content: flex-start; - color: #525252; + color: var(--neutral-600); font-size: 14px; font-weight: 500; line-height: 20px; /* 142.857% */ @@ -30,7 +30,7 @@ export const styles = css` .amountSelectorInput { border: none; - color: #525252; + color: var(--neutral-600); font-size: 34px; font-weight: 500; line-height: 40px; @@ -52,7 +52,7 @@ export const styles = css` } .maxButton { - color: #2563eb; + color: var(--blue-600); border: none; background: none; font-weight: 500; diff --git a/packages/widget/src/components/index.ts b/packages/widget/src/components/index.ts index 35b47082..67dd98b6 100644 --- a/packages/widget/src/components/index.ts +++ b/packages/widget/src/components/index.ts @@ -1,2 +1,3 @@ export { AmountSelector } from './amount-selector'; export { NetworkSelector } from './network-selector'; +export { AddressInput } from './address-input'; diff --git a/packages/widget/src/controllers/widget.ts b/packages/widget/src/controllers/widget.ts index ca9a119b..d13f82ff 100644 --- a/packages/widget/src/controllers/widget.ts +++ b/packages/widget/src/controllers/widget.ts @@ -18,6 +18,7 @@ export class WidgetController implements ReactiveController { public supportedSourceNetworks: Domain[] = []; public supportedDestinationNetworks: Domain[] = []; public supportedResources: Resource[] = []; + public destinatonAddress?: string = ''; //@ts-expect-error it will be used private assetTransfer?: EVMAssetTransfer | SubstrateAssetTransfer; @@ -126,4 +127,9 @@ export class WidgetController implements ReactiveController { console.log('resource amount', amount); this.resourceAmount = amount; }; + + onDestinationAddressChange = (address: string): void => { + console.log('destination address', address); + this.destinatonAddress = address; + }; } diff --git a/packages/widget/src/styles.ts b/packages/widget/src/styles.ts index 5b009932..9ee30549 100644 --- a/packages/widget/src/styles.ts +++ b/packages/widget/src/styles.ts @@ -1,6 +1,17 @@ import { css } from 'lit'; export const styles = css` + :host { + --zinc-200: #e4e4e7; + --zinc-400: #a1a1aa; + --white: #fff; + --gray-100: #f3f4f6; + --neutral-600: #525252; + --primary-300: #a5b4fc; + --primary-500: #6366f1; + --blue-600: #2563eb; + } + @font-face { font-family: 'Inter'; font-style: normal; @@ -15,11 +26,10 @@ export const styles = css` gap: 16px; padding: 24px; - width: 350px; /* TODO: remove these hardcoded values */ - height: 476px; /* TODO: ↑ */ + width: 21.875rem; /* TODO: remove these hardcoded values */ border-radius: 12px; - border: 1px solid #f3f4f6; - background-color: #fff; + border: 1px solid var(--gray-100); + background-color: var(--white); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), @@ -47,7 +57,7 @@ export const styles = css` border-radius: 16px; border: none; - background-color: #a5b4fc; + background-color: var(--primary-300); color: #ffffff; width: 314px; /* TODO: remove these hardcoded values */ @@ -62,7 +72,7 @@ export const styles = css` } .actionButton:active { - background-color: #6366f1; + background-color: var(--primary-500); } .actionButtonReady { @@ -75,13 +85,13 @@ export const styles = css` padding: 12px 20px; border-radius: 16px; - background-color: #6366f1; + background-color: var(--primary-500); color: #ffffff; border: none; } .actionButtonReady:active { - background-color: #a5b4fc; + background-color: var(--primary-300); } .actionButtonReady:hover { @@ -94,7 +104,7 @@ export const styles = css` gap: 6px; align-self: flex-start; - color: #525252; + color: var(--neutral-600); font-size: 12px; line-height: 150%; } diff --git a/packages/widget/src/utils/index.ts b/packages/widget/src/utils/index.ts index 84226984..887615e6 100644 --- a/packages/widget/src/utils/index.ts +++ b/packages/widget/src/utils/index.ts @@ -1,5 +1,9 @@ import type { HTMLTemplateResult } from 'lit'; import { html } from 'lit'; +import { decodeAddress, encodeAddress } from '@polkadot/keyring'; +import { hexToU8a, isHex } from '@polkadot/util'; +import { Network } from '@buildwithsygma/sygma-sdk-core'; +import { ethers } from 'ethers'; import { baseNetworkIcon, cronosNetworkIcon, @@ -46,3 +50,31 @@ export const capitalize = (s: string): string => { const rest = s.slice(1); return `${firstLetter}${rest}`; }; + +export const validateSubstrateAddress = (address: string): boolean => { + try { + encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)); + return true; + } catch (error) { + return false; + } +}; + +export const validateAddress = ( + address: string, + network: Network +): string | null => { + switch (network) { + case Network.SUBSTRATE: { + const validPolkadotAddress = validateSubstrateAddress(address); + return validPolkadotAddress ? null : 'invalid Substrate address'; + } + case Network.EVM: { + const isAddress = ethers.utils.isAddress(address); + + return isAddress ? null : 'invalid Ethereum address'; + } + default: + return 'unsupported network'; + } +}; diff --git a/packages/widget/src/widget.ts b/packages/widget/src/widget.ts index 7c481c84..be57cc16 100644 --- a/packages/widget/src/widget.ts +++ b/packages/widget/src/widget.ts @@ -6,6 +6,7 @@ import { switchNetworkIcon, sygmaLogo } from './assets'; import { WidgetController } from './controllers/widget'; import './components/network-selector'; import './components/amount-selector'; +import './components/address-input'; import { Directions } from './components/network-selector/network-selector'; @customElement('sygmaprotocol-widget') @@ -51,6 +52,14 @@ class SygmaProtocolWidget extends LitElement { > +
+ + +