| undefined
+
+ private copiedTooltipTimeout = 1500
+
+ disconnectedCallback() {
+ if (this.setTimeoutInstance) {
+ clearTimeout(this.setTimeoutInstance)
+ }
+ super.disconnectedCallback()
+ }
+
+ render() {
+ return html` ${this.label}
+ {
+ event.preventDefault()
+ this.dispatchEvent(
+ new CustomEvent('onLinkClick', {
+ bubbles: true,
+ composed: true,
+ detail: { type: 'account', data: this.address },
+ }),
+ )
+ }}
+ >
+ ${shortenAddress(this.address)}
+
+ `
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ width: 100%;
+ box-sizing: border-box;
+ justify-content: space-between;
+ margin-top: 0.5rem;
+ border-radius: 12px;
+ color: var(--color-light);
+ font-size: 14px;
+ height: 40px;
+ align-items: center;
+ padding: 0 20px;
+ }
+
+ .tooltip-wrapper {
+ all: unset;
+ display: inline-flex;
+ position: relative;
+ }
+
+ .tooltip-wrapper::after,
+ .tooltip-wrapper::before {
+ transition: opacity 0.1s ease-out 0.2s;
+ }
+
+ .tooltip-wrapper::after {
+ background: #000;
+ color: #fff;
+ border-radius: 8px;
+ content: attr(aria-label);
+ padding: 0.5rem 1rem;
+ position: absolute;
+ white-space: nowrap;
+ z-index: 10;
+ opacity: 0;
+ pointer-events: none;
+ transform: translate(-70%, -30%);
+ bottom: 100%;
+ }
+
+ .tooltip-wrapper::before {
+ content: '';
+ position: absolute;
+ z-index: 10;
+ opacity: 0;
+ pointer-events: none;
+ width: 0;
+ height: 0;
+ border: 8px solid transparent;
+ border-top-color: #000;
+ transform: translate(-15%, 25%);
+ bottom: 100%;
+ }
+
+ .tooltip-wrapper.tooltip-visible::after,
+ .tooltip-wrapper.tooltip-visible::before {
+ opacity: 1;
+ }
+
+ .label {
+ font-weight: 600;
+ color: var(--color-light);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 10px;
+ }
+
+ a {
+ color: var(--color-light);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ opacity: 0.8;
+ font-size: 12px;
+ }
+
+ i {
+ background-image: url(${unsafeCSS(CopyIcon)});
+ display: inline-block;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-position: center bottom;
+ width: 13px;
+ height: 13px;
+ }
+
+ .label,
+ a,
+ i {
+ text-shadow: 0px 4px 3px rgba(0, 0, 0, 0.08);
+ }
+
+ :host([appearanceId='0']) {
+ background: linear-gradient(276.58deg, #01e2a0 -0.6%, #052cc0 102.8%);
+ }
+
+ :host([appearanceId='1']) {
+ background: linear-gradient(
+ 276.33deg,
+ #ff43ca -14.55%,
+ #052cc0 102.71%
+ );
+ }
+
+ :host([appearanceId='2']) {
+ background: linear-gradient(
+ 276.33deg,
+ #20e4ff -14.55%,
+ #052cc0 102.71%
+ );
+ }
+
+ :host([appearanceId='3']) {
+ background: linear-gradient(94.8deg, #00ab84 -1.2%, #052cc0 103.67%);
+ }
+
+ :host([appearanceId='4']) {
+ background: linear-gradient(94.62deg, #ce0d98 -10.14%, #052cc0 104.1%);
+ }
+
+ :host([appearanceId='5']) {
+ background: linear-gradient(
+ 276.33deg,
+ #052cc0 -14.55%,
+ #0dcae4 102.71%
+ );
+ }
+
+ :host([appearanceId='6']) {
+ background: linear-gradient(90.89deg, #003057 -2.21%, #03d597 102.16%);
+ }
+
+ :host([appearanceId='7']) {
+ background: linear-gradient(276.23deg, #f31dbe -2.1%, #003057 102.67%);
+ }
+
+ :host([appearanceId='8']) {
+ background: linear-gradient(276.48deg, #003057 -0.14%, #052cc0 102.77%);
+ }
+
+ :host([appearanceId='9']) {
+ background: linear-gradient(276.32deg, #1af4b5 -5.15%, #0ba97d 102.7%);
+ }
+
+ :host([appearanceId='10']) {
+ background: linear-gradient(276.23deg, #e225b3 -2.1%, #7e0d5f 102.67%);
+ }
+
+ :host([appearanceId='11']) {
+ background: linear-gradient(276.48deg, #1f48e2 -0.14%, #040b72 102.77%);
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-account': RadixAccount
+ }
+}
diff --git a/packages/connect-button/src/components/button/button.stories.css b/packages/connect-button/src/components/button/button.stories.css
new file mode 100644
index 00000000..09e12a58
--- /dev/null
+++ b/packages/connect-button/src/components/button/button.stories.css
@@ -0,0 +1,17 @@
+.wrapper {
+ display: flex;
+}
+
+.column.dark {
+ background: #7a7676;
+}
+
+.row {
+ margin: 1rem;
+ white-space: nowrap;
+ display: flex;
+}
+
+radix-button {
+ margin-right: 0.3rem;
+}
diff --git a/packages/connect-button/src/components/button/button.stories.ts b/packages/connect-button/src/components/button/button.stories.ts
new file mode 100644
index 00000000..a4203268
--- /dev/null
+++ b/packages/connect-button/src/components/button/button.stories.ts
@@ -0,0 +1,169 @@
+import { Meta, StoryObj } from '@storybook/web-components'
+import { html } from 'lit-html'
+import './button.stories.css'
+import './button'
+import { BUTTON_MIN_HEIGHT, BUTTON_MIN_WIDTH } from '../../constants'
+import { RadixButtonStatus } from 'radix-connect-common'
+
+type Story = StoryObj
+
+const meta: Meta = {
+ title: 'Components / Button',
+ component: 'radix-button',
+}
+export default meta
+
+type Args = (typeof Themes)['args']
+
+const createButton = (args: any) => html`
+ ${args.text}
+`
+
+const createRow = (args: Args) => {
+ const borderRadius = [0, 4, 12, 50]
+ return new Array(4).fill(null).map(
+ (_, index) =>
+ html`
+
+
+ ${createButton({
+ ...args,
+ borderRadius: borderRadius[index],
+ })}
+ ${createButton({
+ ...args,
+ borderRadius: borderRadius[index],
+ })}
+
`,
+ )
+}
+
+export const Primary: Story = {
+ render: (args) =>
+ html`
+ ${args.connected ? args.text : 'Connect'}
`,
+ argTypes: {
+ theme: {
+ options: ['radix-blue', 'black', 'white-with-outline', 'white'],
+ control: 'select',
+ },
+ text: {
+ control: 'text',
+ },
+ status: {
+ options: [
+ RadixButtonStatus.default,
+ RadixButtonStatus.pending,
+ RadixButtonStatus.success,
+ RadixButtonStatus.error,
+ ],
+ control: 'select',
+ },
+ },
+ args: {
+ width: BUTTON_MIN_WIDTH,
+ height: BUTTON_MIN_HEIGHT,
+ borderRadius: 0,
+ theme: 'radix-blue',
+ connected: true,
+ text: 'Matthew Hine',
+ fullWidth: false,
+ status: RadixButtonStatus.default,
+ },
+}
+
+export const Themes: Story = {
+ render: (args) =>
+ html`
+
+
+ ${createRow({ ...args, theme: 'radix-blue', connected: false })}
+
+
+ ${createRow({ ...args, theme: 'black', connected: false })}
+
+
+ ${createRow({
+ ...args,
+ theme: 'white-with-outline',
+ connected: false,
+ })}
+
+
+ ${createRow({ ...args, theme: 'white', connected: false })}
+
+
+ ${createRow({
+ ...args,
+ connected: true,
+ text: 'Matthew Hine',
+ })}
+
+
`,
+ argTypes: {
+ theme: {
+ options: ['radix-blue', 'black', 'white-with-outline', 'white'],
+ control: 'select',
+ },
+ status: {
+ options: [
+ RadixButtonStatus.default,
+ RadixButtonStatus.pending,
+ RadixButtonStatus.success,
+ RadixButtonStatus.error,
+ ],
+ control: 'select',
+ },
+ text: {
+ control: 'text',
+ },
+ },
+ args: {
+ text: 'Connect',
+ status: RadixButtonStatus.default,
+ },
+}
diff --git a/packages/connect-button/src/components/button/button.ts b/packages/connect-button/src/components/button/button.ts
new file mode 100644
index 00000000..10fae7a6
--- /dev/null
+++ b/packages/connect-button/src/components/button/button.ts
@@ -0,0 +1,290 @@
+import { LitElement, css, html, unsafeCSS } from 'lit'
+import { classMap } from 'lit/directives/class-map.js'
+import { customElement, property } from 'lit/decorators.js'
+import {
+ LoadingSpinner,
+ loadingSpinnerCSS,
+} from '../loading-spinner/loading-spinner'
+import { themeCSS } from '../../theme'
+import logo from '../../assets/logo.svg'
+import Gradient from '../../assets/gradient.svg'
+import CompactGradient from '../../assets/compact-gradient.svg'
+import AvatarPlaceholder from '../../assets/button-avatar-placeholder.svg'
+import SuccessIcon from '../../assets/success.svg'
+import ErrorIcon from '../../assets/error.svg'
+import { RadixButtonStatus, RadixButtonTheme } from 'radix-connect-common'
+import {
+ BUTTON_COMPACT_MIN_WIDTH,
+ BUTTON_MIN_HEIGHT,
+ BUTTON_MIN_WIDTH,
+} from '../../constants'
+
+@customElement('radix-button')
+export class RadixButton extends LitElement {
+ @property({
+ type: String,
+ reflect: true,
+ })
+ status: RadixButtonStatus = RadixButtonStatus.default
+
+ @property({
+ type: Boolean,
+ })
+ connected = false
+
+ @property({
+ type: Boolean,
+ reflect: true,
+ })
+ fullWidth = false
+
+ @property({
+ type: String,
+ reflect: true,
+ })
+ theme: RadixButtonTheme = 'radix-blue'
+
+ private onClick(event: MouseEvent) {
+ this.dispatchEvent(
+ new CustomEvent('onClick', {
+ detail: event,
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ private resizeObserver: undefined | ResizeObserver
+
+ connectedCallback() {
+ super.connectedCallback()
+
+ setTimeout(() => {
+ const button = this.shadowRoot!.querySelector('button')!
+
+ this.resizeObserver = new ResizeObserver(() => {
+ this.dispatchEvent(
+ new CustomEvent('onResize', {
+ bubbles: true,
+ composed: false,
+ detail: button,
+ }),
+ )
+ })
+
+ this.resizeObserver.observe(button)
+ })
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback()
+ const button = this.shadowRoot!.querySelector('button')!
+ this.resizeObserver?.unobserve(button)
+ }
+
+ render() {
+ const renderContent = () => {
+ if (this.status === RadixButtonStatus.pending && this.connected) {
+ return html`${LoadingSpinner} `
+ } else if (this.status === RadixButtonStatus.pending) {
+ return LoadingSpinner
+ } else if (!this.connected && ['success', 'error'].includes(this.status))
+ return ''
+
+ return html``
+ }
+
+ const showLogo = this.status !== 'pending' && !this.connected
+ const showGradient = this.connected
+
+ return html`
+
+ `
+ }
+
+ static styles = [
+ themeCSS,
+ loadingSpinnerCSS,
+ css`
+ :host {
+ width: max(var(--radix-connect-button-width, 138px), 40px);
+ min-width: 40px;
+ display: flex;
+ justify-content: flex-end;
+ container-type: inline-size;
+ user-select: none;
+ --radix-connect-button-text-color: var(--color-light);
+ }
+
+ :host([full-width]) > button {
+ width: 100%;
+ }
+
+ :host([full-width]) {
+ width: 100%;
+ display: inline-block;
+ }
+
+ ::slotted(*) {
+ overflow: hidden;
+ display: block;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ text-align: left;
+ width: auto;
+ }
+
+ .gradient ::slotted(*) {
+ padding: 0 4px;
+ }
+
+ button {
+ width: max(var(--radix-connect-button-width, 138px), 40px);
+ height: var(--radix-connect-button-height, 40px);
+ min-width: ${BUTTON_COMPACT_MIN_WIDTH}px;
+ min-height: ${BUTTON_MIN_HEIGHT}px;
+ border-radius: var(--radix-connect-button-border-radius, 0);
+ background-color: var(--radix-connect-button-background);
+ border: 1px solid var(--radix-connect-button-border-color);
+ color: var(--radix-connect-button-text-color);
+ font-size: 14px;
+ align-content: center;
+ align-items: center;
+ font-family: inherit;
+ cursor: pointer;
+ font-weight: 600;
+ transition: background-color 0.1s cubic-bezier(0.45, 0, 0.55, 1);
+
+ display: flex;
+ gap: 3px;
+ justify-content: center;
+ padding: 0 10px;
+ }
+
+ button::before {
+ min-height: 0.94em;
+ min-width: 1.25em;
+ display: block;
+ -webkit-mask-position: center right;
+ mask-position: center right;
+ mask-repeat: no-repeat;
+ -webkit-mask-repeat: no-repeat;
+ background-color: var(--radix-connect-button-text-color);
+ width: 16px;
+ }
+
+ button:hover {
+ background-color: var(--radix-connect-button-background-hover);
+ }
+
+ button.logo::before {
+ content: '';
+ mask-image: url(${unsafeCSS(logo)});
+ -webkit-mask-image: url(${unsafeCSS(logo)});
+ }
+
+ button.gradient.logo::before {
+ background-color: var(--color-light);
+ }
+
+ :host([status='pending']) > button.gradient::before {
+ display: none;
+ }
+
+ button.gradient {
+ border: 1px solid transparent;
+ background-repeat: no-repeat;
+ background-origin: border-box;
+ background-size: cover;
+ background-position: center;
+ background-color: var(--color-radix-blue-2);
+ color: var(--color-light);
+ background-image: url(${unsafeCSS(Gradient)});
+ padding-right: 7px;
+ }
+
+ button.gradient::before {
+ content: '';
+ background-color: var(--color-light);
+ }
+
+ :host([status='default']) > button.gradient::before {
+ mask-image: url(${unsafeCSS(AvatarPlaceholder)});
+ -webkit-mask-image: url(${unsafeCSS(AvatarPlaceholder)});
+ width: 22px;
+ min-width: 22px;
+ height: 22px;
+ -webkit-mask-position: center;
+ mask-position: center;
+ }
+
+ :host([status='success']) > button::before {
+ mask-image: url(${unsafeCSS(SuccessIcon)});
+ -webkit-mask-image: url(${unsafeCSS(SuccessIcon)});
+ width: 22px;
+ min-width: 22px;
+ height: 22px;
+ -webkit-mask-position: center;
+ mask-position: center;
+ }
+
+ :host([status='error']) > button::before {
+ mask-image: url(${unsafeCSS(ErrorIcon)});
+ -webkit-mask-image: url(${unsafeCSS(ErrorIcon)});
+ width: 22px;
+ min-width: 22px;
+ height: 22px;
+ -webkit-mask-position: center;
+ mask-position: center;
+ }
+
+ button.gradient:hover {
+ background-color: var(--color-radix-blue-1);
+ }
+
+ button:focus,
+ button:focus-visible {
+ outline: 0px auto -webkit-focus-ring-color;
+ }
+
+ @container (width < ${BUTTON_MIN_WIDTH - 0.1}px) {
+ button {
+ width: var(--radix-connect-button-height, 40px);
+ max-width: ${BUTTON_MIN_WIDTH}px;
+ max-height: ${BUTTON_MIN_WIDTH}px;
+ justify-content: center;
+ padding: 0;
+ }
+ button::before {
+ -webkit-mask-position: center;
+ mask-position: center;
+ }
+ button.gradient {
+ background-image: url(${unsafeCSS(CompactGradient)});
+ padding: 0;
+ }
+ button.logo::before {
+ font-size: 16px;
+ }
+ ::slotted(*) {
+ display: none;
+ }
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-button': RadixButton
+ }
+}
diff --git a/packages/connect-button/src/components/card/card.stories.ts b/packages/connect-button/src/components/card/card.stories.ts
new file mode 100644
index 00000000..d2aa767f
--- /dev/null
+++ b/packages/connect-button/src/components/card/card.stories.ts
@@ -0,0 +1,127 @@
+import { Meta, StoryObj } from '@storybook/web-components'
+import './card'
+import './persona-card'
+import './request-card'
+import '../popover/popover'
+import { html } from 'lit'
+import '../account/account'
+
+const meta: Meta = {
+ title: 'Components / Cards',
+}
+export default meta
+
+type Story = StoryObj
+
+export const Onboarding: Story = {
+ render: (args) => html`
+
+
+
+
+ Get and link the Radix Connector browser extension to your Radix
+ Wallet to connect securely to dApp websites.
+
+
+ `,
+ argTypes: {},
+ args: {
+ icon: 'unchecked',
+ },
+}
+
+const personaData = [
+ 'Alex Stelea',
+ 'alex.stelea@van.damme',
+ '+42084652103',
+ '45 Avebury Drive, Duncan Road, Elshewhere, NY 98827',
+ 'Passport: 78668279872HS',
+ 'Lorem ipsum',
+ 'dolor sit amet',
+ 'consectetur adipiscing elit',
+ 'Aenean',
+ 'ultrices sodales',
+ 'ex, vitae fringilla',
+]
+
+export const Sharing: Story = {
+ render: (args) => html`
+
+
+
+ `,
+ args: {
+ avatarUrl: 'https://picsum.photos/200',
+ personaDataRows: 2,
+ persona: 'VanDammeStelea',
+ },
+}
+
+export const Requests: Story = {
+ render: (args) => html`
+
+ {
+ console.log(event)
+ }}
+ ?showCancel="${args.showCancel}"
+ transactionIntentHash="${args.transactionIntentHash}"
+ >
+
+ `,
+ argTypes: {
+ type: {
+ options: ['dataRequest', 'loginRequest', 'sendTransaction'],
+ control: 'select',
+ },
+ status: {
+ options: ['pending', 'success', 'fail', 'cancelled'],
+ control: 'select',
+ },
+ timestamp: {
+ control: 'text',
+ },
+ transactionIntentHash: {
+ control: 'text',
+ },
+ showCancel: {
+ control: 'boolean',
+ },
+ },
+ args: {
+ id: 'abcdefg',
+ type: 'loginRequest',
+ status: 'pending',
+ showCancel: true,
+ timestamp: '123536564234',
+ transactionIntentHash: '2343652434',
+ },
+}
diff --git a/packages/connect-button/src/components/card/card.ts b/packages/connect-button/src/components/card/card.ts
new file mode 100644
index 00000000..9eb1f1af
--- /dev/null
+++ b/packages/connect-button/src/components/card/card.ts
@@ -0,0 +1,144 @@
+import { html, css, LitElement, unsafeCSS } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { styleMap } from 'lit/directives/style-map.js'
+import UncheckedIcon from '../../assets/unchecked.svg'
+import CheckedIcon from '../../assets/checked.svg'
+import IconLoading from '../../assets/icon-loading.svg'
+import IconFailed from '../../assets/icon-failed.svg'
+import { formatTimestamp } from '../../helpers/format-timestamp'
+
+@customElement('radix-card')
+export class RadixCard extends LitElement {
+ @property({
+ type: String,
+ reflect: true,
+ })
+ icon?: 'unchecked' | 'checked' | 'pending' | 'success' | 'error'
+
+ @property({
+ type: String,
+ })
+ header: string = ''
+
+ @property({
+ type: String,
+ reflect: true,
+ })
+ timestamp?: string
+
+ render() {
+ const renderDate = () =>
+ this.timestamp
+ ? html`${formatTimestamp(this.timestamp)}
`
+ : ''
+
+ const gridTemplateColumns = `${this.icon ? '30px' : ''} 1fr ${
+ this.timestamp ? '42px' : ''
+ }`
+
+ return html`
+
+
+ ${this.header}
+
+
+ ${renderDate()}
+
`
+ }
+
+ static styles = [
+ css`
+ :host {
+ background-color: var(--radix-card-background);
+ color: var(--radix-card-text-color);
+ display: block;
+ padding: 11px 20px;
+ user-select: none;
+ border-radius: 12px;
+ width: 100%;
+ box-sizing: border-box;
+ }
+
+ :host(.inverted) {
+ background-color: var(--radix-card-inverted-background);
+ color: var(--radix-card-inverted-text-color);
+ }
+
+ :host(.inverted) .card i::before {
+ background-color: var(--radix-card-inverted-text-color);
+ }
+
+ :host(.dimmed) .card i::before {
+ background-color: var(--radix-card-text-dimmed-color);
+ }
+
+ :host(.dimmed) .content {
+ color: var(--radix-card-text-dimmed-color);
+ }
+
+ .timestamp {
+ text-align: right;
+ color: var(--radix-card-text-dimmed-color);
+ font-size: 12px;
+ }
+
+ .card {
+ display: grid;
+ align-items: center;
+ column-gap: 10px;
+ }
+
+ i::before {
+ content: '';
+ display: block;
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ background-color: var(--radix-card-text-color);
+ }
+
+ span {
+ display: block;
+ font-weight: 600;
+ font-size: 14px;
+ }
+
+ p {
+ margin: 0;
+ }
+
+ :host([icon='unchecked']) i::before {
+ -webkit-mask-image: url(${unsafeCSS(UncheckedIcon)});
+ mask-image: url(${unsafeCSS(UncheckedIcon)});
+ width: 24px;
+ height: 24px;
+ }
+
+ :host([icon='pending']) i::before {
+ -webkit-mask-image: url(${unsafeCSS(IconLoading)});
+ mask-image: url(${unsafeCSS(IconLoading)});
+ width: 24px;
+ height: 24px;
+ }
+
+ :host([icon='checked']) i::before {
+ -webkit-mask-image: url(${unsafeCSS(CheckedIcon)});
+ mask-image: url(${unsafeCSS(CheckedIcon)});
+ width: 24px;
+ height: 24px;
+ }
+
+ :host([icon='error']) i::before {
+ -webkit-mask-image: url(${unsafeCSS(IconFailed)});
+ mask-image: url(${unsafeCSS(IconFailed)});
+ width: 24px;
+ height: 24px;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-card': RadixCard
+ }
+}
diff --git a/packages/connect-button/src/components/card/persona-card.ts b/packages/connect-button/src/components/card/persona-card.ts
new file mode 100644
index 00000000..3eaba171
--- /dev/null
+++ b/packages/connect-button/src/components/card/persona-card.ts
@@ -0,0 +1,123 @@
+import { html, css, LitElement, unsafeCSS } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { styleMap } from 'lit/directives/style-map.js'
+import { classMap } from 'lit/directives/class-map.js'
+import './card'
+import '../account/account'
+import AvatarPlaceholder from '../../assets/avatar-placeholder.svg'
+
+@customElement('radix-persona-card')
+export class RadixPersonaCard extends LitElement {
+ @property({
+ type: String,
+ reflect: true,
+ })
+ icon?: 'unchecked' | 'checked' | 'pending' | 'success' | 'error'
+
+ @property({
+ type: String,
+ })
+ persona: string = ''
+
+ @property({
+ type: String,
+ })
+ avatarUrl?: string
+
+ @property({
+ type: Array,
+ })
+ personaData: string[] = []
+
+ render() {
+ return html`
+
+
+
+
${this.persona}
+
+ ${(this.personaData || []).map((item) => html`- ${item}
`)}
+
+
+
`
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ width: 100%;
+ }
+
+ .avatar {
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center;
+ border-radius: 50%;
+ width: 60px;
+ height: 60px;
+ align-self: center;
+ border: 2px solid var(--radix-avatar-border-color);
+ }
+
+ .placeholder {
+ width: 64px;
+ height: 64px;
+ background-image: url(${unsafeCSS(AvatarPlaceholder)});
+ }
+
+ .persona-card {
+ display: grid;
+ gap: 20px;
+ align-items: flex-start;
+ grid-template-columns: 1fr 230px;
+ }
+
+ .persona-card.center {
+ align-items: center;
+ }
+
+ .persona {
+ font-size: 14px;
+ font-weight: 600;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ display: block;
+ white-space: nowrap;
+ }
+
+ ul {
+ margin-top: 5px;
+ margin-bottom: 0;
+ padding-inline-start: 20px;
+ }
+
+ li {
+ font-size: 12px;
+ word-break: break-word;
+ line-height: 18px;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-persona-card': RadixPersonaCard
+ }
+}
diff --git a/packages/connect-button/src/components/card/request-card.ts b/packages/connect-button/src/components/card/request-card.ts
new file mode 100644
index 00000000..1f0e56bc
--- /dev/null
+++ b/packages/connect-button/src/components/card/request-card.ts
@@ -0,0 +1,191 @@
+import { html, css, LitElement } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import './card'
+import '../account/account'
+import '../link/link'
+import { shortenAddress } from '../../helpers/shorten-address'
+import { classMap } from 'lit/directives/class-map.js'
+import { RequestItemType, RequestStatus } from 'radix-connect-common'
+@customElement('radix-request-card')
+export class RadixRequestCard extends LitElement {
+ @property({
+ type: String,
+ })
+ type: keyof typeof RequestItemType = 'dataRequest'
+
+ @property({
+ type: String,
+ })
+ status: keyof typeof RequestStatus = 'pending'
+
+ @property({
+ type: Boolean,
+ })
+ showCancel: boolean = false
+
+ @property({
+ type: String,
+ })
+ timestamp: string = ''
+
+ @property({
+ type: String,
+ })
+ id: string = ''
+
+ @property({
+ type: String,
+ })
+ transactionIntentHash: string = ''
+
+ render() {
+ const icon = this.getIconFromStatus()
+ const styling = this.getStylingFromStatus()
+ const texts = {
+ sendTransaction: {
+ pending: 'Pending Transaction',
+ fail: 'Transaction Failed',
+ cancelled: 'Transaction Cancelled',
+ success: 'Send transaction',
+ content: html`
+ ${this.renderTxIntentHash()}
+ ${this.getRequestContentTemplate(
+ 'Open your Radix Wallet app to review the transaction',
+ )}
+ `,
+ },
+ dataRequest: {
+ pending: 'Data Request Pending',
+ fail: 'Data Request Rejected',
+ cancelled: 'Data Request Rejected',
+ success: 'Data Request',
+ content: this.getRequestContentTemplate(
+ 'Open Your Radix Wallet App to complete the request',
+ ),
+ },
+ loginRequest: {
+ pending: 'Login Request Pending',
+ fail: 'Login Request Rejected',
+ cancelled: 'Login Request Rejected',
+ success: 'Login Request',
+ content: this.getRequestContentTemplate(
+ 'Open Your Radix Wallet App to complete the request',
+ ),
+ },
+ }
+
+ return html`
+ ${texts[this.type].content}
+ `
+ }
+
+ private getRequestContentTemplate(text: string) {
+ return this.status === 'pending'
+ ? html`
+ ${text}
+ ${this.showCancel
+ ? html`
Cancel
`
+ : ``}
+
`
+ : ''
+ }
+
+ private getIconFromStatus() {
+ return this.status === 'pending'
+ ? 'pending'
+ : this.status === 'cancelled' || this.status === 'fail'
+ ? 'error'
+ : 'checked'
+ }
+
+ private getStylingFromStatus() {
+ return classMap({
+ dimmed: this.status === 'fail' || this.status === 'cancelled',
+ inverted: this.status === 'pending',
+ })
+ }
+
+ private onCancel(event: MouseEvent) {
+ this.dispatchEvent(
+ new CustomEvent('onCancelRequestItem', {
+ detail: {
+ ...event,
+ id: this.id,
+ },
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ private renderTxIntentHash() {
+ return this.transactionIntentHash
+ ? html`
+ ID:
+ {
+ event.preventDefault()
+ this.dispatchEvent(
+ new CustomEvent('onLinkClick', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ type: 'transaction',
+ data: this.transactionIntentHash,
+ },
+ }),
+ )
+ }}
+ >
+
`
+ : ''
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: flex;
+ width: 100%;
+ margin-bottom: 10px;
+ }
+
+ .text-dimmed {
+ color: var(--radix-card-text-dimmed-color);
+ margin-right: 5px;
+ }
+
+ .transaction {
+ font-weight: 600;
+ text-decoration: none;
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ }
+
+ .cancel {
+ cursor: pointer;
+ text-decoration: underline;
+ }
+
+ .request-content {
+ margin-top: 5px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ font-size: 14px;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-request-card': RadixRequestCard
+ }
+}
diff --git a/packages/connect-button/src/components/card/request-cards.ts b/packages/connect-button/src/components/card/request-cards.ts
new file mode 100644
index 00000000..f1bd9ba7
--- /dev/null
+++ b/packages/connect-button/src/components/card/request-cards.ts
@@ -0,0 +1,41 @@
+import { html, LitElement } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { themeCSS } from '../../theme'
+import '../card/request-card'
+import { RequestItem } from 'radix-connect-common'
+
+@customElement('radix-request-cards')
+export class RadixRequestCards extends LitElement {
+ @property({ type: Array })
+ requestItems: RequestItem[] = []
+
+ render() {
+ return (this.requestItems || []).map(
+ (requestItem) =>
+ html` {
+ this.dispatchEvent(
+ new CustomEvent('onCancel', {
+ detail: event.detail,
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }}
+ timestamp=${requestItem.createdAt}
+ >`,
+ )
+ }
+
+ static styles = [themeCSS]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-request-cards': RadixRequestCards
+ }
+}
diff --git a/packages/connect-button/src/components/connect-button.stories.css b/packages/connect-button/src/components/connect-button.stories.css
new file mode 100644
index 00000000..bc373702
--- /dev/null
+++ b/packages/connect-button/src/components/connect-button.stories.css
@@ -0,0 +1,5 @@
+.connect-button-wrapper {
+ display: flex;
+ justify-content: end;
+ text-align: center;
+}
diff --git a/packages/connect-button/src/components/connect-button.stories.ts b/packages/connect-button/src/components/connect-button.stories.ts
new file mode 100644
index 00000000..6187998f
--- /dev/null
+++ b/packages/connect-button/src/components/connect-button.stories.ts
@@ -0,0 +1,416 @@
+import { Story, Meta } from '@storybook/web-components'
+import { html } from 'lit-html'
+import {
+ Account,
+ PersonaData,
+ RadixButtonStatus,
+ RequestItem,
+} from 'radix-connect-common'
+import './connect-button'
+import { ConnectButton } from './connect-button'
+import './connect-button.stories.css'
+import { BUTTON_MIN_WIDTH } from '../constants'
+
+export default {
+ title: 'Radix/Connect button states',
+} as Meta
+
+const getConnectButton: () => ConnectButton = () =>
+ document.querySelector('radix-connect-button')!
+
+const argTypes = {
+ status: {
+ options: [
+ RadixButtonStatus.default,
+ RadixButtonStatus.error,
+ RadixButtonStatus.pending,
+ RadixButtonStatus.success,
+ ],
+ control: 'select',
+ },
+ activeTab: {
+ options: ['sharing', 'requests'],
+ control: 'select',
+ },
+ isMobile: {
+ control: 'boolean',
+ },
+}
+
+const defaultArgs = {
+ render: true,
+ borderRadius: 10,
+ showPopoverMenu: true,
+ width: BUTTON_MIN_WIDTH,
+ height: 40,
+}
+
+const Button = (args: any, { globals }: any) => {
+ if (args.render)
+ return html`
+
+
+
+
+ ) => {
+ const connectButton = getConnectButton()
+ console.log('cancelled', event.detail.id)
+ connectButton.requestItems = connectButton.requestItems.map(
+ (item: RequestItem) =>
+ item.interactionId === event.detail.id
+ ? { ...item, status: 'fail', error: 'canceledByUser' }
+ : item,
+ )
+ }}
+ @onDisconnect=${() => {
+ getConnectButton().connected = false
+ getConnectButton().requestItems = []
+ }}
+ @onDestroy=${() => {}}
+ @onConnect=${() => {
+ getConnectButton().status = RadixButtonStatus.pending
+ getConnectButton().requestItems = [
+ {
+ interactionId: crypto.randomUUID(),
+ type: 'loginRequest',
+ status: 'pending',
+ createdAt: 326575486756987,
+ showCancel: true,
+ walletInteraction: {},
+ },
+ ]
+ }}
+ @onShowPopover=${() => {
+ console.log('onShowPopover')
+ }}
+ >
+
+
+ `
+ return ''
+}
+
+const Template: Story> = (args, context) => Button(args, context)
+
+const accounts: Account[] = [
+ {
+ label: 'My Main Account',
+ address:
+ 'account_tdx_b_1qlxj68pketfcx8a6wrrqyvjfzdr7caw08j22gm6d26hq3g6x5m',
+ appearanceId: 1,
+ },
+ {
+ label: 'Second Account',
+ address:
+ 'account_tdx_b_1queslxclg3ya6tyqqgs2ase7wgst2cvpwqq96ptkqj6qaefsgy',
+ appearanceId: 2,
+ },
+ {
+ label: 'Holiday Funds',
+ address:
+ 'account_tdx_b_1queslxclg3ya6tyqqgs2ase7wgst2cvpwqq96ptkqj6qaefsgy',
+ appearanceId: 3,
+ },
+ {
+ label: 'Private Savings',
+ address:
+ 'account_tdx_b_1queslxclg3ya6tyqqgs2ase7wgst2cvpwqq96ptkqj6qaefsgy',
+ appearanceId: 4,
+ },
+]
+
+const personaData: PersonaData[] = [
+ {
+ field: 'givenName',
+ value: 'Matthew',
+ },
+ {
+ field: 'familyName',
+ value: 'Hines',
+ },
+ {
+ field: 'emailAddress',
+ value: 'matt@radmatt.io',
+ },
+ {
+ field: 'phoneNumber',
+ value: '123 123 1234',
+ },
+]
+
+export const connectorExtensionNotInstalled = Template.bind({})
+connectorExtensionNotInstalled.args = {
+ ...defaultArgs,
+ isExtensionAvailable: false,
+}
+
+export const connectorExtensionNotLinked = Template.bind({})
+connectorExtensionNotLinked.args = {
+ ...defaultArgs,
+ isExtensionAvailable: true,
+ isWalletLinked: false,
+}
+
+export const readyToConnect = Template.bind({})
+readyToConnect.args = {
+ ...defaultArgs,
+ isWalletLinked: true,
+ isExtensionAvailable: true,
+}
+
+export const connectingInProgress = Template.bind({})
+// More on args: https://storybook.js.org/docs/web-components/writing-stories/args
+connectingInProgress.args = {
+ ...defaultArgs,
+ status: RadixButtonStatus.pending,
+ isMobile: false,
+ isWalletLinked: false,
+ isExtensionAvailable: false,
+ connected: false,
+ requestItems: [
+ {
+ id: crypto.randomUUID(),
+ type: 'loginRequest',
+ status: 'pending',
+ showCancel: true,
+ timestamp: 1690554318703,
+ },
+ ],
+ accounts,
+ personaLabel: 'RadMatt',
+ render: true,
+}
+connectingInProgress.argTypes = argTypes
+
+export const mobileView = Template.bind({})
+mobileView.args = {
+ ...defaultArgs,
+ width: 120,
+ isMobile: true,
+}
+mobileView.argTypes = argTypes
+
+export const sharing = Template.bind({})
+sharing.args = {
+ ...defaultArgs,
+ connected: true,
+ dAppName: 'Radix Dashboard',
+ status: RadixButtonStatus.default,
+ activeTab: 'sharing',
+ personaLabel: 'VanDammeStelea',
+ avatarUrl:
+ 'https://e7.pngegg.com/pngimages/799/987/png-clipart-computer-icons-avatar-icon-design-avatar-heroes-computer-wallpaper-thumbnail.png',
+}
+sharing.argTypes = argTypes
+
+export const sharing2 = Template.bind({})
+sharing2.args = {
+ ...sharing.args,
+ personaData: [
+ {
+ field: '',
+ value: 'Alex Stelea',
+ },
+ ],
+ accounts: [
+ {
+ label: 'My Main Account',
+ address:
+ 'account_tdx_b_1qlxj68pketfcx8a6wrrqyvjfzdr7caw08j22gm6d26hq3g6x5m',
+ appearanceId: 1,
+ },
+ ],
+}
+sharing2.argTypes = argTypes
+
+export const sharing3 = Template.bind({})
+sharing3.args = {
+ ...sharing2.args,
+ personaData: [
+ {
+ field: '',
+ value: 'Alex Stelea',
+ },
+ {
+ field: 'mail',
+ value: 'alex.stelea@van.damme',
+ },
+ {
+ field: 'phone',
+ value: '+42084652103',
+ },
+ ],
+}
+sharing3.argTypes = argTypes
+
+export const sharing4 = Template.bind({})
+sharing4.args = {
+ ...sharing3.args,
+ accounts: [
+ ...sharing3.args.accounts,
+ {
+ label: 'Second Account',
+ address: 'account_tdx_b_1qlxj68pketfcx8a6wrrqyvjfzdr7caw08j22gm6d26hq',
+ appearanceId: 2,
+ },
+ {
+ label: 'Holiday Funds',
+ address: 'account_tdx_b_1qlxj68pketfcx8a6wrrqyvjfzdr7ca',
+ appearanceId: 3,
+ },
+ ],
+}
+sharing4.argTypes = argTypes
+
+export const sharing5 = Template.bind({})
+sharing5.args = {
+ ...sharing4.args,
+ personaData: [
+ ...sharing4.args.personaData,
+ {
+ field: 'address',
+ value: `
+45 Avebury Drive, Duncan, Elshewhere, NY 98827
+`,
+ },
+ { field: 'passport', value: 'Passport: 78668279872HS' },
+ ],
+}
+sharing5.argTypes = argTypes
+
+export const requests = Template.bind({})
+requests.args = {
+ ...defaultArgs,
+ connected: true,
+ personaLabel: 'RadMatt',
+ dAppName: 'Radix Dashboard',
+ loggedInTimestamp: '3256345786456',
+ status: RadixButtonStatus.default,
+ activeTab: 'requests',
+ requestItems: [
+ {
+ id: crypto.randomUUID(),
+ type: 'dataRequest',
+ status: 'pending',
+ timestamp: 1690154318703,
+ showCancel: true,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'sendTransaction',
+ status: 'success',
+ transactionIntentHash: 'transaction_1342a4b43e8f9fde27ef9284',
+ timestamp: 169032318703,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'dataRequest',
+ status: 'fail',
+ timestamp: 1690254318703,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'loginRequest',
+ status: 'cancelled',
+ timestamp: 1690454318703,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'dataRequest',
+ status: 'success',
+ timestamp: 1690454318703,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'sendTransaction',
+ status: 'success',
+ transactionIntentHash: 'transaction_1342a4b43e8f9fde27ef9284',
+ timestamp: 169032318703,
+ },
+ ],
+}
+requests.argTypes = argTypes
+
+export const connected = Template.bind({})
+// More on args: https://storybook.js.org/docs/web-components/writing-stories/args
+connected.args = {
+ width: BUTTON_MIN_WIDTH,
+ height: 40,
+ borderRadius: 20,
+ dAppName: 'Radix Dashboard',
+ activeTab: 'sharing',
+ showPopoverMenu: true,
+ loggedInTimestamp: Date.now(),
+ status: RadixButtonStatus.success,
+ connected: true,
+ avatarUrl:
+ 'https://e7.pngegg.com/pngimages/799/987/png-clipart-computer-icons-avatar-icon-design-avatar-heroes-computer-wallpaper-thumbnail.png',
+ requestItems: [
+ {
+ id: crypto.randomUUID(),
+ type: 'sendTransaction',
+ status: 'pending',
+ timestamp: Date.now(),
+ showCancel: true,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'loginRequest',
+ status: 'success',
+ timestamp: 169032318703,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'sendTransaction',
+ status: 'fail',
+ timestamp: 1690254318703,
+ },
+ {
+ id: crypto.randomUUID(),
+ type: 'loginRequest',
+ status: 'cancelled',
+ timestamp: 1690454318703,
+ },
+ ],
+ accounts,
+ personaData,
+ personaLabel: 'RadMatt',
+ render: true,
+}
+connected.argTypes = argTypes
diff --git a/packages/connect-button/src/components/connect-button.ts b/packages/connect-button/src/components/connect-button.ts
new file mode 100644
index 00000000..012e0824
--- /dev/null
+++ b/packages/connect-button/src/components/connect-button.ts
@@ -0,0 +1,357 @@
+import { LitElement, css, html } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import './popover/popover'
+import './button/button'
+import './card/card'
+import './link/link'
+import './pages/not-connected'
+import './pages/sharing'
+import './mask/mask'
+import './pages/requests'
+import {
+ Account,
+ PersonaData,
+ RadixButtonStatus,
+ RadixButtonTheme,
+ RequestItem,
+} from 'radix-connect-common'
+import { classMap } from 'lit-html/directives/class-map.js'
+import { themeCSS, variablesCSS } from '../theme'
+
+@customElement('radix-connect-button')
+export class ConnectButton extends LitElement {
+ @property({
+ type: String,
+ })
+ theme: RadixButtonTheme = 'radix-blue'
+
+ @property({ type: String })
+ dAppName: string = ''
+
+ @property({ type: String })
+ personaLabel: string = ''
+
+ @property({ type: Boolean })
+ connected = false
+
+ @property({
+ type: String,
+ })
+ status: RadixButtonStatus = RadixButtonStatus.default
+
+ @property({ type: String })
+ loggedInTimestamp: string = ''
+
+ @property({ type: Boolean })
+ showPopoverMenu: boolean = false
+
+ @property({ type: Array })
+ requestItems: RequestItem[] = []
+
+ @property({ type: Array })
+ accounts: Account[] = []
+
+ @property({
+ type: Array,
+ })
+ personaData: PersonaData[] = []
+
+ @property({
+ type: Boolean,
+ })
+ isMobile: boolean = false
+
+ @property({
+ type: Boolean,
+ })
+ enableMobile: boolean = false
+
+ @property({
+ type: Boolean,
+ })
+ isWalletLinked: boolean = false
+
+ @property({
+ type: Boolean,
+ })
+ isExtensionAvailable: boolean = false
+
+ @property({
+ type: Boolean,
+ })
+ fullWidth: boolean = false
+
+ @property({
+ type: String,
+ })
+ activeTab: 'sharing' | 'requests' = 'sharing'
+
+ @property({ type: String, reflect: true })
+ mode: 'light' | 'dark' = 'light'
+
+ @property({ type: String })
+ avatarUrl: string = ''
+
+ @property({ type: Boolean, state: true })
+ compact = false
+
+ get hasSharedData(): boolean {
+ return !!(this.accounts.length || this.personaData.length)
+ }
+
+ pristine = true
+
+ windowClickEventHandler: (event: MouseEvent) => void
+
+ attributeChangedCallback(
+ name: string,
+ _old: string | null,
+ value: string | null,
+ ): void {
+ super.attributeChangedCallback(name, _old, value)
+ if (name === 'showpopovermenu') {
+ this.pristine = false
+ }
+ }
+
+ constructor() {
+ super()
+ this.windowClickEventHandler = (event) => {
+ if (!this.showPopoverMenu) return
+ if (this.contains(event.target as HTMLElement)) return
+ this.showPopoverMenu = false
+ }
+ document.addEventListener('click', this.windowClickEventHandler)
+ }
+
+ connectedCallback(): void {
+ super.connectedCallback()
+ this.dispatchEvent(
+ new CustomEvent('onRender', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ disconnectedCallback(): void {
+ document.removeEventListener('click', this.windowClickEventHandler)
+ this.dispatchEvent(
+ new CustomEvent('onDestroy', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ private togglePopoverMenu() {
+ this.pristine = false
+ this.showPopoverMenu = !this.showPopoverMenu
+ if (this.showPopoverMenu)
+ this.dispatchEvent(
+ new CustomEvent('onShowPopover', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ private closePopover() {
+ this.showPopoverMenu = false
+ }
+
+ private connectButtonTemplate() {
+ const buttonText = this.connected ? this.personaLabel : 'Connect'
+
+ return html` {
+ this.compact = event.detail.offsetWidth === 40
+ }}
+ >${buttonText}
`
+ }
+
+ private connectTemplate() {
+ if (this.connected) {
+ return
+ }
+
+ return html`
+ `
+ }
+
+ private renderSharingTemplate() {
+ return html` data.value)}
+ .accounts=${this.accounts}
+ @onLogout=${() => {
+ this.dispatchEvent(
+ new CustomEvent('onDisconnect', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }}
+ @onUpdateData=${() => {
+ this.dispatchEvent(
+ new CustomEvent('onUpdateSharedData', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }}
+ >`
+ }
+
+ private renderRequestItemsTemplate() {
+ return html` `
+ }
+
+ private popoverTemplate() {
+ if (this.pristine) return ''
+
+ return html` {
+ this.closePopover()
+ }}
+ class=${classMap({
+ show: this.showPopoverMenu,
+ hide: !this.showPopoverMenu,
+ popoverPosition: !this.isMobile,
+ })}
+ >
+ ${this.isMobile && !this.enableMobile
+ ? this.renderComingSoonTemplate()
+ : this.renderPopoverContentTemplate()}
+ `
+ }
+
+ private renderPopoverContentTemplate() {
+ return this.connected
+ ? html`
+ {
+ this.activeTab = event.detail.value
+ }}"
+ >
+
+ ${this.activeTab === 'sharing'
+ ? this.renderSharingTemplate()
+ : this.renderRequestItemsTemplate()}
+ `
+ : this.connectTemplate()
+ }
+
+ private renderComingSoonTemplate() {
+ return html`
+
+
+ For now, please connect to Radix dApps using a desktop web browser.
+
+
`
+ }
+
+ render() {
+ return html`
+ ${this.connectButtonTemplate()}
+ ${this.isMobile
+ ? html`
+ ${this.popoverTemplate()}
+ `
+ : this.popoverTemplate()}
+ `
+ }
+
+ static styles = [
+ variablesCSS,
+ themeCSS,
+ css`
+ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;600&display=swap');
+
+ :root {
+ font-family: 'IBM Plex Sans';
+ margin: 0;
+ font-size: 16px;
+ line-height: 24px;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+ }
+
+ :host {
+ text-align: left;
+ font-family: 'IBM Plex Sans';
+ position: relative;
+ z-index: 1000;
+ display: inline-block;
+ }
+
+ .mobile-wrapper {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+
+ align-items: center;
+ margin-bottom: 18px;
+ margin-top: 25px;
+ font-size: 14px;
+ }
+
+ .mobile-wrapper .header {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 5px;
+ }
+ .mobile-wrapper .content {
+ font-size: 16px;
+ margin-bottom: 5px;
+ }
+
+ .popoverPosition {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ right: 0;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-connect-button': ConnectButton
+ }
+}
diff --git a/packages/connect-button/src/components/link/link.ts b/packages/connect-button/src/components/link/link.ts
new file mode 100644
index 00000000..547620a5
--- /dev/null
+++ b/packages/connect-button/src/components/link/link.ts
@@ -0,0 +1,55 @@
+import { LitElement, css, html, unsafeCSS } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+
+import NorthEastArrowIcon from '../../assets/icon-north-east-arrow.svg'
+@customElement('radix-link')
+export class RadixLink extends LitElement {
+ @property({
+ type: String,
+ })
+ href: string = ''
+
+ @property({
+ type: String,
+ })
+ displayText: string = ''
+
+ render() {
+ return html`
+ ${this.displayText}
+
+ `
+ }
+
+ static styles = [
+ css`
+ .link {
+ color: var(--radix-link-color);
+ font-weight: 600;
+ text-decoration: none;
+ display: flex;
+ gap: 4px;
+ align-items: center;
+ font-size: 14px;
+ }
+
+ .icon-north-east-arrow::before {
+ content: '';
+ display: block;
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ background-color: var(--radix-card-text-dimmed-color);
+ -webkit-mask-image: url(${unsafeCSS(NorthEastArrowIcon)});
+ mask-image: url(${unsafeCSS(NorthEastArrowIcon)});
+ width: 16px;
+ height: 16px;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-link': RadixLink
+ }
+}
diff --git a/packages/connect-button/src/components/loading-spinner/loading-spinner.ts b/packages/connect-button/src/components/loading-spinner/loading-spinner.ts
new file mode 100644
index 00000000..ed1351da
--- /dev/null
+++ b/packages/connect-button/src/components/loading-spinner/loading-spinner.ts
@@ -0,0 +1,67 @@
+import { css, html } from 'lit'
+import { BUTTON_MIN_WIDTH } from '../../constants'
+
+export const LoadingSpinner = html``
+
+export const loadingSpinnerCSS = css`
+ .loading-spinner-container {
+ display: flex;
+ }
+
+ @container (max-width: ${BUTTON_MIN_WIDTH - 16}px) {
+ .loading-spinner-container {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+
+ button.gradient > .loading-spinner {
+ border-right-color: var(--color-light);
+ border-left-color: color-mix(in srgb, var(--color-light) 30%, transparent);
+ border-top-color: color-mix(in srgb, var(--color-light) 30%, transparent);
+ border-bottom-color: color-mix(
+ in srgb,
+ var(--color-light) 30%,
+ transparent
+ );
+ }
+
+ .loading-spinner {
+ width: 22px;
+ height: 22px;
+ min-width: 22px;
+ min-height: 22px;
+ border: 2px solid var(--radix-connect-button-text-color);
+ border-left-color: color-mix(
+ in srgb,
+ var(--radix-connect-button-text-color) 30%,
+ transparent
+ );
+ border-top-color: color-mix(
+ in srgb,
+ var(--radix-connect-button-text-color) 30%,
+ transparent
+ );
+ border-bottom-color: color-mix(
+ in srgb,
+ var(--radix-connect-button-text-color) 30%,
+ transparent
+ );
+ border-radius: 50%;
+ display: inline-block;
+ box-sizing: border-box;
+ animation: rotation 1s linear infinite;
+ align-self: center;
+ }
+
+ @keyframes rotation {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+`
diff --git a/packages/connect-button/src/components/mask/mask.ts b/packages/connect-button/src/components/mask/mask.ts
new file mode 100644
index 00000000..8e7a0868
--- /dev/null
+++ b/packages/connect-button/src/components/mask/mask.ts
@@ -0,0 +1,44 @@
+import { html, css, LitElement } from 'lit'
+import { customElement } from 'lit/decorators.js'
+
+@customElement('radix-mask')
+export class RadixMask extends LitElement {
+ render() {
+ return html``
+ }
+
+ static styles = [
+ css`
+ :host {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: unset;
+ height: 100%;
+ width: 100%;
+ backdrop-filter: blur(3px);
+ -webkit-backdrop-filter: blur(3px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: opacity 200ms;
+ background: var(--radix-mask-background);
+ }
+
+ :host(.hide) {
+ opacity: 0;
+ pointer-events: none;
+ }
+
+ :host(.show) {
+ opacity: 1;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-mask': RadixMask
+ }
+}
diff --git a/packages/connect-button/src/components/pages/not-connected.ts b/packages/connect-button/src/components/pages/not-connected.ts
new file mode 100644
index 00000000..5a04f8c1
--- /dev/null
+++ b/packages/connect-button/src/components/pages/not-connected.ts
@@ -0,0 +1,233 @@
+import { html, css, LitElement, TemplateResult } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+
+import logoGradient from '../../assets/logo-gradient.png'
+import { RadixButtonStatus, RequestItem } from 'radix-connect-common'
+import { classMap } from 'lit/directives/class-map.js'
+import '../card/request-cards'
+import '../themed-button/themed-button'
+@customElement('radix-not-connected-page')
+export class RadixNotConnectedPage extends LitElement {
+ @property({
+ type: Boolean,
+ })
+ isMobile: boolean = false
+
+ @property({
+ type: String,
+ })
+ status: RadixButtonStatus = RadixButtonStatus.default
+
+ @property({
+ type: Boolean,
+ })
+ isWalletLinked: boolean = false
+
+ @property({
+ type: Boolean,
+ })
+ isExtensionAvailable: boolean = false
+
+ @property({
+ type: Array,
+ })
+ requestItems: RequestItem[] = []
+
+ render() {
+ let template: TemplateResult<1> = this.renderConnectTemplate()
+
+ if (!this.isExtensionAvailable && !this.isMobile)
+ template = this.renderCeNotInstalledTemplate()
+ else if (!this.isWalletLinked && !this.isMobile)
+ template = this.renderCeNotLinkedTemplate()
+ else if (this.status === RadixButtonStatus.pending)
+ template = this.renderRequestItemsTemplate()
+
+ return html`
+
+
+
Connect Your Radix Wallet
+
+ ${template}
+ `
+ }
+
+ private renderRequestItemsTemplate() {
+ return html``
+ }
+
+ private connectNowButtonTemplate() {
+ const disabled =
+ (!this.isExtensionAvailable || !this.isWalletLinked) && !this.isMobile
+ return html` {
+ if (disabled) return
+ this.dispatchEvent(
+ new CustomEvent('onConnect', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }}
+ >
+ Connect Now
+ `
+ }
+
+ private renderCeNotInstalledTemplate() {
+ return html`
+ Before you can connect your Radix Wallet, you need the Radix Connector
+ browser extension.
+
+
+
+ {
+ this.dispatchEvent(
+ new CustomEvent('onLinkClick', {
+ bubbles: true,
+ composed: true,
+ detail: { type: 'setupGuide' },
+ }),
+ )
+ }}
+ >
+
+
+ ${this.connectNowButtonTemplate()} `
+ }
+
+ private renderCeNotLinkedTemplate() {
+ return html`
+ To connect your Radix Wallet, you need to link it to your Radix
+ Connector browser extension using a QR code.
+
+
+ {
+ this.dispatchEvent(
+ new CustomEvent('onLinkClick', {
+ bubbles: true,
+ composed: true,
+ detail: { type: 'showQrCode' },
+ }),
+ )
+ }}
+ >
+ Open QR Code to Link Wallet
+
+
+
+ {
+ this.dispatchEvent(
+ new CustomEvent('onLinkClick', {
+ bubbles: true,
+ composed: true,
+ detail: { type: 'setupGuide' },
+ }),
+ )
+ }}
+ >
+
+
+ ${this.connectNowButtonTemplate()} `
+ }
+
+ private renderConnectTemplate() {
+ return html` ${this.connectNowButtonTemplate()} `
+ }
+
+ static styles = [
+ css`
+ :host {
+ width: 100%;
+ box-sizing: border-box;
+ }
+
+ .wrapper.connect-your-wallet {
+ display: flex;
+ align-items: center;
+ margin: 12px 0.5rem 1.5rem;
+ line-height: 23px;
+ justify-content: center;
+ gap: 12px;
+ }
+
+ .request-cards {
+ display: block;
+ max-height: 410px;
+ overflow-y: auto;
+ }
+
+ .card {
+ margin-bottom: 10px;
+ }
+ .info {
+ margin-bottom: 20px;
+ padding: 0 20px;
+ font-size: 14px;
+ line-height: 18px;
+ text-align: center;
+ }
+
+ .cta-link {
+ display: flex;
+ justify-content: center;
+ margin: 25px 0;
+ }
+
+ .text.connect {
+ color: var(--color-text-primary);
+ font-size: 18px;
+ width: 7.2rem;
+ font-weight: 600;
+ text-align: left;
+ }
+
+ .subtitle {
+ color: var(--radix-card-text-dimmed-color);
+ }
+
+ .mobile-wrapper {
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+
+ align-items: center;
+ margin-bottom: 18px;
+ margin-top: 25px;
+ font-size: 14px;
+ }
+
+ .mobile-wrapper .header {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 5px;
+ }
+ .mobile-wrapper .content {
+ font-size: 16px;
+ margin-bottom: 5px;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-not-connected-page': RadixNotConnectedPage
+ }
+}
diff --git a/packages/connect-button/src/components/pages/pages.stories.ts b/packages/connect-button/src/components/pages/pages.stories.ts
new file mode 100644
index 00000000..c468e46d
--- /dev/null
+++ b/packages/connect-button/src/components/pages/pages.stories.ts
@@ -0,0 +1,149 @@
+import { Meta, StoryObj } from '@storybook/web-components'
+import { html } from 'lit'
+import '../account/account'
+import './sharing'
+import './not-connected'
+import '../popover/popover'
+import { RadixButtonStatus } from 'radix-connect-common'
+
+const meta: Meta = {
+ title: 'Components / Pages',
+}
+export default meta
+
+type Story = StoryObj
+
+export const Sharing: Story = {
+ render: (args) =>
+ html`
+
+ `,
+ args: {
+ avatarUrl: 'https://picsum.photos/200',
+ persona: 'VanDammeStelea',
+ dAppName: 'Dashboard',
+ personaData: [
+ 'Alex Stelea',
+ 'alex.stelea@van.damme',
+ '+42084652103',
+ '45 Avebury Drive, Duncan Road, Elshewhere, NY 98827',
+ 'Passport: 78668279872HS',
+ 'Lorem ipsum',
+ 'dolor sit amet',
+ 'consectetur adipiscing elit',
+ 'Aenean',
+ 'ultrices sodales',
+ 'ex, vitae fringilla',
+ ],
+ accounts: [
+ [
+ 'Main',
+ 'account_tdx_21_12x4zx09f8962a9wesfqvxaue0qn6m39r3cpysrjd6dtqppzhrkjrsr',
+ ],
+ [
+ 'Saving',
+ 'account_tdx_21_12xdjp5dq7haph4c75mst99mc26gkm8mys70v6qlyz0fz86f9ucy0ru',
+ ],
+ [
+ 'Degen',
+ 'account_tdx_21_1298kg54s9r9evc5tgglj2wrqsatuflwxg5s3m845uut6t3jtyh6cyy',
+ ],
+ [
+ 'Gaming',
+ 'account_tdx_21_12y78nedvqg9svp49fjs4f9y5kreweqxt6vszaprnfq8kjhralku6fz',
+ ],
+ [
+ 'Trading',
+ 'account_tdx_21_128pncqprt3gfew04aefqy549ecvfp0a99mxjpa6wcpl2n2ymqr8gj3',
+ ],
+ [
+ 'Staking',
+ 'account_tdx_21_12yccemy8vx37qkctmpkgdtatxe8mdmwl9mndv5dx69mj7tg45d4q88',
+ ],
+ [
+ 'Professional',
+ 'account_tdx_21_129tr5q2g6eh7zxwzl6tj0ndq87zzuqynqt56xpe3v2pf5k9wp67ju6',
+ ],
+ [
+ 'Fun',
+ 'account_tdx_21_12xgzze2krhmw95r07y4pccssgyjxzwgem86hndy8cujfzhkggdpt7s',
+ ],
+ [
+ 'Travel',
+ 'account_tdx_21_129q44nllnywkm8pscgqfq5wkpcfxtq2xffyca745c3fau3swhkhrjw',
+ ],
+ [
+ 'Alpha',
+ 'account_tdx_21_12yc8neefcqfum2u4r5xtgder57va8ahdjm3qr9eatyhmdec62ya6m4',
+ ],
+ [
+ 'Beta',
+ 'account_tdx_21_12yg7c2752f4uwy6ayljg3g5pvj36xxdy690hj7fpllsed53jsgczz4',
+ ],
+ [
+ 'VeryLongAccountName',
+ 'account_tdx_21_129vzduy6q5ufxxekf66eqdjy2vrm6ezdl0sh5kjhgrped9p5k6t9nf',
+ ],
+ ].map(([label, address], appearanceId) => ({
+ label,
+ address,
+ appearanceId,
+ })),
+ },
+}
+
+export const NotConnected: Story = {
+ render: (args) =>
+ html`
+
+
+ `,
+ argTypes: {
+ status: {
+ options: [RadixButtonStatus.default, RadixButtonStatus.pending],
+ control: 'select',
+ },
+ requestItems: {
+ mapping: {
+ loginRequestWithCancel: [
+ {
+ type: 'loginRequest',
+ status: 'pending',
+ timestamp: 1690554318703,
+ showCancel: true,
+ },
+ ],
+ loginRequestWithoutCancel: [
+ {
+ type: 'loginRequest',
+ status: 'pending',
+ timestamp: 1690554318703,
+ showCancel: false,
+ },
+ ],
+ empty: undefined,
+ },
+ control: 'select',
+ options: ['loginRequestWithCancel', 'loginRequestWithoutCancel', 'empty'],
+ },
+ },
+ args: {
+ isMobile: false,
+ status: 'default',
+ isExtensionAvailable: false,
+ isWalletLinked: false,
+ requestItems: undefined,
+ },
+}
diff --git a/packages/connect-button/src/components/pages/requests.ts b/packages/connect-button/src/components/pages/requests.ts
new file mode 100644
index 00000000..4f54aee7
--- /dev/null
+++ b/packages/connect-button/src/components/pages/requests.ts
@@ -0,0 +1,86 @@
+import { css, html, LitElement } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import '../card/request-card'
+import { RequestItem } from 'radix-connect-common'
+import { pageStyles } from './styles'
+import { formatTimestamp } from '../../helpers/format-timestamp'
+
+@customElement('radix-requests-page')
+export class RadixRequestsPage extends LitElement {
+ @property({ type: Array })
+ requestItems: RequestItem[] = []
+
+ @property({
+ type: String,
+ })
+ dAppName: string = ''
+
+ @property({
+ type: String,
+ })
+ loggedInTimestamp: string = ''
+
+ render() {
+ return html`
+
+
+ ${this.loggedInTimestamp
+ ? html``
+ : ''}
+
+ ${(this.requestItems || []).map(
+ (requestItem) =>
+ html` {
+ this.dispatchEvent(
+ new CustomEvent('onCancel', {
+ detail: event.detail,
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }}
+ timestamp=${requestItem.createdAt}
+ >`,
+ )}
+
+ `
+ }
+
+ static styles = [
+ pageStyles,
+ css`
+ .subheader {
+ color: var(--radix-card-text-dimmed-color);
+ margin-top: -12px;
+ margin-bottom: 15px;
+ text-align: center;
+ font-size: 12px;
+ }
+
+ .content {
+ padding-bottom: 25px;
+ max-height: calc(100vh - 180px);
+ }
+
+ @media (min-height: 580px) {
+ .content {
+ max-height: 360px;
+ }
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-requests-page': RadixRequestsPage
+ }
+}
diff --git a/packages/connect-button/src/components/pages/sharing.ts b/packages/connect-button/src/components/pages/sharing.ts
new file mode 100644
index 00000000..5f2aae96
--- /dev/null
+++ b/packages/connect-button/src/components/pages/sharing.ts
@@ -0,0 +1,141 @@
+import { html, css, LitElement, unsafeCSS } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { Account } from 'radix-connect-common'
+import '../account/account'
+import '../card/persona-card'
+import '../popover/popover'
+import '../tabs-menu/tabs-menu'
+import '../themed-button/themed-button'
+import RefreshIcon from '../../assets/refresh.svg'
+import LogoutIcon from '../../assets/logout.svg'
+import { pageStyles } from './styles'
+
+@customElement('radix-sharing-page')
+export class RadixSharingPage extends LitElement {
+ @property({
+ type: String,
+ })
+ avatarUrl: string = ''
+
+ @property({
+ type: String,
+ })
+ persona: string = ''
+
+ @property({
+ type: String,
+ })
+ dAppName: string = ''
+
+ @property({
+ type: Array,
+ })
+ personaData: string[] = []
+
+ @property({
+ type: Array,
+ })
+ accounts: Account[] = []
+
+ private onUpdateData(event: MouseEvent) {
+ this.dispatchEvent(
+ new CustomEvent('onUpdateData', {
+ detail: event,
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ private onLogout(event: MouseEvent) {
+ this.dispatchEvent(
+ new CustomEvent('onLogout', {
+ detail: event,
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ render() {
+ return html`
+
+
+
+ ${(this.accounts || []).map(
+ ({ label, address, appearanceId }) =>
+ html``,
+ )}
+
+
+ `
+ }
+
+ static styles = [
+ pageStyles,
+ css`
+ :host {
+ width: 100%;
+ }
+ .icon::before {
+ content: '';
+ -webkit-mask-position: center;
+ mask-position: center;
+ -webkit-mask-size: cover;
+ mask-size: cover;
+ background: var(--radix-button-text-color);
+ display: block;
+ width: 20px;
+ height: 20px;
+ }
+ .content {
+ max-height: 193px;
+ overflow-x: hidden;
+ padding-bottom: 19px;
+ }
+ .buttons {
+ display: grid;
+ bottom: 0;
+ width: 100%;
+ grid-template-columns: 1fr 115px;
+ grid-gap: 10px;
+ width: 100%;
+ padding-top: 5px;
+ align-items: end;
+ }
+
+ .update-data::before {
+ -webkit-mask-image: url(${unsafeCSS(RefreshIcon)});
+ mask-image: url(${unsafeCSS(RefreshIcon)});
+ }
+
+ .logout::before {
+ -webkit-mask-image: url(${unsafeCSS(LogoutIcon)});
+ mask-image: url(${unsafeCSS(LogoutIcon)});
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-sharing-page': RadixSharingPage
+ }
+}
diff --git a/packages/connect-button/src/components/pages/styles.ts b/packages/connect-button/src/components/pages/styles.ts
new file mode 100644
index 00000000..8d53debb
--- /dev/null
+++ b/packages/connect-button/src/components/pages/styles.ts
@@ -0,0 +1,27 @@
+import { css } from 'lit'
+
+export const pageStyles = css`
+ :host {
+ width: 100%;
+ }
+
+ .header {
+ font-size: 12px;
+ font-weight: 400;
+ margin: 15px 0px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: 100%;
+ text-align: center;
+ }
+
+ .content {
+ overflow: auto;
+ width: 100%;
+ margin-bottom: 0;
+ position: relative;
+ -webkit-mask-image: linear-gradient(180deg, black 90%, transparent 100%);
+ mask-image: linear-gradient(180deg, black 90%, transparent 95%);
+ }
+`
diff --git a/packages/connect-button/src/components/popover/popover.stories.ts b/packages/connect-button/src/components/popover/popover.stories.ts
new file mode 100644
index 00000000..cb5c5dcf
--- /dev/null
+++ b/packages/connect-button/src/components/popover/popover.stories.ts
@@ -0,0 +1,21 @@
+import { Meta, StoryObj } from '@storybook/web-components'
+import './popover'
+import '../tabs-menu/tabs-menu'
+import { html } from 'lit'
+
+const meta: Meta = {
+ title: 'Components / Popover',
+ component: 'radix-popover',
+}
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: (args) => html`
+
+ `,
+ args: {
+ connected: true,
+ },
+}
diff --git a/packages/connect-button/src/components/popover/popover.ts b/packages/connect-button/src/components/popover/popover.ts
new file mode 100644
index 00000000..7b725a4e
--- /dev/null
+++ b/packages/connect-button/src/components/popover/popover.ts
@@ -0,0 +1,179 @@
+import { html, css, LitElement, unsafeCSS } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { themeCSS } from '../../theme'
+import '../tabs-menu/tabs-menu'
+import { encodeBase64 } from '../../helpers/encode-base64'
+import CloseIcon from '../../assets/icon-close.svg'
+
+@customElement('radix-popover')
+export class RadixPopover extends LitElement {
+ @property({
+ type: Boolean,
+ })
+ connected = false
+
+ @property({
+ type: Boolean,
+ })
+ compact = false
+
+ @property({
+ type: Boolean,
+ reflect: true,
+ })
+ isMobile = false
+
+ closePopover() {
+ this.dispatchEvent(
+ new CustomEvent('onClosePopover', {
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ svgBorder = `data:image/svg+xml;base64,${encodeBase64(``)}`
+
+ private closeButton() {
+ return html``
+ }
+
+ render() {
+ return html`
+
+
+ ${this.isMobile ? this.closeButton() : ''}
+
+
+ `
+ }
+
+ static styles = [
+ themeCSS,
+ css`
+ :host {
+ user-select: none;
+ display: inline-flex;
+ background-position: center top;
+ background-repeat: no-repeat;
+ justify-content: center;
+ align-items: flex-start;
+ background: var(--radix-popover-background);
+ backdrop-filter: blur(30px);
+ -webkit-backdrop-filter: blur(30px);
+ box-sizing: border-box;
+ max-height: 100vh;
+ border-radius: 12px;
+ padding: 12px;
+ box-shadow: 0px 16px 35px 0px #00000047;
+ }
+
+ :host([isMobile]) {
+ max-width: 100%;
+ }
+
+ #radix-popover-content {
+ width: 344px;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ flex-direction: column;
+ overflow: auto;
+ }
+
+ #close-button {
+ -webkit-mask-image: url(${unsafeCSS(CloseIcon)});
+ mask-image: url(${unsafeCSS(CloseIcon)});
+ background-color: var(--radix-card-text-color);
+ width: 24px;
+ height: 24px;
+ background-repeat: no-repeat;
+ align-self: flex-start;
+ margin-bottom: 10px;
+ cursor: pointer;
+ }
+
+ #close-button:hover {
+ opacity: 0.8;
+ }
+
+ @-webkit-keyframes slide-bottom {
+ 0% {
+ -webkit-transform: translateY(-10px);
+ transform: translateY(-10px);
+ opacity: 0;
+ }
+ 100% {
+ -webkit-transform: translateY(0px);
+ transform: translateY(0px);
+ opacity: 1;
+ }
+ }
+ @keyframes slide-bottom {
+ 0% {
+ -webkit-transform: translateY(-10px);
+ transform: translateY(-10px);
+ opacity: 0;
+ }
+ 100% {
+ -webkit-transform: translateY(0px);
+ transform: translateY(0px);
+ opacity: 1;
+ }
+ }
+
+ @-webkit-keyframes slide-up {
+ 0% {
+ -webkit-transform: translateY(0px);
+ transform: translateY(0px);
+ opacity: 1;
+ }
+ 100% {
+ -webkit-transform: translateY(-10px);
+ transform: translateY(-10px);
+ opacity: 0;
+ }
+ }
+ @keyframes slide-up {
+ 0% {
+ -webkit-transform: translateY(0px);
+ transform: translateY(0px);
+ opacity: 1;
+ }
+ 100% {
+ -webkit-transform: translateY(-10px);
+ transform: translateY(-10px);
+ opacity: 0;
+ }
+ }
+
+ :host(.hide) {
+ pointer-events: none;
+ -webkit-animation: slide-up 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)
+ both;
+ animation: slide-up 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ }
+ :host(.show) {
+ -webkit-animation: slide-bottom 0.2s
+ cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ animation: slide-bottom 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-popover': RadixPopover
+ }
+}
diff --git a/packages/connect-button/src/components/tabs-menu/tabs-menu.stories.ts b/packages/connect-button/src/components/tabs-menu/tabs-menu.stories.ts
new file mode 100644
index 00000000..50b41a3b
--- /dev/null
+++ b/packages/connect-button/src/components/tabs-menu/tabs-menu.stories.ts
@@ -0,0 +1,30 @@
+import { Meta, StoryObj } from '@storybook/web-components'
+import './tabs-menu'
+import { html } from 'lit'
+
+const meta: Meta = {
+ title: 'Components / Tabs menu',
+}
+export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: (args) => html`
+ {
+ console.log(`clicked: ${ev.detail.value}`)
+ }}
+ >
+ `,
+ argTypes: {
+ activeTab: {
+ options: ['sharing', 'requests'],
+ control: 'select',
+ },
+ },
+ args: {
+ activeTab: 'sharing',
+ },
+}
diff --git a/packages/connect-button/src/components/tabs-menu/tabs-menu.ts b/packages/connect-button/src/components/tabs-menu/tabs-menu.ts
new file mode 100644
index 00000000..4a8b93fa
--- /dev/null
+++ b/packages/connect-button/src/components/tabs-menu/tabs-menu.ts
@@ -0,0 +1,117 @@
+import { html, css, LitElement } from 'lit'
+import { customElement, property } from 'lit/decorators.js'
+import { classMap } from 'lit/directives/class-map.js'
+
+@customElement('radix-tabs-menu')
+export class RadixTabsMenu extends LitElement {
+ @property({
+ type: String,
+ reflect: true,
+ })
+ active: 'sharing' | 'requests' = 'sharing'
+
+ private onClick(value: 'sharing' | 'requests', event: MouseEvent) {
+ this.dispatchEvent(
+ new CustomEvent('onClick', {
+ detail: { value, event },
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+
+ render() {
+ return html`
+
+
+
+
+
+ `
+ }
+
+ static styles = [
+ css`
+ :host {
+ display: block;
+ width: 100%;
+ user-select: none;
+ }
+
+ .tabs {
+ width: calc(100% - 10px);
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ justify-content: space-between;
+ padding: 5px;
+ border-radius: 12px;
+ position: relative;
+ background: var(--radix-popover-tabs-background);
+ }
+
+ button {
+ border: unset;
+ font-size: 14px;
+ background: transparent;
+ text-align: center;
+ flex: 1;
+ border-radius: 8px;
+ font-weight: 600;
+ color: var(--radix-popover-text-color);
+ width: 100%;
+ height: 32px;
+ z-index: 1;
+ margin: 0;
+ padding: 0;
+ }
+
+ button:not(.active) {
+ cursor: pointer;
+ }
+
+ .active-indicator {
+ width: calc(50% - 5px);
+ height: 32px;
+ border-radius: 8px;
+ position: absolute;
+ box-shadow: 0px 4px 5px 0px #0000001a;
+ background: var(--radix-popover-tabs-button-active-background);
+ top: 5px;
+ transition: transform 0.125s cubic-bezier(0.45, 0, 0.55, 1);
+ }
+
+ :host([active='sharing']) .active-indicator {
+ transform: translateX(5px);
+ }
+
+ :host([active='requests']) .active-indicator {
+ transform: translateX(calc(100% + 5px));
+ }
+
+ button:focus,
+ button:focus-visible {
+ outline: 0px auto -webkit-focus-ring-color;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-tabs-menu': RadixTabsMenu
+ }
+}
diff --git a/packages/connect-button/src/components/themed-button/themed-button.ts b/packages/connect-button/src/components/themed-button/themed-button.ts
new file mode 100644
index 00000000..212f9728
--- /dev/null
+++ b/packages/connect-button/src/components/themed-button/themed-button.ts
@@ -0,0 +1,59 @@
+import { html, css, LitElement } from 'lit'
+import { customElement } from 'lit/decorators.js'
+
+@customElement('radix-themed-button')
+export class RadixThemedButton extends LitElement {
+ render() {
+ return html` `
+ }
+
+ static styles = [
+ css`
+ button {
+ transition: background-color 0.1s cubic-bezier(0.45, 0, 0.55, 1);
+ border-radius: 12px;
+ border: none;
+ background: var(--radix-button-background);
+ color: var(--radix-button-text-color);
+ font-size: 14px;
+ font-weight: 600;
+ padding: 11px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ }
+
+ button:hover {
+ background: var(--radix-button-background-hover);
+ }
+
+ button:active {
+ background: var(--radix-button-background-pressed);
+ }
+
+ :host(.primary) button {
+ background: var(--color-radix-blue-2);
+ color: var(--color-light);
+ }
+
+ :host(.full) button {
+ width: 100%;
+ }
+
+ :host(.primary.disabled) button,
+ :host(.disabled) button {
+ background: var(--radix-button-disabled-background);
+ color: var(--radix-button-disabled-text-color);
+ cursor: default;
+ }
+ `,
+ ]
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'radix-themed-button': RadixThemedButton
+ }
+}
diff --git a/packages/connect-button/src/config.ts b/packages/connect-button/src/config.ts
new file mode 100644
index 00000000..c298ddf3
--- /dev/null
+++ b/packages/connect-button/src/config.ts
@@ -0,0 +1,4 @@
+export const config = {
+ elementTag: 'radix-connect-button',
+ links: { 'What is a radix wallet?': 'https://wallet.radixdlt.com/' },
+} as const
diff --git a/packages/connect-button/src/constants.ts b/packages/connect-button/src/constants.ts
new file mode 100644
index 00000000..6325dd4b
--- /dev/null
+++ b/packages/connect-button/src/constants.ts
@@ -0,0 +1,3 @@
+export const BUTTON_MIN_HEIGHT = 32
+export const BUTTON_COMPACT_MIN_WIDTH = 32
+export const BUTTON_MIN_WIDTH = 138
diff --git a/packages/connect-button/src/helpers/encode-base64.ts b/packages/connect-button/src/helpers/encode-base64.ts
new file mode 100644
index 00000000..4565a1d5
--- /dev/null
+++ b/packages/connect-button/src/helpers/encode-base64.ts
@@ -0,0 +1,9 @@
+export const encodeBase64 = (data: string) => {
+ if (typeof btoa === 'function') {
+ return btoa(data)
+ } else if (typeof Buffer === 'function') {
+ return Buffer.from(data, 'utf-8').toString('base64')
+ } else {
+ throw new Error('Failed to determine the platform specific encoder')
+ }
+}
diff --git a/packages/connect-button/src/helpers/format-timestamp.ts b/packages/connect-button/src/helpers/format-timestamp.ts
new file mode 100644
index 00000000..c34981d4
--- /dev/null
+++ b/packages/connect-button/src/helpers/format-timestamp.ts
@@ -0,0 +1,38 @@
+const isToday = (someDate: Date) => {
+ const today = new Date()
+ return (
+ someDate.getDate() == today.getDate() &&
+ someDate.getMonth() == today.getMonth() &&
+ someDate.getFullYear() == today.getFullYear()
+ )
+}
+
+const isYesterday = (someDate: Date) => {
+ const yesterday = new Date()
+ yesterday.setDate(yesterday.getDate() - 1)
+ return (
+ someDate.getDate() == yesterday.getDate() &&
+ someDate.getMonth() == yesterday.getMonth() &&
+ someDate.getFullYear() == yesterday.getFullYear()
+ )
+}
+
+export const formatTimestamp = (timestamp: number | string, divider = ' ') => {
+ const date = new Date(Number(timestamp))
+
+ const today = isToday(date)
+ const yesterday = isYesterday(date)
+ const time = date.toLocaleTimeString('en-Gb', {
+ // en-GB is causing midnight to be 00:00
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: false,
+ })
+
+ if (today) return `Today${divider}${time}`
+ if (yesterday) return `Yesterday${divider}${time}`
+
+ return `${date.getDate()} ${date.toLocaleString('en-US', {
+ month: 'short',
+ })}${divider}${time}`
+}
diff --git a/packages/connect-button/src/helpers/shorten-address.ts b/packages/connect-button/src/helpers/shorten-address.ts
new file mode 100644
index 00000000..4128a6a5
--- /dev/null
+++ b/packages/connect-button/src/helpers/shorten-address.ts
@@ -0,0 +1,7 @@
+export const shortenAddress = (address?: string) =>
+ address
+ ? `${address.slice(0, 4)}...${address.slice(
+ address.length - 6,
+ address.length,
+ )}`
+ : ''
diff --git a/packages/connect-button/src/index.ts b/packages/connect-button/src/index.ts
new file mode 100644
index 00000000..f0bc36fa
--- /dev/null
+++ b/packages/connect-button/src/index.ts
@@ -0,0 +1 @@
+export * from './components/connect-button'
diff --git a/packages/connect-button/src/theme.ts b/packages/connect-button/src/theme.ts
new file mode 100644
index 00000000..88af0826
--- /dev/null
+++ b/packages/connect-button/src/theme.ts
@@ -0,0 +1,171 @@
+import { css } from 'lit'
+
+export const variablesCSS = css`
+ :host {
+ /* Core colors */
+ --color-radix-green-1: #00ab84;
+ --color-radix-green-2: #00c389;
+ --color-radix-green-3: #21ffbe;
+ --color-radix-blue-1: #060f8f;
+ --color-radix-blue-2: #052cc0;
+ --color-radix-blue-3: #20e4ff;
+ --color-light: #ffffff;
+ --color-dark: #000000;
+
+ /* Accent colors */
+ --color-accent-red: #ef4136;
+ --color-accent-blue: #00aeef;
+ --color-accent-yellow: #fff200;
+ --color-alert: #e59700;
+ --color-radix-error-red-1: #c82020;
+ --color-radix-error-red-2: #fcebeb;
+
+ /* Neutral colors */
+ --color-grey-1: #003057;
+ --color-grey-2: #8a8fa4;
+ --color-grey-3: #ced0d6;
+ --color-grey-4: #e2e5ed;
+ --color-grey-5: #f4f5f9;
+ }
+`
+
+export const themeCSS = css`
+ :host {
+ font-family:
+ 'IBM Plex Sans',
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ 'Open Sans',
+ 'Helvetica Neue',
+ sans-serif;
+ }
+
+ :host([mode='light']) {
+ --radix-popover-background: color-mix(in srgb, #efefef 50%, transparent);
+ --radix-popover-text-color: var(--color-grey-1);
+
+ --radix-popover-tabs-background: color-mix(
+ in srgb,
+ var(--color-grey-2) 15%,
+ transparent
+ );
+ --radix-popover-tabs-button-active-background: var(--color-light);
+
+ --radix-link-color: var(--color-radix-blue-2);
+
+ --radix-card-background: var(--color-light);
+ --radix-card-text-color: var(--color-grey-1);
+ --radix-card-text-dimmed-color: var(--color-grey-2);
+ --radix-card-inverted-background: var(--color-grey-1);
+ --radix-card-inverted-text-color: var(--color-light);
+
+ --radix-avatar-border-color: var(--color-grey-5);
+ --radix-mask-background: color-mix(
+ in srgb,
+ var(--color-light) 50%,
+ transparent
+ );
+
+ --radix-button-background: color-mix(
+ in srgb,
+ var(--color-light) 80%,
+ transparent
+ );
+ --radix-button-background-hover: var(--color-light);
+ --radix-button-background-pressed: var(--color-grey-5);
+ --radix-button-text-color: var(--color-radix-blue-2);
+
+ --radix-button-disabled-background: color-mix(
+ in srgb,
+ var(--color-light) 80%,
+ transparent
+ );
+ --radix-button-disabled-text-color: var(--color-grey-3);
+
+ color: var(--color-grey-1);
+ }
+
+ :host([mode='dark']) {
+ --radix-popover-background: color-mix(in srgb, #000000 50%, transparent);
+ --radix-popover-text-color: var(--color-light);
+
+ --radix-popover-tabs-background: color-mix(
+ in srgb,
+ var(--color-dark) 60%,
+ transparent
+ );
+ --radix-popover-tabs-button-active-text-color: var(--color-light);
+ --radix-popover-tabs-button-active-background: #515151;
+
+ --radix-link-color: var(--color-white);
+
+ --radix-card-background: #515151;
+ --radix-card-text-color: var(--color-light);
+ --radix-card-text-dimmed-color: var(--color-grey-3);
+ --radix-card-inverted-background: var(--color-grey-5);
+ --radix-card-inverted-text-color: var(--color-grey-1);
+
+ --radix-avatar-border-color: #656565;
+ --radix-mask-background: color-mix(
+ in srgb,
+ var(--color-dark) 40%,
+ transparent
+ );
+
+ --radix-button-background: color-mix(
+ in srgb,
+ var(--color-dark) 40%,
+ transparent
+ );
+ --radix-button-background-hover: var(--color-dark);
+ --radix-button-background-pressed: #414141;
+ --radix-button-text-color: var(--color-light);
+
+ --radix-button-disabled-background: color-mix(
+ in srgb,
+ var(--color-dark) 40%,
+ transparent
+ );
+ --radix-button-disabled-text-color: color-mix(
+ in srgb,
+ var(--color-light) 20%,
+ transparent
+ );
+
+ color: var(--color-light);
+ }
+
+ :host([theme='radix-blue']) {
+ --radix-connect-button-background: var(--color-radix-blue-2);
+ --radix-connect-button-background-hover: var(--color-radix-blue-1);
+ --radix-connect-button-border-color: var(--color-radix-blue-2);
+ --radix-connect-button-text-color: var(--color-light);
+ }
+
+ :host([theme='black']) {
+ --radix-connect-button-background: var(--color-dark);
+ --radix-connect-button-background-hover: #3e3e3e;
+ --radix-connect-button-border-color: var(--color-dark);
+ --radix-connect-button-text-color: var(--color-light);
+ }
+
+ :host([theme='white-with-outline']) {
+ --radix-connect-button-background: var(--color-light);
+ --radix-connect-button-background-hover: var(--color-grey-5);
+ --radix-connect-button-border-color: var(--color-dark);
+ --radix-connect-button-text-color: var(--color-dark);
+ }
+
+ :host([theme='white']) {
+ --radix-connect-button-background: var(--color-light);
+ --radix-connect-button-background-hover: var(--color-grey-5);
+ --radix-connect-button-border-color: var(--color-light);
+ --radix-connect-button-text-color: var(--color-dark);
+ }
+`
diff --git a/packages/connect-button/src/vite-env.d.ts b/packages/connect-button/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/packages/connect-button/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/tsconfig.json b/packages/connect-button/tsconfig.json
similarity index 73%
rename from tsconfig.json
rename to packages/connect-button/tsconfig.json
index 6b156021..73507920 100644
--- a/tsconfig.json
+++ b/packages/connect-button/tsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
- "module": "ESNext",
+ "module": "es2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
- "target": "ES6",
+ "target": "ES2021",
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
@@ -17,8 +17,14 @@
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": false,
- "skipLibCheck": true
+ "skipLibCheck": true,
+ "plugins": [
+ {
+ "name": "ts-lit-plugin",
+ "strict": true
+ }
+ ]
},
- "include": ["src/**/*.ts"],
+ "include": ["src/**/*.ts", "src/components"],
"references": [{ "path": "./tsconfig.node.json" }]
}
diff --git a/tsconfig.node.json b/packages/connect-button/tsconfig.node.json
similarity index 100%
rename from tsconfig.node.json
rename to packages/connect-button/tsconfig.node.json
diff --git a/vite.config.ts b/packages/connect-button/vite.config.ts
similarity index 60%
rename from vite.config.ts
rename to packages/connect-button/vite.config.ts
index b5052d73..51bc0f46 100644
--- a/vite.config.ts
+++ b/packages/connect-button/vite.config.ts
@@ -1,15 +1,17 @@
import { defineConfig } from 'vite'
+import minifyHTML from 'rollup-plugin-minify-html-literals'
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
- name: 'radix-dapp-toolkit',
+ name: 'connect-button',
},
rollupOptions: {
external: /^lit/,
},
},
- define: { 'process.env.NODE_ENV': '"production"' },
+ // @ts-ignore
+ plugins: [minifyHTML.default()],
})
diff --git a/packages/dapp-toolkit/.github/workflows/build.yml b/packages/dapp-toolkit/.github/workflows/build.yml
new file mode 100644
index 00000000..13fe527b
--- /dev/null
+++ b/packages/dapp-toolkit/.github/workflows/build.yml
@@ -0,0 +1,106 @@
+name: Build
+
+on:
+ push:
+ branches:
+ - '**'
+
+jobs:
+ snyk-scan-deps-licences:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ pull-requests: read
+ contents: read
+ deployments: write
+ steps:
+ - uses: RDXWorks-actions/checkout@main
+ - uses: radixdlt/public-iac-resuable-artifacts/fetch-secrets@main
+ with:
+ role_name: ${{ secrets.AWS_ROLE_NAME_SNYK_SECRET }}
+ app_name: 'radix-dapp-toolkit'
+ step_name: 'snyk-scan-deps-licenses'
+ secret_prefix: 'SNYK'
+ secret_name: ${{ secrets.AWS_SECRET_NAME_SNYK }}
+ parse_json: true
+ - name: Run Snyk to check for deps vulnerabilities
+ uses: RDXWorks-actions/snyk-actions/node@master
+ with:
+ args: --all-projects --org=${{ env.SNYK_PROJECTS_ORG_ID }} --severity-threshold=critical
+
+ snyk-scan-code:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ pull-requests: read
+ contents: read
+ deployments: write
+ steps:
+ - uses: RDXWorks-actions/checkout@main
+ - uses: radixdlt/public-iac-resuable-artifacts/fetch-secrets@main
+ with:
+ role_name: ${{ secrets.AWS_ROLE_NAME_SNYK_SECRET }}
+ app_name: 'radix-dapp-toolkit'
+ step_name: 'snyk-scan-code'
+ secret_prefix: 'SNYK'
+ secret_name: ${{ secrets.AWS_SECRET_NAME_SNYK }}
+ parse_json: true
+ - name: Run Snyk to check for code vulnerabilities
+ uses: RDXWorks-actions/snyk-actions/node@master
+ with:
+ args: --all-projects --org=${{ env.SNYK_PROJECTS_ORG_ID }} --severity-threshold=high
+ command: code test
+
+ snyk-sbom:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ pull-requests: read
+ contents: read
+ deployments: write
+ needs:
+ - snyk-scan-deps-licences
+ - snyk-scan-code
+ steps:
+ - uses: RDXWorks-actions/checkout@main
+ - uses: radixdlt/public-iac-resuable-artifacts/fetch-secrets@main
+ with:
+ role_name: ${{ secrets.AWS_ROLE_NAME_SNYK_SECRET }}
+ app_name: 'radix-dapp-toolkit'
+ step_name: 'snyk-sbom'
+ secret_prefix: 'SNYK'
+ secret_name: ${{ secrets.AWS_SECRET_NAME_SNYK }}
+ parse_json: true
+ - name: Generate SBOM # check SBOM can be generated but nothing is done with it
+ uses: RDXWorks-actions/snyk-actions/node@master
+ with:
+ args: --all-projects --org=${{ env.SNYK_PROJECTS_ORG_ID }} --format=cyclonedx1.4+json --json-file-output sbom.json
+ command: sbom
+
+ build:
+ runs-on: ubuntu-latest
+ needs:
+ - snyk-scan-deps-licences
+ - snyk-scan-code
+ steps:
+ - uses: RDXWorks-actions/checkout@main
+
+ - name: Use Node.js
+ uses: RDXWorks-actions/setup-node@main
+ with:
+ node-version: '18.x'
+
+ - name: Authenticate with private NPM package
+ run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPMJS_TOKEN }}" > ~/.npmrc
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run tests
+ run: npm run test
+
+ - name: Build
+ run: npm run build
+
+ - name: Dump context
+ uses: RDXWorks-actions/ghaction-dump-context@master
\ No newline at end of file
diff --git a/packages/dapp-toolkit/.github/workflows/release.yml b/packages/dapp-toolkit/.github/workflows/release.yml
new file mode 100644
index 00000000..3a398ab4
--- /dev/null
+++ b/packages/dapp-toolkit/.github/workflows/release.yml
@@ -0,0 +1,57 @@
+name: Release
+on:
+ push:
+ branches:
+ - main
+ - develop
+ - release/**
+ workflow_dispatch:
+
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ permissions: write-all
+ steps:
+ - name: Checkout
+ uses: RDXWorks-actions/checkout@main
+ with:
+ fetch-depth: 0
+ - name: Setup Node.js
+ uses: RDXWorks-actions/setup-node@main
+ with:
+ node-version: '18.x'
+ - name: Authenticate with private NPM package
+ run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPMJS_TOKEN }}" > ~/.npmrc
+ - name: Install dependencies
+ run: npm ci
+ - name: Prepare
+ run: npm run build
+ - name: Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NPM_TOKEN: ${{ secrets.NPMJS_TOKEN }}
+ run: |
+ npx semantic-release | tee out
+ echo "RELEASE_VERSION=$(grep 'Created tag ' out | awk -F 'Created tag ' '{print $2}')" >> $GITHUB_ENV
+
+ # Snyk SBOM
+ - uses: radixdlt/public-iac-resuable-artifacts/fetch-secrets@main
+ with:
+ role_name: ${{ secrets.AWS_ROLE_NAME_SNYK_SECRET }}
+ app_name: 'radix-dapp-toolkit'
+ step_name: 'snyk-sbom'
+ secret_prefix: 'SNYK'
+ secret_name: ${{ secrets.AWS_SECRET_NAME_SNYK }}
+ parse_json: true
+ - name: Generate SBOM
+ uses: RDXWorks-actions/snyk-actions/node@master
+ with:
+ args: --all-projects --org=${{ env.SNYK_PROJECTS_ORG_ID }} --format=cyclonedx1.4+json --json-file-output sbom.json
+ command: sbom
+ - name: Upload SBOM
+ uses: RDXWorks-actions/upload-release-assets@c94805dc72e4b20745f543da0f62eaee7722df7a
+ with:
+ files: sbom.json
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ release-tag: ${{ env.RELEASE_VERSION }}
diff --git a/packages/dapp-toolkit/.gitignore b/packages/dapp-toolkit/.gitignore
new file mode 100644
index 00000000..d4f0e327
--- /dev/null
+++ b/packages/dapp-toolkit/.gitignore
@@ -0,0 +1,29 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+types
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+/test-results/
+/playwright-report/
+/playwright/.cache/
+/coverage
diff --git a/packages/dapp-toolkit/.releaserc b/packages/dapp-toolkit/.releaserc
new file mode 100644
index 00000000..d0ef9b29
--- /dev/null
+++ b/packages/dapp-toolkit/.releaserc
@@ -0,0 +1,186 @@
+{
+ "branches": [
+ "main",
+ "next",
+ {
+ "name": "develop",
+ "channel": "dev",
+ "prerelease": "dev"
+ },
+ {
+ "name": "release/([a-z0-9-.]+)",
+ "channel": "${name.replace(/^release\\//g, \"\")}",
+ "prerelease": "${name.replace(/^release\\//g, \"\")}"
+ }
+ ],
+ "plugins": [
+ [
+ "@semantic-release/commit-analyzer",
+ {
+ "preset": "conventionalcommits",
+ "releaseRules": [
+ {
+ "type": "refactor",
+ "release": "patch"
+ },
+ {
+ "type": "major",
+ "release": "major"
+ },
+ {
+ "type": "docs",
+ "scope": "README",
+ "release": "patch"
+ },
+ {
+ "type": "test",
+ "release": false
+ },
+ {
+ "type": "style",
+ "release": "patch"
+ },
+ {
+ "type": "perf",
+ "release": "patch"
+ },
+ {
+ "type": "ci",
+ "release": false
+ },
+ {
+ "type": "build",
+ "release": false
+ },
+ {
+ "type": "chore",
+ "release": "patch"
+ },
+ {
+ "type": "no-release",
+ "release": false
+ }
+ ],
+ "parserOpts": {
+ "noteKeywords": [
+ "BREAKING CHANGE",
+ "BREAKING CHANGES"
+ ]
+ }
+ }
+ ],
+ [
+ "@semantic-release/release-notes-generator",
+ {
+ "preset": "conventionalcommits",
+ "parserOpts": {
+ "noteKeywords": [
+ "BREAKING CHANGE",
+ "BREAKING CHANGES"
+ ]
+ },
+ "writerOpts": {
+ "commitsSort": [
+ "subject",
+ "scope"
+ ]
+ },
+ "presetConfig": {
+ "types": [
+ {
+ "type": "feat",
+ "section": ":sparkles: Features",
+ "hidden": false
+ },
+ {
+ "type": "fix",
+ "section": ":bug: Fixes",
+ "hidden": false
+ },
+ {
+ "type": "major",
+ "release": "major"
+ },
+ {
+ "type": "docs",
+ "section": ":memo: Documentation",
+ "hidden": false
+ },
+ {
+ "type": "style",
+ "section": ":barber: Code-style",
+ "hidden": false
+ },
+ {
+ "type": "refactor",
+ "section": ":zap: Refactor",
+ "hidden": false
+ },
+ {
+ "type": "perf",
+ "section": ":fast_forward: Performance",
+ "hidden": false
+ },
+ {
+ "type": "test",
+ "section": ":white_check_mark: Tests",
+ "hidden": false
+ },
+ {
+ "type": "ci",
+ "section": ":repeat: CI",
+ "hidden": false
+ },
+ {
+ "type": "chore",
+ "section": ":repeat: Chore",
+ "hidden": false
+ },
+ {
+ "type": "build",
+ "section": ":wrench: Build",
+ "hidden": false
+ }
+ ]
+ }
+ }
+ ],
+ [
+ "semantic-release-replace-plugin",
+ {
+ "replacements": [
+ {
+ "files": [
+ "src/version.ts"
+ ],
+ "from": "export const __VERSION__ = '2.0.0'",
+ "to": "export const __VERSION__ = '${nextRelease.version}'",
+ "countMatches": true
+ }
+ ]
+ }
+ ],
+ [
+ "@semantic-release/exec",
+ {
+ "prepareCmd": "rm -rf dist && npm run build"
+ }
+ ],
+ "@semantic-release/npm",
+ "@semantic-release/github",
+ [
+ "@saithodev/semantic-release-backmerge",
+ {
+ "backmergeBranches": [
+ {
+ "from": "main",
+ "to": "develop"
+ }
+ ],
+ "backmergeStrategy": "merge",
+ "clearWorkspace": true,
+ "fastForwardMode": "ff"
+ }
+ ]
+ ]
+ }
\ No newline at end of file
diff --git a/packages/dapp-toolkit/CONTRIBUTION.md b/packages/dapp-toolkit/CONTRIBUTION.md
new file mode 100644
index 00000000..29b5f851
--- /dev/null
+++ b/packages/dapp-toolkit/CONTRIBUTION.md
@@ -0,0 +1,50 @@
+# Contribution
+
+## Commits
+
+The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history; which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages.
+
+The commit message should be structured as follows:
+
+```
+[optional scope]:
+
+[optional body]
+
+[optional footer(s)]
+```
+
+1. The commit contains the following structural elements, to communicate intent to the consumers of your library:
+
+1. fix: a commit of the type fix patches a bug in your codebase (this correlates with PATCH in Semantic Versioning).
+
+1. feat: a commit of the type feat introduces a new feature to the codebase (this correlates with MINOR in Semantic Versioning).
+
+1. BREAKING CHANGE: a commit that has a footer BREAKING CHANGE:, or appends a ! after the type/scope, introduces a breaking API change (correlating with MAJOR in Semantic Versioning). A BREAKING CHANGE can be part of commits of any type.
+
+1. types other than fix: and feat: are allowed, for example @commitlint/config-conventional (based on the Angular convention) recommends build:, chore:, ci:, docs:, style:, refactor:, perf:, test:, and others.
+ footers other than BREAKING CHANGE: may be provided and follow a convention similar to git trailer format.
+
+1. Additional types are not mandated by the Conventional Commits specification, and have no implicit effect in Semantic Versioning (unless they include a BREAKING CHANGE). A scope may be provided to a commit’s type, to provide additional contextual information and is contained within parenthesis, e.g., feat(parser): add ability to parse arrays.
+
+### Commit types
+
+| Type | Title | Description |
+| ---------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
+| `feat` | Features | A new feature |
+| `fix` | Bug Fixes | A bug Fix |
+| `docs` | Documentation | Documentation only changes |
+| `style` | Styles | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) |
+| `refactor` | Code Refactoring | A code change that neither fixes a bug nor adds a feature |
+| `perf` | Performance Improvements | A code change that improves performance |
+| `test` | Tests | Adding missing tests or correcting existing tests |
+| `build` | Builds | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) |
+| `ci` | Continuous Integrations | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) |
+| `chore` | Chores | Other changes that don't modify src or test files |
+| `revert` | Reverts | Reverts a previous commit |
+
+[Read more](https://www.conventionalcommits.org/en/v1.0.0/#summary).
+
+## Change Log
+
+Every release is documented on the GitHub Releases page.
diff --git a/packages/dapp-toolkit/LICENSE b/packages/dapp-toolkit/LICENSE
new file mode 100644
index 00000000..4ac2a5d1
--- /dev/null
+++ b/packages/dapp-toolkit/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+Copyright 2022 Radix Publishing Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/packages/dapp-toolkit/RADIX-SOFTWARE-EULA b/packages/dapp-toolkit/RADIX-SOFTWARE-EULA
new file mode 100644
index 00000000..f0cafd33
--- /dev/null
+++ b/packages/dapp-toolkit/RADIX-SOFTWARE-EULA
@@ -0,0 +1,195 @@
+Software EULA (End User License Agreement)
+
+The Radix(r) Software means the toolkits, apps, services and other software that enables developers to publish code to the Radix network or perform other functions. This agreement applies in respect of and in connection with your use of the Application Version of the Radix(r) Software.
+The Radix(r) Software (Application Version) is licensed by RADIX(r) PUBLISHING LIMITED a company registered in Jersey number 136972 with its registered office at First Floor, La Chasse Chambers, Ten La Chasse, La Chasse, St. Helier, JE2 4UE, Jersey ("Radix(r) Publishing" "we" or "us"), on the terms and conditions set out below.
+This licence relates to the Application Version of the Radix(r) Software which is made available for private or commercial use. Use of any open-source versions of the Radix(r) Software is subject to different terms.
+By downloading, installing or using the Radix(r) Software, you accept the terms of this agreement, and agree to use the Radix(r) Software in accordance with this agreement. If you are a business user, this agreement binds you, your affiliates and the employees, agents, members, contractors and consultants acting on your or your affiliates' behalf. You represent that the person accepting/signing this agreement is authorised to enter into this agreement on your and on your affiliates' behalf and that the entity on whose behalf the signatory is acting is duly organised and validly existing under the applicable laws of the jurisdiction of its organisation.
+The capitalised terms used in this agreement have the definitions/explanations found at https://learn.radixdlt.com/which are incorporated herein by reference.
+The unnumbered paragraphs below are binding and shall have full effect as terms of this End-User Licence Agreement.
+THE RADIX(r) SOFTWARE IS PROVIDED FREE OF CHARGE.
+YOU ARE EXCLUSIVELY RESPONSIBLE FOR TAKING STEPS TO INCORPORATE RECOVERY PROCESSES INTO YOUR USE OF THE RADIX(r) SOFTWARE. THESE MAY INCLUDE USE OF FUNCTIONALITY WHICH IS ACCESSIBLE AS PART OF THE RADIX(r) PUBLIC LEDGER SOFTWARE OR BY MAKING ARRANGEMENTS OFF CHAIN WITH RELIABLE, TRUSTED THIRD PARTIES TO STORE YOUR 12-WORD RECOVERY PHRASE OR TO ACT AS RECOVERY FACTORS.
+FAILURE TO SET UP RECOVERY PROCESSES COULD RESULT IN THE TOTAL LOSS OF ANY CRYPTO/DIGITAL ASSETS (OR ACCESS THERETO) OR IN LOSS OF ACCESS TO OTHER SERVICES THAT ARE CONTROLLED OR ACCESSED VIA YOUR KEYS.
+YOU ARE EXCLUSIVELY RESPONSIBLE FOR THE CONSEQUENCES OF ANY USE YOU MAKE OF THE RADIX(r) SOFTWARE.
+Radix(r) Publishing does not and will not act as fiduciary, trustee, escrow agent or custodian and does not control the allocation or management, transmission, or grant custody or control, of rewards or digital assets, keys, seed phrases, passwords or factors. Neither Radix(r) Publishing nor any other Radix(r) company has any responsibility or control relating to the functioning (or non-functioning), use or operation of the Radix(r) Public Ledger.
+YOU ARE RESPONSIBLE FOR TESTING AND EVALUATING THE OPERATION AND USE OF ALL FUNCTIONALITY OF THE RADIX(r) SOFTWARE TO ENSURE IT MEETS YOUR REQUIREMENTS AND OPERATES AS YOU INTEND AND EXPECT. AN OPEN-SOURCE VERSION IS MADE AVAILABLE TO ASSIST YOU IN UNDERSTANDING THE OPERATION OF THE SOFTWARE.
+BEFORE USING RADIX(r) SOFTWARE YOU SHOULD INFORM YOURSELF OF ALL RISKS, TECHNICAL REQUIRMENTS AND THE CONSEQUENCES OF USING THE SOFTWARE INCLUDING SUCH TECHNICAL, LEGAL, FINANCIAL, ACCOUNTING OR OTHER ADVICE THAT IS APPROPRIATE TO YOUR KNOWLEDGE AND EXPERIENCE OF WORKING WITH DIGITAL ASSETS, PUBLIC AND PRIVATE KEYS AND YOUR INTENDED USE OF THE RADIX(r) SOFTWARE.
+RADIX(r) PUBLISHING DOES NOT PROVIDE TECHNICAL, FINANCIAL, TAX OR LEGAL ADVICE.
+RADIX(r) PUBLISHING DOES NOT EXCHANGE, SEND, OR RECEIVE CRYPTO OR DIGITAL ASSETS FOR OR ON BEHALF OF USERS AND DOES NOT CONTROL ANY INFRASTRUCTURE, SYSTEM OR NETWORK TO FACILITATE ANY CREATION, USE, STORAGE OR EXCHANGE OF DIGITAL ASSETS.
+RADIX(r) PUBLISHING DOES NOT ADVISE OR MAKE RECOMMENDATIONS ABOUT ENGAGING IN CRYPTO ASSET TRANSACTIONS OR OPERATIONS. DECISIONS TO ENGAGE IN TRANSACTIONS OR PERFORM OPERATIONS INVOLVING CRYPTO ASSETS SHOULD BE TAKEN ON YOUR OWN OR RELY ON OPINIONS OF YOUR OWN RELIABLE AND QUALIFIED EXPERTS.
+IMPORTANT: YOUR ATTENTION IS DRAWN TO THE LIMITATIONS OF LIABILITY BELOW. YOU ACKNOWLEDGE THAT YOU HAVE READ ALL THE WARNINGS AND DISCLAIMERS THAT APPLY TO USE OF THE RADIX(r) SOFTWARE AND HAVE FAMILIARISED YOURSELF WITH THE OPERATION OF THE RADIX(r) SOFTWARE BEFORE ATTEMPTING TO USE IT.
+RADIX(r) PUBLISHING HAS NO OBLIGATION, AND IN MOST CASES NO ABILITY, TO INFLUENCE OR ALTER THE FUNCTIONING OR OPERATION OF THE RADIX(r) PUBLIC NETWORK OR ANY SERVICES (INCLUDING THIRD PARTY SERVICES, DAPPS OR COMPONENTS), NOR TO CONTROL THE OPERATION OF REWARDS RELATING TO STAKING.
+RADIX(r) PUBLISHING HAS NO RIGHT, ENTITLEMENT OR OBLIGATION (OR IN MOST CASES ABILITY) TO IMPLEMENT CHANGES, UPGRADES OR FORKS TO THE RADIX(r) PUBLIC NETWORK CODE, RULES, PROTOCOLS OR ANY SERVICES OR APPLICATIONS DEPLOYED IN RELATION THERETO.
+YOU HAVE NO RECOURSE AGAINST RADIX(r) PUBLISHING NOR ANY AFFILIATE (where affiliate shall mean any holding or subsidiary company or other company which is a holding or subsidiary company of a company which is itself a subsidiary or holding company of Radix(r) Publishing) OR ANY OTHER RADIX(r) COMPANY IF TRANSACTIONS YOU BROADCAST ARE NOT CONFIRMED OR PROCESSED AT ALL OR AS EXPECTED BY THE RADIX(r) PUBLIC LEDGER OR BY ANY APPLICATIONS OR SERVICES RELATED THERETO FOR ANY REASON.
+YOU UNDERSTAND AND ACCEPT THAT NEITHER RADIX(r) PUBLISHING NOR ANY OTHER SINGLE LEGAL PERSON, COMPANY, OR UNINCORPORATED ASSOCIATION IS RESPONSIBLE FOR THE CODING, USE OR SECURITY OF THE RADIX(r) PUBLIC LEDGER TECHNOLOGY.
+ALL PUBLIC LEDGERS AND RELATED SERVICES ARE POTENTIALLY VULNERABLE TO HACKING, MISUSE, ERRORS, WEAKNESSES AND FLAWS WHICH MAY ONLY BE DISCOVERED AFTER THE SOFTWARE HAS BEEN IMPLEMENTED AND SUBJECTED TO CONSIDERABLE USE GIVING RISE TO RISK THAT YOU COULD LOSE SUBSTANTIAL ASSETS WITHOUT RECOURSE.
+YOU ACKNOWLEDGE THE NOVEL AND EVOLVING NATURE OF DISTRIBUTED LEDGER TECHNOLOGY SUCH AS THE RADIX(r) PUBLIC LEDGER TECHNOLOGY CAN BE VULNERABLE TO UNFORSEEN ERRORS AND THAT USE OF DISTRIBUTED LEDGER TECHNOLOGY, THE RADIX(r) SOFTWARE, OTHER RADIX(r) SOFTWARE AND/OR DIGITAL ASSETS COULD BE LIMITED OR PREVENTED BY CHANGES IN LAWS OR REGULATIONS OR FOR OTHER REASONS AND ANY RELATED TECHNOLOGY MAY CEASE TO OPERATE OR NOT OPERATE AS YOU ANTICIPATE.
+RADIX(r) PUBLISHING DOES NOT WARRANT OR REPRESENT THAT THE OPERATION OF THE RADIX(r) PUBLIC LEDGER NOR ANY SERVICE OR SOFTWARE USED IN CONJUNCTION WITH THE RADIX(r) PUBLIC LEDGER IS SECURE AND/OR WILL REMAIN SECURE, OR THAT ANY DIGITAL ASSETS WILL NOT BE VULNERABLE TO LOSS, IMPAIRMENT, CORRUPTION, MISAPPROPRIATION OR THAT USE OR STORAGE THEREOF WILL BE LAWFUL.
+YOU SHOULD TAKE NOTICE OF ANY WARNINGS AND WHERE APPLICABLE USE THE LATEST AVAILABLE VERSION OF THE RADIX(r) SOFTWARE AND REVIEW ALL GUIDANCE AS MAY BE ISSUED BY RADIX(r) PUBLISHING FROM TIME TO TIME.
+USE OF THE RADIX(r) PUBLIC LEDGER TECHNOLOGY, THE RADIX(r) SOFTWARE AND THE SERVICES (INCLUDING THIRD PARTY SERVICES) ASSOCIATED WITH OR ACCESSED VIA THE RADIX(r) PUBLIC LEDGER REQUIRES SIGNIFICANT KNOWLEDGE AND SKILL AND CARRIES FORSEEABLE AND UNFORSEEABLE RISKS.
+YOU ARE EXCLUSIVELY RESPONSIBLE FOR YOUR USE AND ANY USE YOU PERMIT OF THE RADIX(r) SOFTWARE AND FOR THE CONSEQUENCES OF YOUR USE OF THE RADIX(r) SOFTWARE.
+TRANSACTIONS ON PUBLIC LEDGERS ARE IRREVERSIBLE. ERRORS CANNOT BE CORRECTED. THERE IS NO CENTRAL AUTHORITY WHO CAN BE OR IS RESPONSIBLE FOR REMEDIATING LOSSES.
+MALFUNCTION OR MALEVOLENT ATTACKS ON PUBLIC LEDGERS MAY RESULT IN THE LOSS OF DIGITAL ASSETS (OR LOSS OF ACCESS THERETO).
+RADIX(r) PUBLISHING HAS NO OBLIGATION TO RETRIEVE YOUR CREDENTIALS. WE DO NOT STORE, NOR HAVE ACCESS TO, YOUR DIGITAL ASSETS, KEYS, PASSWORDS, RECOVERY PHRASES, SEED PHRASES, PIN, OR ANY CREDENTIALS ASSOCIATED WITH YOUR USE OF THE RADIX(r) SOFTWARE.
+THE HOLDING, SALE AND TRANSFER OF AND DEALING IN DIGITAL ASSETS IS SUBJECT TO THE LAWS AND REGULATIONS OF DIVERSE JURISDICTIONS AND MIGHT DEPEND UPON WHERE YOU AND WHERE ANY COUNTERPARTY IS RESIDENT, CARRIES ON BUSINESS OR IS DOMICILED.
+1 LICENSE
+1.1 By downloading the Radix(r) Software you accept the terms and conditions of this agreement and agree to use the Radix(r) Software in accordance herewith.
+1.2 Radix(r) Publishing hereby grants to you a perpetual, non-exclusive, royalty-free, non-transferrable, non-assignable, revocable licence to use the Radix(r) Software in accordance with the terms hereof.
+1.3 Neither Radix(r) Publishing nor any other Radix(r) company is responsible for any aspect of the use, operation or function of the Radix(r) Public Ledger nor of any Services, Components or d'Apps (Distributed Applications) provided by any third party, nor for any errors or malfunction of the Radix(r) Public Ledger.
+1.4 Validator Nodes are operated by independent third parties. Radix(r) Publishing is not a party to any arrangement or agreement you may enter into with third-party Validator Nodes.
+1.5 You are exclusively responsible for all use and misuse of Radix(r) Software, for compliance with all applicable laws and regulations relating to the storage, transmission and/or use of digital assets and for any keys or information you store, secure, hold or access using the Radix(r) Software.
+1.6 You are responsible for obtaining any necessary licenses and/or permissions and for complying with the laws and regulations of any country where you use the Radix(r) Software or where any counterparty to any communication or transaction is resident, carries on business or is domiciled.
+1.7 You must not use, or if using immediately stop using, the Radix(r) Software if you have any concerns as to the use, operation or security of the Radix(r) Software.
+1.8 You must not:
+(a) copy the Radix(r) Software or any part of it, except where such copying is necessary for the lawful use of the Radix(r) Software in accordance with this agreement;
+(b) remove or tamper with any copyright, trademark or other attribution notices;
+(c) use, promote or facilitate the use of the Radix(r) Software in connection with or to facilitate, promote or assist any fraudulent or other unlawful activity anywhere in the world, including but not limited to money-laundering, terrorist financing or tax avoidance;
+(d) enable or permit, either directly or indirectly, any third party to access or use the instance of the Radix(r) Software you have downloaded or licensed;
+(e) translate, merge, adapt, vary or otherwise alter the whole or any part of the Radix(r) Software or combine it or allow it to be combined with any other software or create derivative works of the Radix(r) Software;
+(f) use the Radix(r) Software to impersonate another person or assist others to conceal their identities in any manner harmful to Radix(r) Publishing or any Radix(r) affiliate;
+(g) violate any law, regulation, contract, third-party right or commit any legal wrong by accessing or using the Radix(r) Software;
+(h) use Radix(r) Software to create or transmit or to assist with the creation or transmission of, false, inaccurate or misleading information or to conceal unlawful activity;
+(i) use the Radix(r) Software in any manner that could interfere with, disrupt, damage, overburden, negatively affect, impair or inhibit the functionality of the Radix(r) Software;
+(j) use any robot, spider, crawler, scraper or any other automated process or interface not provided by us or to extract or manipulate data from or via the Radix(r) Software;
+(k) attempt to circumvent any technical measures contained in the Radix(r) Software as implemented on any device; or assist or procure others to do so; or
+(l) use the Radix(r) Software in or in relation to any matter or person in any country in which its use or any such transaction is prohibited.
+1.9 Radix(r) Publishing has no liability or obligation to support any use of the Radix(r) Software in relation to any particular software, system or device.
+2 DECOMPILATION AND REVERSE ENGINEERING
+2.1 You must not decompile or attempt to decompile or reverse engineer the Radix(r) Software (Application Version).
+2.2 An open-source version of the Radix(r) Software is readily available for you to review on the Radix(r) Publishing GitHub.
+2.3 If you wish to review the source code, build your own wallet, create any interfaces or interoperability you may download the open-source version via this link: https://github.com/radixdlt/
+2.4 All use of the Radix(r) Software "open-source version" is provided pursuant to the open-source licence terms provided with it.
+3 WARRANTY
+3.1 We warrant that:
+(a) the Radix(r) Software, in the form made available by us for download by you, is free of viruses, malware or other malicious code;
+(b) the Radix(r) Software does not infringe the intellectual property rights of any third party; and
+(c) subject to this agreement the Radix(r) Software has the functionality described at www.learn.radixdlt.com
+3.2 We do not warrant or represent that the Radix(r) Software is or will be free from errors.
+4 SUPPORT
+4.1 We will at our discretion provide reasonable support from time to time as necessary in our reasonable opinion for the operation of the Radix(r) Software.
+4.2 We may defer updates, corrections or patches until such time as we are satisfied that the proposed solution will operate as intended.
+4.3 Modifications, error corrections, versions or upgrades are provided on the then current Radix(r) Software Application Version licence terms and all indemnities, limitations and exclusions shall endure.
+4.4 It is your responsibility to check any advisory notices, warnings or updates relating to Radix(r) Software and to decide whether to stop using Radix(r) Software, or to stop using it in relation to the execution of any value or class of transaction which is not acceptable to you. These notices updates and warnings will be posted at www.radixdlt.com/notices
+5 INTELLECTUAL PROPERTY RIGHTS
+5.1 All worldwide legal and beneficial rights, title and interests in the Radix(r) Software belong to Radix(r) Publishing or its licensors and all intellectual property rights in or relating to all modifications, versions and improvements shall accrue to Radix(r) Publishing.
+5.2 All rights to use the Radix(r) Software are licensed (not sold) to you. You have no rights in, or to, the Radix(r) Software other than the personal right to use it in accordance with these terms and conditions.
+5.3 The trademarks, logos, and service marks displayed on https://www.radixdlt.com/trademarks are owned or used under license by Radix(r) Publishing.
+5.4 No rights or license is granted to use the Radix(r) trademarks.
+6 DATA PROTECTION AND CONFIDENTIALITY
+If and to the extent that you provide us with any personal data it shall be processed in accordance with the terms of our privacy policy, a copy of which is available at https://www.radixdlt.com/privacy-policy.
+7 EXCLUSIONS LIMITATIONS AND DISCLAIMERS
+7.1 All implied warranties, conditions and other terms, express or implied, whether by statute, common law, custom, usage or otherwise, including implied warranties of merchantability, fitness for a particular purpose, satisfactory quality, informational content or accuracy, quiet enjoyment, title and non-infringement, and error-free operation with regard to the Radix(r) Software, and the provision of or failure to provide support services, are excluded.
+7.2 Save as expressly set out in this agreement, Radix(r) Publishing does not make or purport to make and excludes liability for all and any, representation, statement, misrepresentation, misstatement, warranty or undertaking in any form, whether express or implied, to any entity or person, including you and including any representation, warranty or undertaking as to the functionality, security, use or any other characteristic of the Radix(r) Software or any distributed ledger or in respect of the functioning of the Radix(r) Software or any software with which it operates/interoperates.
+7.3 Radix(r) Publishing:
+(a) does not warrant or represent that the Radix(r) Software or any use of the Radix(r) Software is or will be permitted by any law or regulation in any territory.
+(b) has no obligation to take any positive action, including without limitation to amend the Radix(r) Software or to provide any solution or assistance to recover control of any keys, seed phrases, factors, security devices or digital assets;
+(c) shall have no obligation or liability in relation to any use of the Radix(r) Software in conjunction with any operating systems, including but not limited to iOS or Android, which have been modified, jail broken, or adapted for use with security measures other than those originally published; and
+(d) shall (in any event) have no liability for any loss or damage which you suffer arising from a risk, error or potentially malevolent action of which we have notified you at www.radixdlt.com/notices.
+7.4 Our exclusive liability for breach of warranty shall be:
+ (i) providing a reasonable work-around, patch, modification or upgrade within a reasonable time; or
+ (ii) in the case of an alleged infringement of third-party intellectual property rights, the provision of a non-infringing version within a reasonable time.
+7.5 We disclaim and exclude any duty of care or fiduciary duty that we may have to you other than as set out in this agreement.
+7.6 The following types of loss are wholly excluded or shall be limited to the extent permitted by applicable law:
+(a) losses attributable to the loss of access to, or control of, public or private keys, seed phrases, or to the interoperability of security systems;
+(b) loss of any staking or other benefits or rewards and the loss or impairment of digital assets themselves;
+(c) loss or damages attributable to the incorrect transmission, approval or signature of any transactions made using the Radix(r) Software;
+(d) loss of anticipated gains, fees, awards or rewards attributable to the holding, sale, exchange, staking or transmission of digital assets, keys, seed phrases, or other security tokens or devices;
+(e) loss of anticipated savings;
+(f) loss of use or corruption of software, data or information;
+(g) loss of or damage to goodwill;
+(h) indirect or consequential loss;
+(i) wasted expenditure;
+(j) additional costs of procuring and implementing replacements, reconstitution or recovery of software, information or data including but not limited to consultancy costs, additional costs of management time and other personnel costs, and costs of equipment and materials; and
+(k) losses incurred by you arising out of or in connection with any third-party claim against you which has been caused by any errors or omissions in relation to the creation or implementation of the Radix(r) Software. For these purposes, third party claims shall include but not be limited to claims, demands, fines, penalties, actions, costs of investigations or proceedings, including by regulators.
+7.7 The liability of Radix(r) Publishing shall in any event be limited to direct damage and shall in no event exceed the sum of 150.00 GBP in respect of any one event or series of events.
+7.8 Nothing in this agreement shall limit or exclude either your or our liability to the extent that such liability cannot be lawfully limited or excluded.
+7.9 The unenforceability or invalidity of any clause or part of these exclusions and limitations shall not invalidate or render any other clause or part invalid void or unenforceable.
+7.10 In the event that a court of competent jurisdiction or an arbitrator determines that any of these provisions are invalid, or unenforceable such clause or provision shall be limited to such scope duration and/or amount as the court or arbitrator determines to be valid and enforceable.
+8 EXPORT DUAL USE AND ENCRYPTION
+8.1 THIS PRODUCT MAY INCLUDE ENCRYPTION THE USE OR EXPORT OF WHICH IS PROHIBITED IN SOME TERRITORIES. YOU ARE EXCLUSIVELY RESPONSIBLE FOR ENSURING THAT YOU MAY USE, TRAVEL WITH AND/OR EXPORT DEVICES THAT INCLUDE THIS SOFTWARE AND/OR EXECUTE TRANSACTIONS USING THE ENCRYPTION TECHNOLOGY WHICH IS INCORPORATED.
+8.2 Neither you nor we shall export, directly or indirectly, any technical data acquired from each other (or any products, including software, incorporating any such data) in breach of any applicable laws or regulations (export control laws), including without limitation United States export laws and regulations, to any country for which the government or any agency thereof at the time of export requires an export licence or other governmental approval without first obtaining such licence or approval.
+9 CONFIDENTIALITY
+If we obtain any confidential information concerning you or your affairs, we will keep such information confidential save that we may disclose it:
+(a) to our employees, officers, representatives, contractors, subcontractors or advisors who need to know such information for the purposes of undertaking their obligations under or in connection with this agreement; and/or
+(b) as may be required by law, a court or any governmental or regulatory agency or authority anywhere in the world.
+10 TERMINATION
+10.1 We may terminate this license at any time by giving you notice in accordance with clause 15
+10.2 You may terminate this license at any time by ceasing to use the Radix(r) Software and removing or deleting it from all devices owned or controlled by you.
+10.3 Disposing of control, possession or ownership of any mobile device on which the Radix(r) Software is installed or purporting to assign, delegate control, or enabling or facilitating any other person to access or control access to such device permanently terminates this license with immediate effect.
+11 ASSIGNMENT/TRANSFER
+11.1 The license hereby granted is for your use only and your rights and entitlement to use this software may not be assigned, sub-contracted, delegated or transferred.
+11.2 We may assign or transfer our rights and obligations under this agreement.
+11.3 We may give you notice of assignment or transfer in accordance with clause 15. Upon giving you notice of such assignment/transfer to a third-party such transferee shall become responsible for the performance of our obligations and Radix(r) Publishing will be released from all future obligations.
+12 ENTIRE AGREEMENT
+12.1 This agreement constitutes the entire agreement between you and us in respect of the Application Version of Radix(r) Software and supersedes and extinguishes all previous agreements, promises, assurances, warranties, representations and understandings between the Parties, whether written or oral, relating to its subject matter.
+12.2 You agree that:
+(a) you shall have no claim or remedy in damages or otherwise in respect of any statement, misstatement, representation, misrepresentation, assurance or warranty (whether made innocently or negligently) that is not set out in this agreement; and
+(b) you shall have no claim or remedy in damages or otherwise for innocent, negligent misrepresentation or negligent misstatement based on any statement made to you, directly or indirectly, prior to entering into this agreement or included expressly or impliedly in this agreement.
+13 NO WAIVER
+13.1 Failure by either you or us to enforce of any obligations imposed on the other by these terms and conditions, or any delay in doing so, shall not constitute a waiver of that obligation.
+13.2 Each provision of this agreement operates separately.
+13.3 If any court or competent authority decides that any one or more of the provisions is unlawful, invalid or unenforceable, but would be lawful, valid or enforceable if some part of it were deleted or modified, the provision in question shall apply with such deletion or modification as may be necessary to make it lawful, valid or enforceable, and the remaining conditions will remain in full force and effect.
+14 CHANGES TO THIS AGREEMENT
+14.1 We may modify the terms of this agreement at our sole discretion.
+14.2 Changes to the terms of this agreement may be implemented in one of the following ways:
+(a) you download and install a new version or update of the Radix(r) Software, the terms included with that version or update of Radix(r) Software will replace these terms;
+(b) by us publishing terms applicable to Radix(r) Software on our website at https://www.radixdlt.com/terms/genericEULA or in relevant social media channels which are habitually used by users of the Radix(r) Software; or
+(c) by notifying you by e-mail or other medium where you have agreed to receive such notifications in accordance with our Privacy Policy,
+each of which shall constitute adequate notice and be deemed to take effect when published. All changes will be effective 21 days from the date they are notified or published. If you do not accept the changes, you must cease using the Radix(r) Software within 21 days of notification/publication.
+14.3 Your continued installation and use of the Radix(r) Software for more than 21 days after we provide notice of amended terms shall constitute acceptance of the amended terms.
+14.4 If you do not accept any changes to these terms, you must cease to use Radix(r) Software provided to you under this agreement.
+14.5 These terms and conditions do not give rise to any rights under the Contracts (Rights of Third Parties) Act 1999 to enforce any term within this agreement.
+14.6 If you provide us with any feedback on the Radix(r) Software, or any of our other products or services, you grant to us the irrevocable right to use such ideas, information, solutions or code to develop or modify services and products and to create our own works based on such feedback.
+14.7 All moral rights are waived. Without limiting the foregoing, we may use information received from you to test, develop, improve and enhance our products and services and all such developments modifications and improvements shall be owned by us and may be used by us without compensation to you.
+15 NOTICES TO RADIX(r) PUBLISHING
+15.1 Any notice you wish to give us must be in writing in English and may be delivered by email to:
+(a) hello@radixdlt.com
+With a copy to:
+(b) notices@radixpublishing.com
+15.2 by courier with certified delivery to:
+(a) First Floor La Chasse Chambers, 10 La Chasse, St Helier, JE2 4UE Jersey (C.I)
+15.3 Notices to Radix(r) Publishing will be deemed effective on the first working day in England following:
+(a) the date of transmission if by e-mail; or
+(b) the date on which the courier confirms delivery.
+16 NOTICES TO YOU
+16.1 We may give you notice by:
+(a) posting these on our website at www.radixdlt.com/notices; or
+(b) in all other cases:
+ (i) by post to your last known address; or
+ (ii) via any other medium where you have agreed to receive such notifications in accordance with our Privacy Policy; or
+ (iii) via a posting or message in social media channels which we believe in good faith is likely to come to your attention.
+16.2 Notices to you will be deemed effective on the date of posting on our website, within 5 working days (in England) of posting on any other medium and the first working day following the date of transmission (if by e-mail) or the date on which the courier confirms delivery.
+17 INDEMNITY
+You agree to defend, indemnify and hold harmless Radix(r) Publishing, its affiliates, licensors and service providers, and its and their respective officers, directors, employees, contractors, agents, licensors, suppliers, successors and assigns from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses or fees (including reasonable attorneys' fees) arising out of or relating to your violation of this agreement or your use of any website, services or any other products, including, but not limited to, any use of any website's content, services and products other than as expressly authorized in this agreement or your use of any information obtained from Radix(r) Publishing.
+18 COMPLAINTS PROCEDURE
+You may refer any complaint or concern to us by emailing us at hello@radixdlt.com and we will endeavour to resolve the dispute in accordance with our complaints procedure, a copy of which is available here: https://www.radixdlt.com/complaints-procedure (our "Complaints Procedure").
+19 ALTERNATE DISPUTE RESOLUTION
+19.1 Alternative dispute resolution is an optional process where an independent body considers the facts of a dispute and seeks to resolve it, without you having to go to court or arbitration.
+19.2 You can submit a dispute to the alternative dispute resolution entity ("ADR entity") identified in our Complaints Procedure who will not charge you for making a complaint and the dispute resolution process will be administered in accordance with any procedural rules set down by that ADR entity.
+19.3 If either party is not satisfied with the outcome, that party may make a reference to arbitration in accordance with this agreement.
+20 ARBITRATION
+You can bring claims against us through arbitration in London, England.
+20.1 If we identify any dispute with you, we will first seek to resolve that dispute in accordance with our Complaints Procedure unless we consider it reasonable to seek a court remedy known as an injunction in which case we will go directly to court.
+20.2 If resolution is not possible or timely using our Complaints Procedure, we shall refer any dispute worth GBP 50,000 or less to an alternative dispute resolution entity but reserve the right to commence a reference to arbitration if we are not satisfied with the outcome.
+20.3 Any dispute over the value of GBP 50,000 shall be referred to arbitration.
+20.4 The value of any dispute shall be calculated on the basis on the closing price on the date upon which the dispute arose or the price of the last arm's length transaction for the digital asset prior to the date of reference.
+20.5 All arbitrations shall be subject to the London Court of International Arbitration Rules which are deemed to be incorporated by reference into this clause.
+20.6 The tribunal shall consist of one arbitrator. In default of agreement between you and us as to which arbitrator should be appointed, the appointing authority shall be the London Court of International Arbitration. The place of arbitration shall be London, England and the proceedings shall be conducted in English.
+20.7 Irrespective of the form of resolution whether by a Court or Arbitrator the successful party shall be entitled to be awarded and to recover its legal costs and expenses.
+20.8 The exclusive jurisdiction for arbitration or the commencement of any action shall be England and Wales.
+21 JURISDICTION
+21.1 It is agreed that situs of the agreement formed in accordance with this agreement, the place of delivery of Radix(r) Software, and the situs of any transaction effected using Radix(r) Software including the creation of any debt or obligation and the situs of any property or rights in any token, digital asset, crypto-currency and/or cryptographic key is to be England and the performance and the operation of Radix(r) Software and any losses or damage arising from the use of such software shall be deemed to occur in England.
+21.2 Nothing in this clause 21 shall limit or exclude our entitlement or ability to enforce any rights to any intellectual property in any territory in relation or accordance with the laws application to such rights that subsist in any territory.
+21.3 This agreement and any dispute or claim or complaint arising out of or in connection with it, its subject matter or its formation (including contractual and non-contractual disputes) will be exclusively governed by and construed in accordance with the laws of England and Wales.
+21.4 The 1980 United Convention on Contracts for the International Sale of Goods and its related instruments will not apply to this agreement.
+21.5 All operation of conflict of laws is excluded.
+22 WAIVER OF CLASS ACTIONS/ALTERNATE JURISIDCTIONS
+22.1 You irrevocably waive on behalf of yourself and your successors in title, trustees, assigns, including any person, group or body appointed under any bankruptcy or insolvency rules:
+(a) all rights to bring or participate in any class action;
+(b) any right to request a jury trial against us or other Radix(r) affiliates or entities;
+(c) all rights and entitlement to seek or participate in, commence or pursue any action or seek any order or remedy in any territory (other than England) which may, or is intended to, or is framed so as to be, binding upon the property, assets or rights of Radix(r) Publishing, its officers, directors, employees, agents or affiliates; and
+(d) all rights to assert personal and or subject matter jurisdiction in any territory other than England.
+
+Radix(r) EULA V1.1 Issued 2023.09.07
+
+
+
diff --git a/packages/dapp-toolkit/README.md b/packages/dapp-toolkit/README.md
new file mode 100644
index 00000000..fd2c81f8
--- /dev/null
+++ b/packages/dapp-toolkit/README.md
@@ -0,0 +1,609 @@
+[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
+
+- [What is Radix dApp Toolkit?](#what-is-radix-dapp-toolkit)
+ - [Resources](#resources)
+ - [Building a dApp frontend](#building-a-dapp-frontend)
+- [Installation](#installation)
+- [Usage](#usage)
+ - [Getting started](#getting-started)
+ - [Login requests](#login-requests)
+ - [User authentication](#user-authentication)
+ - [Handle user authentication](#handle-user-authentication)
+ - [User authentication management](#user-authentication-management)
+ - [Wallet data requests](#wallet-data-requests)
+ - [Trigger wallet data request programmatically](#trigger-wallet-data-request-programmatically)
+ - [Change requested data](#change-requested-data)
+ - [Data request builder](#data-request-builder)
+ - [`DataRequestBuilder.persona()`](#datarequestbuilderpersona)
+ - [`DataRequestBuilder.accounts()`](#datarequestbuilderaccounts)
+ - [`OneTimeDataRequestBuilderItem.accounts()`](#onetimedatarequestbuilderitemaccounts)
+ - [`DataRequestBuilder.personaData()`](#datarequestbuilderpersonadata)
+ - [`OneTimeDataRequestBuilderItem.personaData()`](#onetimedatarequestbuilderitempersonadata)
+ - [`DataRequestBuilder.config(input: DataRequestState)`](#datarequestbuilderconfiginput-datarequeststate)
+ - [Handle connect responses](#handle-connect-responses)
+ - [One Time Data Request](#one-time-data-request)
+ - [Data Requests Sandbox](#data-requests-sandbox)
+ - [State changes](#state-changes)
+ - [Transaction requests](#transaction-requests)
+ - [Build transaction manifest](#build-transaction-manifest)
+ - [sendTransaction](#sendtransaction)
+- [ROLA (Radix Off-Ledger Authentication)](#rola-radix-off-ledger-authentication)
+- [√ Connect Button](#-connect-button)
+ - [Styling](#styling)
+ - [Themes](#themes)
+ - [Modes](#modes)
+ - [CSS variables](#css-variables)
+ - [Compact mode](#compact-mode)
+ - [Sandbox](#sandbox)
+- [Setting up your dApp Definition](#setting-up-your-dapp-definition)
+ - [Setting up a dApp Definition on the Radix Dashboard](#setting-up-a-dapp-definition-on-the-radix-dashboard)
+- [Data storage](#data-storage)
+- [Examples](#examples)
+- [License](#license)
+
+# What is Radix dApp Toolkit?
+
+Radix dApp Toolkit (RDT) is a TypeScript library that automates getting users logged in to your dApp using a Persona, maintains a browser session for that login, and provides a local cache of data the user has given permission to your app to access associated with their Persona. It also provides an interface to request accounts and personal data from the user's wallet, either as a permission for ongoing access or as a one-time request, as well as to submit transaction manifest stubs for the user to review, sign, and submit in their wallet.
+
+The current version only supports desktop browser webapps with requests made via the Radix Wallet Connector browser extension. It is intended to later add support for mobile browser webapps using deep linking with the same essential interface.
+
+**RDT is composed of:**
+
+- **√ Connect Button** – A framework agnostic web component that keeps a minimal internal state and have properties are pushed to it.
+
+- **Tools** – Abstractions over lower level APIs for developers to build their radix dApps at lightning speed.
+
+- **State management** – Handles wallet responses, caching and provides data to √ Connect button.
+
+## Resources
+
+### [Building a dApp frontend](https://docs.radixdlt.com/docs/building-a-frontend-dapp)
+
+# Installation
+
+**Using NPM**
+
+```bash
+npm install @radixdlt/radix-dapp-toolkit
+```
+
+**Using Yarn**
+
+```bash
+yarn add @radixdlt/radix-dapp-toolkit
+```
+
+# Usage
+
+## Getting started
+
+Add the `` element in your HTML code and instantiate `RadixDappToolkit`.
+
+```typescript
+import { RadixDappToolkit, RadixNetwork } from '@radixdlt/radix-dapp-toolkit'
+
+const rdt = RadixDappToolkit({
+ dAppDefinitionAddress:
+ 'account_tdx_e_128uml7z6mqqqtm035t83alawc3jkvap9sxavecs35ud3ct20jxxuhl',
+ networkId: RadixNetwork.RCnetV3,
+ applicationName: 'Radix Web3 dApp',
+ applicationVersion: '1.0.0',
+})
+```
+
+**Input**
+
+- **requires** dAppDefinitionAddress - Specifies the dApp that is interacting with the wallet. Used in dApp verification process on the wallet side. [Read more](#setting-up-your-dapp-definition)
+- **requires** networkId - Target radix network ID.
+- _optional_ applicationName - Your dApp name. It's only used for statistics purposes on gateway side
+- _optional_ applicationVersion - Your dApp version. It's only used for statistics purposes on gateway side
+
+## Login requests
+
+The user's journey on your dApp always always starts with connecting their wallet and logging in with a Persona. The "Connect" button always requests a Persona login from the user's wallet.
+
+The default behavior is to request the login alone, but you may also choose to add additional requests for account information or personal data to get at the time of login. This is useful if there is information that you know your dApp always needs to be able to function. You can also however choose to keep the login simple and make other requests later, as needed. Doing it this way allows your dApp to provide a helpful description in its UI of what a given piece of requested information is needed for, such as "please share all of your accounts that you want to use with this dApp" or "providing your email address will let us keep you informed of new features".
+
+The Persona the user logs in with sets the context for all ongoing account and personal data requests for that session. The Radix Wallet keeps track of what permissions the user has provided for each dApp and each Persona they've used with that dApp. RDT automatically keeps track of the currently logged in Persona so that requests to the wallet are for the correct Persona.
+
+After login, RDT also provides your dApp with a local cache of all account information and personal data that a user has given permission to share with your dApp for their chosen Persona.
+
+For a pure frontend dApp (where you have no backend or user database), there is typically no reason for a Persona login to be verified and the login process is completely automated by RDT.
+
+### User authentication
+
+For a full-stack dApp there is also the user authentication flow. Typically, a full-stack dApp would request a persona together with a proof of ownership, which is then verified on the dApp backend using ROLA verification.
+
+**What is a proof of ownership?**
+
+A signature produced by the wallet used to verify that the wallet is in control of a persona or account.
+
+```typescript
+// Signed challenge
+{
+ type: 'persona' | 'account'
+ challenge: string
+ proof: {
+ publicKey: string
+ signature: string
+ curve: 'curve25519' | 'secp256k1'
+ }
+ address: string
+}
+```
+
+The signature is composed of:
+
+| | |
+| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **prefix** | "R" (as in ROLA) in ascii encoding |
+| **challenge** | 32 random bytes provided by the dApp |
+| **length of dApp definition address** | String length of the dApp definition address |
+| **dApp definition address** | The dApp definition address of the requesting dApp |
+| **origin** | The origin of the dApp (e.g. `https://dashboard.radixdlt.com`). This is a value that is added to the wallet data request by the Connector extension. |
+
+**Challenge**
+
+In order to request a persona or account with proof of ownership a challenge is needed.
+
+A challenge is a random 32 bytes hex encoded string that looks something like: `4ccb0555d6b4faad0d7f5ed40bf4e4f0665c8ba35929c638e232e09775d0fa0e`
+
+**Why do we need a challenge?**
+
+The challenge plays an important role in the authentication flow, namely preventing replay attacks from bad actors. The challenge ensures that an authentication request payload sent from the client can only be used once. After a challenge is claimed by a request, the subsequent requests can no longer be resolved successfully with the same payload. As a security best practice, a stored challenge should have a short expiration time. In this case, just enough time for a user to interact with the wallet.
+
+**Request persona with proof**
+
+In order to request a proof, it is required to provide a function to RDT that produces a challenge.
+
+```typescript
+// type requestChallengeFromDappBackendFn = () => Promise
+
+rdt.walletApi.provideChallengeGenerator(requestChallengeFromDappBackendFn)
+
+rdt.walletApi.setRequestData(DataRequestBuilder.persona.withProof())
+
+// handle the wallet response
+rdt.walletApi.dataRequestControl(async (walletData) => {
+ const personaProof = walletData.proofs.find(
+ (proof) => proof.type === 'persona',
+ )
+ if (personaProof) await handleLogin(personaProof)
+})
+```
+
+### Handle user authentication
+
+A typical full stack dApp will require the user to provide proof of ownership. After sending a data request and getting the proof from the wallet, you need authenticate the user through ROLA on the dApp backend.
+
+Use `walletApi.dataRequestControl` to provide a callback function that intercepts the RDT data request response flow. If no error has been thrown inside of the callback function the RDT flow will proceed as usual.
+
+```typescript
+rdt.walletApi.dataRequestControl(async (walletData) => {
+ const personaProof = walletData.proofs.find(
+ (proof) => proof.type === 'persona',
+ )
+ if (personaProof) await handleLogin(personaProof)
+})
+```
+
+Throwing an error inside of `walletApi.dataRequestControl` callback will prevent RDT from getting into a logged in state. A full stack dApp may wish to do this to prevent RDT from treating the user as logged in because the ROLA authentication check failed, or for other application-specific reasons why a given user should not be allowed to login.
+
+```typescript
+rdt.walletApi.dataRequestControl(async (walletData) => {
+ throw new Error('something bad happened...')
+})
+```
+
+See [ROLA example](https://github.com/radixdlt/rola-examples) for an end-to-end implementation.
+
+### User authentication management
+
+After a successful ROLA verification it is up to the dApp's business logic to handle user authentication session in order to keep the user logged-in between requests. Although RDT is persisting state between page reloads, it is not aware of user authentication. The dApp logic needs to control the login state and sign out a user when needed.
+
+**Expired user auth session**
+
+If a user's auth session has expired it is recommended to logout the user in RDT as well. The dApp needs to call the `disconnect` method in order to but the user in a **not connected** state.
+
+```typescript
+rdt.disconnect()
+```
+
+The `disconnect` method resets the RDT state, to login anew, a wallet data request needs to be triggered.
+
+## Wallet data requests
+
+For your dApp to access data from a user's wallet, whether account information or personal data, a request must be sent to the wallet. By default, the request will be "ongoing", meaning that the user will be asked for permission to share the information whenever they login to your dApp with their current Persona. A request may also be "one time" if it is for transient use and you do not require the permission to be retained by the user's wallet.
+
+There are two ways to trigger a data request:
+
+1. As part of the login request when the user clicks the √ Connect button's "Connect"
+2. Programmatically through the walletApi.sendRequest method
+
+#### Trigger wallet data request programmatically
+
+```typescript
+const result = await rdt.walletApi.sendRequest()
+
+if (result.isErr()) return handleException()
+
+// {
+// persona?: Persona,
+// accounts: Account[],
+// personaData: WalletDataPersonaData[],
+// proofs: SignedChallenge[],
+// }
+const walletData = result.value
+```
+
+### Change requested data
+
+By default, a data request requires a Persona to set its context and so if the user is not already logged in, the data request will include a request for login.
+
+Use `walletApi.setRequestData` together with `DataRequestBuilder` to change the wallet data request.
+
+```typescript
+rdt.walletApi.setRequestData(
+ DataRequestBuilder.persona().withProof(),
+ DataRequestBuilder.accounts().exactly(1),
+ DataRequestBuilder.personaData().fullName().emailAddresses(),
+)
+```
+
+### Data request builder
+
+The `DataRequestBuilder` and `OneTimeDataRequestBuilder` is there to assist you in constructing a wallet data request.
+
+#### `DataRequestBuilder.persona()`
+
+```typescript
+withProof: (value?: boolean) => PersonaRequestBuilder
+```
+
+Example: Request persona with proof of ownership
+
+```typescript
+rdt.walletApi.setRequestData(DataRequestBuilder.persona().withProof())
+```
+
+#### `DataRequestBuilder.accounts()`
+
+```typescript
+atLeast: (n: number) => AccountsRequestBuilder
+exactly: (n: number) => AccountsRequestBuilder
+withProof: (value?: boolean) => AccountsRequestBuilder
+reset: (value?: boolean) => AccountsRequestBuilder
+```
+
+Example: Request at least 1 account with proof of ownership
+
+```typescript
+rdt.walletApi.setRequestData(
+ DataRequestBuilder.accounts().atLeast(1).withProof(),
+)
+```
+
+#### `OneTimeDataRequestBuilderItem.accounts()`
+
+```typescript
+atLeast: (n: number) => OneTimeAccountsRequestBuilder
+exactly: (n: number) => OneTimeAccountsRequestBuilder
+withProof: (value?: boolean) => OneTimeAccountsRequestBuilder
+```
+
+Example: Exactly 2 accounts
+
+```typescript
+rdt.walletApi.sendOneTimeRequest(
+ OneTimeDataRequestBuilder.accounts().exactly(2),
+)
+```
+
+#### `DataRequestBuilder.personaData()`
+
+```typescript
+fullName: (value?: boolean) => PersonaDataRequestBuilder
+emailAddresses: (value?: boolean) => PersonaDataRequestBuilder
+phoneNumbers: (value?: boolean) => PersonaDataRequestBuilder
+reset: (value?: boolean) => PersonaDataRequestBuilder
+```
+
+Example: Request full name and email address
+
+```typescript
+rdt.walletApi.setRequestData(
+ DataRequestBuilder.personaData().fullName().emailAddresses(),
+)
+```
+
+#### `OneTimeDataRequestBuilderItem.personaData()`
+
+```typescript
+fullName: (value?: boolean) => PersonaDataRequestBuilder
+emailAddresses: (value?: boolean) => PersonaDataRequestBuilder
+phoneNumbers: (value?: boolean) => PersonaDataRequestBuilder
+```
+
+Example: Request phone number
+
+```typescript
+rdt.walletApi.sendOneTimeRequest(
+ OneTimeDataRequestBuilder.personaData().phoneNumbers(),
+)
+```
+
+#### `DataRequestBuilder.config(input: DataRequestState)`
+
+Use this method if you prefer to provide a raw data request object.
+
+Example: Request at least 1 account and full name.
+
+```typescript
+rdt.walletApi.setRequestData(
+ DataRequestBuilder.config({
+ personaData: { fullName: true },
+ accounts: { numberOfAccounts: { quantifier: 'atLeast', quantity: 1 } },
+ }),
+)
+```
+
+### Handle connect responses
+
+Add a callback function to `provideConnectResponseCallback` that emits a wallet response.
+
+```typescript
+rdt.walletApi.provideConnectResponseCallback((result) => {
+ if (result.isErr()) {
+ // handle connect error
+ }
+})
+```
+
+### One Time Data Request
+
+One-time data requests do not have a Persona context, and so will always result in the Radix Wallet asking the user to select where to draw personal data from. The wallet response from a one time data request is meant to be discarded after usage. A typical use case would be to populate a web-form with user data.
+
+```typescript
+const result = rdt.walletApi.sendOneTimeRequest(
+ OneTimeDataRequestBuilder.accounts().exactly(1),
+ OneTimeDataRequestBuilder.personaData().fullName(),
+)
+
+if (result.isErr()) return handleException()
+
+// {
+// accounts: Account[],
+// personaData: WalletDataPersonaData[],
+// proofs: SignedChallenge[],
+// }
+const walletData = result.value
+```
+
+### Data Requests Sandbox
+
+Play around with the different data requests in
+
+- [Stokenet sandbox environment](https://stokenet-sandbox.radixdlt.com/)
+- [Mainnet sandbox environment](https://sandbox.radixdlt.com/)
+
+## State changes
+
+Listen to wallet data changes by subscribing to `walletApi.walletData$`.
+
+```typescript
+const subscription = rdt.walletApi.walletData$.subscribe((walletData) => {
+ // {
+ // persona?: Persona,
+ // accounts: Account[],
+ // personaData: WalletDataPersonaData[],
+ // proofs: SignedChallenge[],
+ // }
+ doSomethingWithAccounts(walletData.accounts)
+})
+```
+
+When your dApp is done listening to state changes remember to unsubscribe in order to prevent memory leaks.
+
+```typescript
+subscription.unsubscribe()
+```
+
+Get the latest wallet data by calling `walletApi.getWalletData()`.
+
+```typescript
+// {
+// persona?: Persona,
+// accounts: Account[],
+// personaData: WalletDataPersonaData[],
+// proofs: SignedChallenge[],
+// }
+const walletData = rdt.walletApi.getWalletData()
+```
+
+## Transaction requests
+
+Your dApp can send transactions to the user's Radix Wallet for them to review, sign, and submit them to the Radix Network.
+
+Radix transactions are built using "transaction manifests", that use a simple syntax to describe desired behavior. See [documentation on transaction manifest commands here](https://docs.radixdlt.com/docs/transaction-manifest).
+
+It is important to note that what your dApp sends to the Radix Wallet is actually a "transaction manifest stub". It is completed before submission by the Radix Wallet. For example, the Radix Wallet will automatically add a command to lock the necessary amount of network fees from one of the user's accounts. It may also add "assert" commands to the manifest according to user desires for expected returns.
+
+**NOTE:** Information will be provided soon on a ["comforming" transaction manifest stub format](https://docs.radixdlt.com/docs/conforming-transaction-manifest-types) that ensures clear presentation and handling in the Radix Wallet.
+
+### Build transaction manifest
+
+We recommend using template strings for constructing simpler transaction manifests. If your dApp is sending complex manifests a manifest builder can be found in [TypeScript Radix Engine Toolkit](https://github.com/radixdlt/typescript-radix-engine-toolkit#building-manifests)
+
+### sendTransaction
+
+This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed.
+
+```typescript
+type SendTransactionInput = {
+ transactionManifest: string
+ version?: number
+ blobs?: string[]
+ message?: string
+ onTransactionId?: (transactionId: string) => void
+}
+```
+
+- **requires** transactionManifest - specify the transaction manifest
+- **optional** version - specify the version of the transaction manifest
+- **optional** blobs - used for deploying packages
+- **optional** message - message to be included in the transaction
+- **optional** onTransactionId - provide a callback that emits a transaction ID
+
+
+
+sendTransaction example
+
+```typescript
+const result = await rdt.walletApi.sendTransaction({
+ transactionManifest: '...',
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+const transactionIntentHash = result.value.transactionIntentHash
+```
+
+
+
+# ROLA (Radix Off-Ledger Authentication)
+
+ROLA is method of authenticating something claimed by the user connected to your dApp with the Radix Wallet. It uses the capabilities of the Radix Network to make this possible in a way that is decentralized and flexible for the user.
+
+ROLA is intended for use in the server backend portion of a Full Stack dApp. It runs "off-ledger" alongside backend business and user management logic, providing reliable authentication of claims of user control using "on-ledger" data from the Radix Network.
+
+The primary use for ROLA is to authenticate the user's Persona login with the user's control of account(s) on Radix. Let's say that Alice is subscribed to an online streaming service on the Radix network called Radflix, which requires a subscription badge to enter the website. Alice logs in with her Persona to Radflix and now needs to prove that she owns an account that contains a Radflix subscription badge. By using Rola we can verify that Alice is the owner of the account that contains the Radflix subscription badge. Once we have verified that Alice is the owner of the account, we can then use the account to check for the Radflix subscription badge and verify that Alice has a valid subscription.
+
+**Read more**
+
+- [ROLA example](https://github.com/radixdlt/rola-examples)
+- [Full-stack dApp](https://docs.radixdlt.com/docs/building-a-full-stack-dapp)
+
+# √ Connect Button
+
+Provides a consistent and delightful user experience between radix dApps. Although complex by itself, RDT is off-loading the developer burden of having to handle the logic of all its internal states.
+
+Just add the HTML element in your code, and you're all set.
+
+```html
+
+```
+
+## Styling
+
+Configure the √ Connect Button to fit your dApp's branding.
+
+### Themes
+
+Available themes:
+
+- `radix-blue` (default)
+- `black`
+- `white-with-outline`
+- `white`
+
+```typescript
+rdt.buttonApi.setTheme('black')
+```
+
+### Modes
+
+Available modes:
+
+- `light` (default)
+- `dark`
+
+```typescript
+rdt.buttonApi.setMode('dark')
+```
+
+### CSS variables
+
+There are three CSS variables available:
+
+- `--radix-connect-button-width` (default 138px)
+- `--radix-connect-button-height` (default 42px)
+- `--radix-connect-button-border-radius` (default 0px)
+
+```css
+body {
+ --radix-connect-button-width: 200px;
+ --radix-connect-button-height: 42px;
+ --radix-connect-button-border-radius: 12px;
+}
+```
+
+### Compact mode
+
+Setting `--radix-connect-button-width` below `138px` will enable compact mode.
+
+### Sandbox
+
+Play around with the different configurations on the
+[sandbox environment](https://connect-button-storybook.radixdlt.com/)
+
+# Setting up your dApp Definition
+
+A dApp Definition account should be created after you’ve built your dApp’s components and resources, and created a website front end for it. dApp Definition account is a special account on the Radix Network with some metadata set on it that does some nice things, like:
+
+- Provides the necessary unique identifier (the dApp Definition’s address) that the Radix Wallet needs to let users login to your dApp and save sharing preferences for it.
+
+- Defines things like name, description, and icon so the Radix Wallet can inform users what they are interacting with.
+
+- Lets you link together everything associated with your dApp – like websites, resources, and components – so that the Radix Wallet knows what they all belong to.
+
+Creating a dApp Definition for your dApp will provide the necessary information for clients like the Radix Wallet to let users interact with your dApp in a way that is easy, safe, and informative. It also acts as a hub that connects all your dApp pieces together.
+
+You can read more about dApp Definitions [here](https://docs.radixdlt.com/docs/metadata-for-verification).
+
+## Setting up a dApp Definition on the Radix Dashboard
+
+1. **Create a new account in the Radix Wallet.** This is the account which we will convert to a dApp Definition account.
+
+2. **Head to the Radix Dashboard’s Manage dApp Definitions page**. This page provides a simple interface to set the metadata on an account to make it a dApp Definition.
+
+3. **Connect your Radix Wallet to the Dashboard** and make sure you share the account that you just created to be a dApp Definition. Select that account on the Dashboard page.
+
+4. **Now check the box for “Set this account as a dApp Definition”, and fill in the name and description you want to use for your dApp.** Later you’ll also be able to specify an icon image, but that’s not ready just yet.
+
+5. **Click “Update”** and an approve transaction should appear in your Radix Wallet. Done!
+
+Provide account address as the the dApp Definition address that you just created, and it will be sent to the Radix Wallet whenever a user connects or receives a transaction from your dApp. The Wallet will then look up that dApp Definition address on the Radix Network, pull the latest metadata, and show it to the user. When a user logins to your dApp, an entry in the wallet’s preferences for your dApp will appear too. Try it out for yourself!
+
+# Data storage
+
+To provide a consistent user experience RDT stores data to the browser’s local storage. This will enable state rehydration and keep state between page reloads.
+
+To understand which wallet responses that get stored we need to understand the difference between one-time and regular data requests.
+
+One-time data requests do not register the dApp in the wallet and the connect button does not display that data in the UI. The data is meant to be used temporarily by the dApp and discarded thereafter.
+
+A user connecting her wallet will be the first user flow in the majority of dApps. The connect flow is a bit different from subsequent data request flows. Its purpose is to provide the dApp with a minimal amount of user data in order for the user to be able to use the dApp, e.g. the minimal amount of data for a DEX dApp is an account.
+
+RDT handles writing and reading data to the browser’s local storage so that it will be persisted between page loads. The dApp frontend logic can at any time ask RDT to provide the stored data by subscribing to the `walletApi.walletData$` observable or calling `walletApi.getWalletData`. One time data requests or requests that can not be resolved by the internal state are sent as data requests to the wallet.
+
+# Examples
+
+The `examples` directory contains a react dApp that consumes RDT. Its main purpose is to be used by us internally for debugging but can also serve as a source of inspiration.
+
+# License
+
+The Radix Dapp Toolkit binaries are licensed under the [Radix Software EULA](http://www.radixdlt.com/terms/genericEULA).
+
+The Radix Dapp Toolkit code is released under [Apache 2.0 license](LICENSE).
+
+ Copyright 2023 Radix Publishing Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
+
+ You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+ See the License for the specific language governing permissions and limitations under the License.
diff --git a/packages/dapp-toolkit/docs/wallet-request-sdk.md b/packages/dapp-toolkit/docs/wallet-request-sdk.md
new file mode 100644
index 00000000..a54811e8
--- /dev/null
+++ b/packages/dapp-toolkit/docs/wallet-request-sdk.md
@@ -0,0 +1,673 @@
+[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
+
+This is a TypeScript developer SDK that facilitates communication with the Radix Wallet for two purposes: **requesting various forms of data from the wallet** and **sending transactions to the wallet**.
+
+**Important Note:** This is an early release for development on the Radix Betanet and the Radix Wallet developer preview. This readme describes the intended full interface for the Radix mainnet release, but many features are not yet available (and are flagged as such).
+
+The current version only supports desktop browser webapps with requests made via the Radix Wallet Connector browser extension. It is intended to later add support for mobile browser webapps using deep linking with the same essential interface.
+
+You may wish to consider using this with [dApp toolkit](https://github.com/radixdlt/radix-dapp-toolkit), which works with this SDK to provide additional features for your application and users.
+
+- [⬇️ Getting Wallet Data](#️-getting-wallet-data)
+ - [💶 Accounts](#-accounts)
+ - [Request](#request)
+ - [Response](#response)
+ - [ℹ️ Persona Data](#ℹ️-persona-data)
+ - [Request](#request-1)
+ - [Response](#response-1)
+ - [🗑️ Reset](#️-reset)
+ - [Request](#request-2)
+ - [Response](#response-2)
+ - [🛂 Auth](#-auth)
+ - [Request](#request-3)
+ - [Response](#response-3)
+- [💸 Send transaction](#-send-transaction)
+ - [Build transaction manifest](#build-transaction-manifest)
+ - [sendTransaction](#sendtransaction)
+ - [Errors](#errors)
+- [License](#license)
+
+## Getting started
+
+```typescript
+import { WalletRequestSdk } from '@radixdlt/radix-dapp-toolkit'
+
+const walletRequestSdk = WalletRequestSdk({
+ networkId: 12,
+ dAppDefinitionAddress:
+ 'account_tdx_c_1p8j5r3umpgdwpedqssn0mwnwj9tv7ae7wfzjd9srwh5q9stufq',
+})
+```
+
+```typescript
+type Metadata = {
+ networkId: number
+ dAppDefinitionAddress: string
+}
+```
+
+| Network | ID |
+| :------- | :-: |
+| Mainnet | 1 |
+| Stokenet | 2 |
+
+- **requires** networkId - Specifies which network to use
+- **requires** dAppDefinitionAddress - Specifies the dApp that is interacting with the wallet. Used in dApp verification process on the wallet side.
+
+# ⬇️ Getting Wallet Data
+
+**About oneTime VS ongoing requests**
+
+There are two types of data requests: `oneTime` and `ongoing`.
+
+**OneTime** data requests will **always** result in the Radix Wallet asking for the user's permission to share the data with the dApp.
+
+```typescript
+type WalletUnauthorizedRequestItems = {
+ discriminator: 'unauthorizedRequest'
+ oneTimeAccounts?: AccountsRequestItem
+ oneTimePersonaData?: PersonaDataRequestItem
+}
+```
+
+**Ongoing** data requests will only result in the Radix Wallet asking for the user's permission the first time. If accepted, the Radix Wallet will automatically respond to future data requests of this type with the current data. The user's permissions for ongoing data sharing with a given dApp can be managed or revoked by the user at any time in the Radix Wallet.
+
+```typescript
+type WalletAuthorizedRequestItems = {
+ discriminator: 'authorizedRequest'
+ auth: AuthRequestItem
+ reset?: ResetRequestItem
+ oneTimeAccounts?: AccountsRequestItem
+ oneTimePersonaData?: PersonaDataRequestItem
+ ongoingAccounts?: AccountsRequestItem
+ ongoingPersonaData?: PersonaDataRequestItem
+}
+```
+
+The user's ongoing data sharing permissions are associated with a given Persona (similar to a login) in the Radix Wallet. This means that in order to request `ongoing` data, a `identityAddress` must be included.
+
+Typically the dApp should begin with a `login` request which will return the `identityAddress` for the user's chosen Persona, which can be used for further requests (perhaps while the user has a valid session)
+
+## 💶 Accounts
+
+This request type is for getting one or more Radix accounts managed by the user's Radix Wallet app. You may specify the number of accounts desired, and if you require proof of ownership of the account.
+
+### Request
+
+```typescript
+type NumberOfValues = {
+ quantifier: 'exactly' | 'atLeast'
+ quantity: number
+}
+```
+
+```typescript
+type AccountsRequestItem = {
+ challenge?: Challenge
+ numberOfAccounts: NumberOfValues
+}
+```
+
+### Response
+
+```typescript
+type Account = {
+ address: string
+ label: string
+ appearanceId: number
+}
+```
+
+```typescript
+type AccountProof = {
+ accountAddress: string
+ proof: Proof
+}
+```
+
+```typescript
+type Proof = {
+ publicKey: string
+ signature: string
+ curve: 'curve25519' | 'secp256k1'
+}
+```
+
+```typescript
+type AccountsRequestResponseItem = {
+ accounts: Account[]
+ challenge?: Challenge
+ proofs?: AccountProof[]
+}
+```
+
+
+
+ongoingAccounts example
+
+```typescript
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: { discriminator: 'loginWithoutChallenge' },
+ ongoingAccounts: {
+ numberOfAccounts: { quantifier: 'atLeast', quantity: 1 },
+ },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "authorizedRequest",
+// auth: {
+// discriminator: loginWithoutChallenge,
+// persona: Persona
+// },
+// ongoingAccounts: {
+// accounts: Account[]
+// }
+// }
+const value = result.value
+```
+
+
+
+
+
+oneTimeAccounts example
+
+```typescript
+const result = await walletSdk.request({
+ discriminator: 'unauthorizedRequest',
+ oneTimeAccounts: { numberOfAccounts: { quantifier: 'atLeast', quantity: 1 } },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "unauthorizedRequest",
+// oneTimeAccounts: {
+// accounts: Account[]
+// }
+// }
+const value = result.value
+```
+
+
+
+
+
+with proof of ownership example
+
+```typescript
+// hex encoded 32 random bytes
+const challenge = [...crypto.getRandomValues(new Uint8Array(32))]
+ .map((item) => item.toString(16).padStart(2, '0'))
+ .join('')
+
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: { discriminator: 'loginWithoutChallenge' },
+ ongoingAccounts: {
+ challenge,
+ numberOfAccounts: { quantifier: 'atLeast', quantity: 1 },
+ },
+ oneTimeAccounts: {
+ challenge,
+ numberOfAccounts: { quantifier: 'atLeast', quantity: 1 },
+ },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "authorizedRequest",
+// auth: {
+// discriminator: loginWithoutChallenge,
+// persona: Persona
+// },
+// ongoingAccounts: {
+// accounts: Account[],
+// challenge,
+// proofs: AccountProof[]
+// },
+// oneTimeAccounts: {
+// accounts: Account[],
+// challenge,
+// proofs: AccountProof[]
+// }
+// }
+const value = result.value
+```
+
+
+
+## ℹ️ Persona Data
+
+This request type is for a list of personal data fields associated with the user's selected Persona.
+
+### Request
+
+```typescript
+type PersonaDataRequestItem = {
+ isRequestingName?: boolean()
+ numberOfRequestedEmailAddresses?: NumberOfValues
+ numberOfRequestedPhoneNumbers?: NumberOfValues
+}
+```
+
+### Response
+
+```typescript
+type PersonaDataRequestResponseItem = {
+ name?: {
+ variant: 'eastern' | 'western'
+ family: string
+ given: string
+ }
+ emailAddresses?: NumberOfValues
+ phoneNumbers?: NumberOfValues
+}
+```
+
+
+
+ongoingPersonaData example
+
+```typescript
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: { discriminator: 'loginWithoutChallenge' },
+ ongoingPersonaData: {
+ isRequestingName: true,
+ numberOfRequestedEmailAddresses: { quantifier: 'atLeast', quantity: 1 },
+ numberOfRequestedPhoneNumbers: { quantifier: 'exactly', quantity: 1 },
+ },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "authorizedRequest",
+// auth: {
+// discriminator: loginWithoutChallenge,
+// persona: Persona
+// },
+// ongoingPersonaData: {
+// name: {
+// variant: 'western',
+// given: 'John',
+// family: 'Conner'
+// },
+// emailAddresses: ['jc@resistance.ai'],
+// phoneNumbers: ['123123123']
+// }
+// }
+
+const value = result.value
+```
+
+
+
+
+
+oneTimePersonaData example
+
+```typescript
+const result = await sdk.request({
+ discriminator: 'unauthorizedRequest',
+ oneTimePersonaData: {
+ isRequestingName: true,
+ },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "unauthorizedRequest",
+// oneTimePersonaData: {
+// name: {
+// variant: 'eastern',
+// given: 'Jet',
+// family: 'Li'
+// }
+// }
+// }
+
+const value = result.value
+```
+
+
+
+## 🗑️ Reset
+
+You can send a reset request to ask the user to provide new values for ongoing accounts and/or persona data.
+
+### Request
+
+```typescript
+type ResetRequestItem = {
+ accounts: boolean
+ personaData: boolean
+}
+```
+
+### Response
+
+A Reset request has no response.
+
+
+
+reset example
+
+```typescript
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: { discriminator: 'loginWithoutChallenge' },
+ reset: { accounts: true, personaData: true },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+```
+
+
+
+## 🛂 Auth
+
+Sometimes your dApp may want a more personalized, consistent user experience and the Radix Wallet is able to login users with a Persona.
+
+For a pure frontend dApp without any server backend, you may simply want to request such a login from the users's wallet so that the wallet keeps track of data sharing preferences for your dApp and they don't have to re-select that data each time they connect.
+
+If your dApp does have a server backend and you are keeping track of users to personalize their experience, a Persona-based login provides strong proof of user identity, and the ID returned from the wallet provides a unique index for that user.
+
+Once your dApp has a given `identityAddress`, it may be used for future requests for data that the user has given "ongoing" permission to share.
+
+```typescript
+type Persona = {
+ identityAddress: string
+ label: string
+}
+```
+
+**Login**
+
+This request type results in the Radix Wallet asking the user to select a Persona to login to this dApp (or suggest one already used in the past there), and providing cryptographic proof of control.
+
+```typescript
+// Hex encoded 32 random bytes
+type Challenge = string
+```
+
+This proof comes in the form of a signed "challenge" against an on-ledger Identity component. For each Persona a user creates in the Radix Wallet, the wallet automatically creates an associated on-ledger Identity (which contains none of the personal data held in the wallet). This Identity includes a public key in its metadata, and the signature on the challenge uses the corresponding private key. ROLA (Radix Off-Ledger Authentication) may be used in your dApp backend to check if the login challenge is correct against on-ledger state.
+
+```typescript
+type Proof = {
+ publicKey: string
+ signature: string
+ curve: 'curve25519' | 'secp256k1'
+}
+```
+
+The on-ledger address of this Identity will be the `identityAddress` used to identify that user – in future queries, or perhaps in your dApp's own user database.
+
+If you are building a pure frontend dApp where the login is for pure user convenience, you may safely ignore the challenge and simply keep track of the `identityAddress` in the user's session for use in data requests that require it.
+
+**usePersona**
+
+If you have already identified the user via a login (perhaps for a given active session), you may specify a `identityAddress` directly without requesting a login from the wallet.
+
+
+
+login example
+
+```typescript
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: { discriminator: 'loginWithoutChallenge' },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "authorizedRequest",
+// auth: {
+// discriminator: 'loginWithoutChallenge',
+// persona: Persona
+// },
+// }
+const value = result.value
+```
+
+
+
+
+login with challenge example
+
+```typescript
+// hex encoded 32 random bytes
+const challenge = [...crypto.getRandomValues(new Uint8Array(32))]
+ .map((item) => item.toString(16).padStart(2, '0'))
+ .join('')
+
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: {
+ discriminator: 'loginWithChallenge',
+ challenge,
+ },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "authorizedRequest",
+// auth: {
+// discriminator: 'loginWithChallenge',
+// persona: Persona,
+// challenge: Challenge,
+// proof: Proof
+// },
+// }
+const value = result.value
+```
+
+
+
+
+usePersona example
+
+```typescript
+const result = await sdk.request({
+ discriminator: 'authorizedRequest',
+ auth: {
+ discriminator: 'usePersona',
+ identityAddress:
+ 'identity_tdx_c_1p35qpky5sczp5t4qkhzecz3nm8tcvy4mz4997mqtuzlsvfvrwm',
+ },
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+// {
+// discriminator: "authorizedRequest",
+// auth: {
+// discriminator: usePersona,
+// persona: Persona
+// },
+// }
+const value = result.value
+```
+
+
+
+### Request
+
+```typescript
+type AuthRequestItem = AuthUsePersonaRequestItem | AuthLoginRequestItem
+```
+
+```typescript
+type AuthUsePersonaRequestItem = {
+ discriminator: 'usePersona'
+ identityAddress: string
+}
+```
+
+```typescript
+type AuthLoginRequestItem =
+ | AuthLoginWithoutChallengeRequestItem
+ | AuthLoginWithChallengeRequestItem
+```
+
+```typescript
+type AuthLoginWithoutChallengeRequestItem = {
+ discriminator: 'loginWithoutChallenge'
+}
+```
+
+```typescript
+type AuthLoginWithChallengeRequestItem = {
+ discriminator: 'loginWithChallenge'
+ challenge: Challenge
+}
+```
+
+### Response
+
+```typescript
+type AuthRequestResponseItem =
+ | AuthUsePersonaRequestResponseItem
+ | AuthLoginRequestResponseItem
+```
+
+```typescript
+type AuthUsePersonaRequestResponseItem = {
+ discriminator: 'usePersona'
+ persona: Persona
+}
+```
+
+```typescript
+type AuthLoginRequestResponseItem =
+ | AuthLoginWithoutChallengeResponseRequestItem
+ | AuthLoginWithChallengeRequestResponseItem
+```
+
+```typescript
+type AuthLoginWithoutChallengeRequestResponseItem = {
+ discriminator: 'loginWithoutChallenge'
+ persona: Persona
+}
+```
+
+```typescript
+type AuthLoginWithChallengeRequestResponseItem = {
+ discriminator: 'loginWithChallenge'
+ persona: Persona
+ challenge: Challenge
+ proof: Proof
+}
+```
+
+# 💸 Send transaction
+
+Your dApp can send transactions to the user's Radix Wallet for them to review, sign, and submit them to the Radix Network.
+
+Radix transactions are built using "transaction manifests", that use a simple syntax to describe desired behavior. See [documentation on transaction manifest commands here](https://docs-babylon.radixdlt.com/main/scrypto/transaction-manifest/intro.html).
+
+It is important to note that what your dApp sends to the Radix Wallet is actually a "transaction manifest stub". It is completed before submission by the Radix Wallet. For example, the Radix Wallet will automatically add a command to lock the necessary amount of network fees from one of the user's accounts. It may also add "assert" commands to the manifest according to user desires for expected returns.
+
+**NOTE:** Information will be provided soon on a ["comforming" transaction manifest stub format](https://docs-babylon.radixdlt.com/main/standards/comforming-transactions.html) that ensures clear presentation and handling in the Radix Wallet.
+
+## Build transaction manifest
+
+We recommend using template strings for constructing simpler transaction manifests. If your dApp is sending complex manifests a manifest builder can be found in [TypeScript Radix Engine Toolkit](https://github.com/radixdlt/typescript-radix-engine-toolkit#building-manifests)
+
+## sendTransaction
+
+This sends the transaction manifest stub to a user's Radix Wallet, where it will be completed, presented to the user for review, signed as required, and submitted to the Radix network to be processed.
+
+```typescript
+type SendTransactionInput = {
+ transactionManifest: string
+ version: number
+ blobs?: string[]
+ message?: string
+}
+```
+
+- **requires** transactionManifest - specify the transaction manifest
+- **requires** version - specify the version of the transaction manifest
+- **optional** blobs - used for deploying packages
+- **optional** message - message to be included in the transaction
+
+
+
+sendTransaction example
+
+```typescript
+const result = await sdk.sendTransaction({
+ version: 1,
+ transactionManifest: '...',
+})
+
+if (result.isErr()) {
+ // code to handle the exception
+}
+
+const transactionIntentHash = result.value.transactionIntentHash
+```
+
+
+
+## Errors
+
+| Error type | Description | Message |
+| :----------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ |
+| rejectedByUser | User has rejected the request in the wallet | |
+| missingExtension | Connector extension is not detected | |
+| canceledByUser | User has canceled the request | |
+| walletRequestValidation | SDK has constructed an invalid request | |
+| walletResponseValidation | Wallet sent an invalid response | |
+| wrongNetwork | Wallet is currently using a network with a network ID that does not match the one specified in request from Dapp (inside metadata) | "Wallet is using network ID: \(currentNetworkID), request sent specified network ID: \(requestFromP2P.requestFromDapp.metadata.networkId)." |
+| failedToPrepareTransaction | Failed to get Epoch for Transaction Header | |
+| failedToCompileTransaction | Failed to compile TransactionIntent or any other later form to SBOR using EngineToolkit | |
+| failedToSignTransaction | Failed to sign any form of the transaction either with keys for accounts or with notary key, or failed to convert the signature to by EngineToolkit require format | |
+| failedToSubmitTransaction | App failed to submit the transaction to Gateway for some reason | |
+| failedToPollSubmittedTransaction | App managed to submit transaction but got error while polling it | "TXID: " |
+| submittedTransactionWasDuplicate | App submitted a transaction and got informed by Gateway it was duplicated | "TXID: " |
+| submittedTransactionHasFailedTransactionStatus | App submitted a transaction to Gateway and polled transaction status telling app it was a failed transaction | "TXID: " |
+| submittedTransactionHasRejectedTransactionStatus | App submitted a transaction to Gateway and polled transaction status telling app it was a rejected transaction | "TXID: " |
+
+# License
+
+The Wallet SDK binaries are licensed under the [Radix Software EULA](http://www.radixdlt.com/terms/genericEULA).
+
+The Wallet SDK code is released under [Apache 2.0 license](LICENSE).
+
+ Copyright 2023 Radix Publishing Ltd
+
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
+
+ You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+ See the License for the specific language governing permissions and limitations under the License.
diff --git a/packages/dapp-toolkit/package.json b/packages/dapp-toolkit/package.json
new file mode 100644
index 00000000..96ad26dd
--- /dev/null
+++ b/packages/dapp-toolkit/package.json
@@ -0,0 +1,80 @@
+{
+ "name": "@radixdlt/radix-dapp-toolkit",
+ "description": "Radix dApp Toolkit repository",
+ "version": "0.0.0",
+ "homepage": "https://developers.radixdlt.com",
+ "type": "module",
+ "keywords": [
+ "radix",
+ "dapp",
+ "toolkit",
+ "dlt",
+ "web3",
+ "dApp",
+ "crypto",
+ "DeFi"
+ ],
+ "authors": [
+ "Alex Stelea ",
+ "Dawid Sowa "
+ ],
+ "engines": {
+ "node": ">=18"
+ },
+ "bugs": "https://github.com/radixdlt/radix-dapp-toolkit/issues",
+ "license": "SEE LICENSE IN RADIX-SOFTWARE-EULA",
+ "main": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ }
+ },
+ "files": [
+ "RADIX-SOFTWARE-EULA",
+ "dist",
+ "types"
+ ],
+ "scripts": {
+ "dev": "npm run build -- --watch",
+ "build": "tsup src/index.ts",
+ "test": "vitest",
+ "test:watch": "vitest --watch"
+ },
+ "dependencies": {
+ "@noble/curves": "1.4.0",
+ "bowser": "2.11.0",
+ "buffer": "6.0.3",
+ "immer": "10.0.4",
+ "lit": "3.1.2",
+ "lit-html": "3.1.2",
+ "neverthrow": "6.1.0",
+ "rxjs": "7.8.1",
+ "tslog": "4.8.2",
+ "valibot": "0.30.0",
+ "radix-connect-common": "*"
+ },
+ "devDependencies": {
+ "@radixdlt/connect-button": "*",
+ "semantic-release": "^23.0.0",
+ "@saithodev/semantic-release-backmerge": "^3.2.1",
+ "@semantic-release/exec": "^6.0.3",
+ "semantic-release-replace-plugin": "^1.2.7",
+ "tsup": "^8.0.2",
+ "typescript": "^5.4.4",
+ "vitest": "^1.4.0",
+ "vitest-mock-extended": "^1.3.1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/radixdlt/radix-dapp-toolkit.git"
+ },
+ "publishConfig": {
+ "registry": "https://registry.npmjs.org"
+ }
+}
\ No newline at end of file
diff --git a/setup-tests.js b/packages/dapp-toolkit/setup-tests.js
similarity index 100%
rename from setup-tests.js
rename to packages/dapp-toolkit/setup-tests.js
diff --git a/src/_types.ts b/packages/dapp-toolkit/src/_types.ts
similarity index 68%
rename from src/_types.ts
rename to packages/dapp-toolkit/src/_types.ts
index d634eb86..d1d2dbff 100644
--- a/src/_types.ts
+++ b/packages/dapp-toolkit/src/_types.ts
@@ -1,39 +1,33 @@
-import { Result, ResultAsync } from 'neverthrow'
-import {
- WalletSdk,
- Account,
- AppLogger,
- PersonaDataName,
- Persona,
-} from '@radixdlt/wallet-sdk'
-import { Observable } from 'rxjs'
-import { WalletClient } from './wallet/wallet-client'
-import {
+import type { Result } from 'neverthrow'
+import { ResultAsync } from 'neverthrow'
+import type { Observable } from 'rxjs'
+import type { WalletRequestClient } from './wallet-request/wallet-request'
+import type {
RadixButtonStatus,
RadixButtonTheme,
RequestItem,
-} from '@radixdlt/connect-button'
-import { GatewayClient } from './gateway/gateway'
-import { StateClient } from './state/state'
-import { RequestItemClient } from './request-items/request-item-client'
-import { DataRequestClient } from './data-request/data-request'
-import { DataRequestStateClient } from './data-request/data-request-state'
-import {
- State,
- Status,
- Transaction,
- TransactionStatus,
-} from '@radixdlt/babylon-gateway-api-sdk'
-import { SignedChallenge, WalletData } from './state/types'
-import {
+} from 'radix-connect-common'
+import type { GatewayClient } from './gateway/gateway'
+import type { RequestItemClient } from './wallet-request/request-items/request-item-client'
+import type { DataRequestStateClient } from './wallet-request/data-request/data-request-state'
+import type {
DataRequestBuilderItem,
OneTimeDataRequestBuilderItem,
-} from './data-request/builders'
-
-export type StorageProvider = {
- getData: (key: string) => ResultAsync
- setData: (key: string, data: any) => ResultAsync
-}
+} from './wallet-request/data-request/builders'
+import {
+ Account,
+ CallbackFns,
+ Persona,
+ PersonaDataName,
+ WalletInteraction,
+} from './schemas'
+import { StateClient } from './state/state'
+import { WalletData, SignedChallenge } from './state/types'
+import type { Logger } from './helpers'
+import { SdkError } from './error'
+import { WalletRequestSdk } from './wallet-request'
+import { TransactionStatus } from './gateway/types'
+import { StorageProvider } from './storage/local-storage-client'
export type ConnectButtonProvider = {
status$: Observable
@@ -67,13 +61,13 @@ export type ConnectButtonProvider = {
export type Providers = {
stateClient: StateClient
connectButton: ConnectButtonProvider
- walletClient: WalletClient
+ walletRequestClient: WalletRequestClient
gatewayClient: GatewayClient
- walletSdk: WalletSdk
+ walletRequestSdk: WalletRequestSdk
requestItemClient: RequestItemClient
storageClient: StorageProvider
- dataRequestClient: DataRequestClient
dataRequestStateClient: DataRequestStateClient
+ transports: TransportProvider[]
}
export type ExplorerConfig = {
@@ -82,18 +76,14 @@ export type ExplorerConfig = {
accountsPath: string
}
-type WalletDataRequest = Parameters[0]
+export type WalletDataRequest = Parameters[0]
export type WalletRequest =
- | { type: 'sendTransaction'; payload: SendTransactionInput }
- | { type: 'dataRequest'; payload: WalletDataRequest }
-
-export type RequestInterceptor = (
- input: T
-) => Promise
+ | { type: 'sendTransaction'; payload: WalletInteraction }
+ | { type: 'dataRequest'; payload: WalletInteraction }
export type OptionalRadixDappToolkitOptions = {
- logger: AppLogger
+ logger: Logger
onDisconnect: () => void
explorer: ExplorerConfig
gatewayBaseUrl: string
@@ -101,7 +91,8 @@ export type OptionalRadixDappToolkitOptions = {
applicationVersion: string
useCache: boolean
providers: Partial
- requestInterceptor: RequestInterceptor
+ requestInterceptor: (input: WalletInteraction) => Promise
+ enableMobile: boolean
}
export type RadixDappToolkitOptions = {
@@ -131,12 +122,6 @@ export type SendTransactionInput = {
onTransactionId?: (transactionId: string) => void
}
-export type GatewayApi = {
- state: State
- status: Status
- transaction: Transaction
-}
-
export type ButtonApi = {
setMode: (value: 'light' | 'dark') => void
setTheme: (value: RadixButtonTheme) => void
@@ -160,11 +145,11 @@ export type AwaitedWalletDataRequestResult = Result<
>
export type WalletApi = {
- getWalletData: () => WalletDataState
+ getWalletData: () => WalletDataState | undefined
walletData$: Observable
provideChallengeGenerator: (fn: () => Promise) => void
provideConnectResponseCallback: (
- fn: (result: AwaitedWalletDataRequestResult) => void
+ fn: (result: AwaitedWalletDataRequestResult) => void,
) => void
dataRequestControl: (fn: (walletResponse: WalletData) => Promise) => void
updateSharedData: () => WalletDataRequestResult
@@ -197,16 +182,15 @@ export type WalletDataState = {
persona?: Persona
}
-export type RequestInterceptorFactoryOutput = ReturnType<
- typeof requestInterceptorFactory
->
-export const requestInterceptorFactory =
- (requestInterceptor: RequestInterceptor) =>
- (walletRequest: T) =>
- ResultAsync.fromPromise(
- requestInterceptor(walletRequest),
- (jsError) => ({
- error: 'requestInterceptorError',
- jsError,
- })
- )
+export type TransportProvider = {
+ isLinked$?: Observable
+ isAvailable$?: Observable
+ showQrCode?: () => void
+ isSupported: () => boolean
+ send: (
+ walletInteraction: WalletInteraction,
+ callbackFns: Partial,
+ ) => ResultAsync
+ disconnect: () => void
+ destroy: () => void
+}
diff --git a/packages/dapp-toolkit/src/connect-button/connect-button-client.ts b/packages/dapp-toolkit/src/connect-button/connect-button-client.ts
new file mode 100644
index 00000000..18007c6b
--- /dev/null
+++ b/packages/dapp-toolkit/src/connect-button/connect-button-client.ts
@@ -0,0 +1,447 @@
+import {
+ filter,
+ first,
+ fromEvent,
+ map,
+ merge,
+ mergeMap,
+ of,
+ Subscription,
+ switchMap,
+ tap,
+ timer,
+} from 'rxjs'
+import type { ConnectButton } from '@radixdlt/connect-button'
+import type {
+ Account,
+ RadixButtonStatus,
+ RadixButtonTheme,
+ RequestItem,
+} from 'radix-connect-common'
+import { ConnectButtonSubjects } from './subjects'
+import { type Logger } from '../helpers'
+import { ConnectButtonProvider, ExplorerConfig } from '../_types'
+import {
+ transformWalletDataToConnectButton,
+ WalletRequestClient,
+} from '../wallet-request'
+import { GatewayClient } from '../gateway/gateway'
+import { StateClient } from '../state'
+import { RadixNetworkConfigById } from '../gateway/types'
+import { LocalStorageClient, StorageProvider } from '../storage'
+
+export type ConnectButtonClient = ReturnType
+
+export const ConnectButtonClient = (input: {
+ networkId: number
+ environment?: string
+ dAppDefinitionAddress?: string
+ onConnect?: (done: (input?: { challenge: string }) => void) => void
+ subjects?: ConnectButtonSubjects
+ logger?: Logger
+ onDisconnect?: () => void
+ explorer?: ExplorerConfig
+ enableMobile?: boolean
+ providers: {
+ stateClient: StateClient
+ gatewayClient: GatewayClient
+ walletRequestClient: WalletRequestClient
+ storageClient?: StorageProvider
+ }
+}): ConnectButtonProvider => {
+ import('@radixdlt/connect-button')
+ const logger = input?.logger?.getSubLogger({ name: 'ConnectButtonClient' })
+ const subjects = input.subjects || ConnectButtonSubjects()
+ const dAppDefinitionAddress = input.dAppDefinitionAddress
+ const { baseUrl, accountsPath, transactionPath } = input.explorer ?? {
+ baseUrl: RadixNetworkConfigById[input.networkId].dashboardUrl,
+ transactionPath: '/transaction/',
+ accountsPath: '/account/',
+ }
+ const statusStorage =
+ input.providers?.storageClient ??
+ LocalStorageClient(
+ `rdt:${dAppDefinitionAddress}:${input.networkId}`,
+ 'connectButtonStatus',
+ )
+
+ const stateClient = input.providers.stateClient
+ const gatewayClient = input.providers.gatewayClient
+ const enableMobile = input.enableMobile ?? false
+
+ const walletRequestClient = input.providers.walletRequestClient
+ const onConnectDefault = (done: (input?: { challenge: string }) => void) => {
+ done()
+ }
+ const onConnect = input.onConnect || onConnectDefault
+ const transport = walletRequestClient.getTransport()
+
+ const getConnectButtonElement = (): ConnectButton | null =>
+ document.querySelector('radix-connect-button')
+
+ const subscriptions = new Subscription()
+
+ subscriptions.add(
+ merge(
+ fromEvent(document, 'onRender'),
+ of(getConnectButtonElement()).pipe(filter((e) => !!e)),
+ )
+ .pipe(
+ map(() => getConnectButtonElement()),
+ filter((element): element is ConnectButton => !!element),
+ first(),
+ switchMap((connectButtonElement) => {
+ logger?.debug({ event: `connectButtonDiscovered` })
+
+ connectButtonElement.enableMobile = enableMobile
+
+ const onConnect$ = fromEvent(connectButtonElement, 'onConnect').pipe(
+ tap(() => {
+ onConnect((value) => subjects.onConnect.next(value))
+ }),
+ )
+
+ const onDisconnect$ = fromEvent(
+ connectButtonElement,
+ 'onDisconnect',
+ ).pipe(tap(() => subjects.onDisconnect.next()))
+
+ const onLinkClick$ = fromEvent<
+ CustomEvent<{
+ type: 'account' | 'transaction'
+ data: string
+ }>
+ >(connectButtonElement, 'onLinkClick').pipe(
+ tap((ev) => {
+ subjects.onLinkClick.next(ev.detail)
+ }),
+ )
+
+ const onDestroy$ = fromEvent(connectButtonElement, 'onDestroy').pipe(
+ tap(() => {
+ logger?.debug(`connectButtonRemovedFromDOM`)
+ }),
+ )
+
+ const onCancelRequestItem$ = fromEvent(
+ connectButtonElement,
+ 'onCancelRequestItem',
+ ).pipe(
+ tap((event) => {
+ const id = (event as CustomEvent<{ id: string }>).detail.id
+ logger?.debug({ method: 'onCancelRequestItem', id })
+ subjects.onCancelRequestItem.next(id)
+ }),
+ )
+
+ const onUpdateSharedData$ = fromEvent(
+ connectButtonElement,
+ 'onUpdateSharedData',
+ ).pipe(
+ tap(() => {
+ logger?.debug(`onUpdateSharedData`)
+ subjects.onUpdateSharedData.next()
+ }),
+ )
+
+ const onShowPopover$ = fromEvent(
+ connectButtonElement,
+ 'onShowPopover',
+ ).pipe(tap(() => subjects.onShowPopover.next()))
+
+ const isWalletLinked$ = subjects.isWalletLinked.pipe(
+ tap((value) => (connectButtonElement.isWalletLinked = value)),
+ )
+
+ const isExtensionAvailable$ = subjects.isExtensionAvailable.pipe(
+ tap((value) => (connectButtonElement.isExtensionAvailable = value)),
+ )
+
+ const status$ = subjects.status.pipe(
+ tap((value) => (connectButtonElement.status = value)),
+ )
+
+ const mode$ = subjects.mode.pipe(
+ tap((value) => (connectButtonElement.mode = value)),
+ )
+
+ const connected$ = subjects.connected.pipe(
+ tap((value) => (connectButtonElement.connected = value)),
+ )
+
+ const isMobile$ = subjects.isMobile.pipe(
+ tap((value) => (connectButtonElement.isMobile = value)),
+ )
+
+ const loggedInTimestamp$ = subjects.loggedInTimestamp.pipe(
+ tap((value) => (connectButtonElement.loggedInTimestamp = value)),
+ )
+
+ const activeTab$ = subjects.activeTab.pipe(
+ tap((value) => (connectButtonElement.activeTab = value)),
+ )
+
+ const requestItems$ = subjects.requestItems.pipe(
+ tap((items) => (connectButtonElement.requestItems = items)),
+ )
+
+ const accounts$ = subjects.accounts.pipe(
+ tap((items) => (connectButtonElement.accounts = items)),
+ )
+
+ const personaData$ = subjects.personaData.pipe(
+ tap((items) => (connectButtonElement.personaData = items)),
+ )
+
+ const personaLabel$ = subjects.personaLabel.pipe(
+ tap((items) => (connectButtonElement.personaLabel = items)),
+ )
+
+ const dAppName$ = subjects.dAppName.pipe(
+ tap((value) => (connectButtonElement.dAppName = value)),
+ )
+
+ const theme$ = subjects.theme.pipe(
+ tap((value) => (connectButtonElement.theme = value)),
+ )
+
+ return merge(
+ onConnect$,
+ status$,
+ theme$,
+ mode$,
+ connected$,
+ requestItems$,
+ loggedInTimestamp$,
+ isMobile$,
+ activeTab$,
+ isWalletLinked$,
+ isExtensionAvailable$,
+ onDisconnect$,
+ onCancelRequestItem$,
+ accounts$,
+ personaData$,
+ personaLabel$,
+ onDestroy$,
+ onUpdateSharedData$,
+ onShowPopover$,
+ dAppName$,
+ onLinkClick$,
+ )
+ }),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ ((transport && transport.isAvailable$) || of(true))
+ .pipe(tap((value) => subjects.isExtensionAvailable.next(value)))
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ ((transport && transport.isLinked$) || of(true))
+ .pipe(tap((value) => subjects.isWalletLinked.next(value)))
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ subjects.onLinkClick
+ .pipe(
+ tap(({ type, data }) => {
+ if (['account', 'transaction'].includes(type)) {
+ if (!baseUrl || !window) return
+
+ const url = `${baseUrl}${
+ type === 'transaction' ? transactionPath : accountsPath
+ }${data}`
+
+ window.open(url)
+ } else if (type === 'setupGuide')
+ window.open('https://wallet.radixdlt.com')
+ else if (type === 'showQrCode' && transport?.showQrCode)
+ transport.showQrCode()
+ }),
+ )
+ .subscribe(),
+ )
+
+ const connectButtonApi = {
+ status$: subjects.status.asObservable(),
+ onConnect$: subjects.onConnect.asObservable(),
+ onDisconnect$: subjects.onDisconnect.asObservable(),
+ onShowPopover$: subjects.onShowPopover.asObservable(),
+ onUpdateSharedData$: subjects.onUpdateSharedData.asObservable(),
+ onCancelRequestItem$: subjects.onCancelRequestItem.asObservable(),
+ onLinkClick$: subjects.onLinkClick.asObservable(),
+ setStatus: (value: RadixButtonStatus) => subjects.status.next(value),
+ setTheme: (value: RadixButtonTheme) => subjects.theme.next(value),
+ setMode: (value: 'light' | 'dark') => subjects.mode.next(value),
+ setActiveTab: (value: 'sharing' | 'requests') =>
+ subjects.activeTab.next(value),
+ setIsMobile: (value: boolean) => subjects.isMobile.next(value),
+ setIsWalletLinked: (value: boolean) => subjects.isWalletLinked.next(value),
+ setIsExtensionAvailable: (value: boolean) =>
+ subjects.isExtensionAvailable.next(value),
+ setLoggedInTimestamp: (value: string) =>
+ subjects.loggedInTimestamp.next(value),
+ setConnected: (value: boolean) => subjects.connected.next(value),
+ setRequestItems: (items: RequestItem[]) =>
+ subjects.requestItems.next(items),
+ setAccounts: (accounts: Account[]) => subjects.accounts.next(accounts),
+ setPersonaData: (personaData: { value: string; field: string }[]) =>
+ subjects.personaData.next(personaData),
+ setPersonaLabel: (personaLabel: string) =>
+ subjects.personaLabel.next(personaLabel),
+ setDappName: (dAppName: string) => subjects.dAppName.next(dAppName),
+ disconnect: () => {
+ subjects.connected.next(false)
+ subjects.status.next('default')
+ },
+ destroy: () => {
+ subscriptions.unsubscribe()
+ },
+ }
+
+ const setPropsFromState = () =>
+ stateClient.getState().map((state) => {
+ const { personaData, accounts, personaLabel, connected } =
+ transformWalletDataToConnectButton(state.walletData)
+
+ connectButtonApi.setLoggedInTimestamp(state.loggedInTimestamp)
+ connectButtonApi.setAccounts(accounts)
+ connectButtonApi.setPersonaData(personaData)
+ connectButtonApi.setPersonaLabel(personaLabel)
+ connectButtonApi.setConnected(connected)
+ })
+
+ subscriptions.add(
+ merge(stateClient.store.storage$, of(null))
+ .pipe(switchMap(() => setPropsFromState()))
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ subjects.onCancelRequestItem
+ .pipe(
+ tap((value) => {
+ walletRequestClient.cancelRequest(value)
+ }),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ walletRequestClient.requestItems$
+ .pipe(
+ tap((items) => {
+ const hasPendingItem = items.find((item) => item.status === 'pending')
+
+ if (hasPendingItem) {
+ connectButtonApi.setStatus('pending')
+ }
+
+ connectButtonApi.setRequestItems([...items].reverse())
+ }),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ subjects.onShowPopover
+ .pipe(
+ tap(() => {
+ walletRequestClient.getPendingRequests().map((pendingRequests) => {
+ if (pendingRequests.length > 0) {
+ subjects.activeTab.next('requests')
+ }
+ })
+ }),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ subjects.onConnect
+ .pipe(
+ switchMap(() =>
+ stateClient.reset().andThen(() =>
+ walletRequestClient.sendRequest({
+ isConnect: true,
+ oneTime: false,
+ }),
+ ),
+ ),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ subjects.onUpdateSharedData
+ .pipe(switchMap(() => walletRequestClient.updateSharedData()))
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ subjects.onDisconnect
+ .pipe(
+ tap(() => {
+ subjects.connected.next(false)
+ subjects.status.next('default')
+ walletRequestClient.disconnect()
+ if (input.onDisconnect) input.onDisconnect()
+ }),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ statusStorage.storage$
+ .pipe(
+ tap(({ newValue }) => {
+ if (newValue?.status) {
+ subjects.status.next(newValue.status)
+ }
+ }),
+ )
+ .subscribe(),
+ )
+
+ subscriptions.add(
+ walletRequestClient.interactionStatusChange$
+ .pipe(
+ mergeMap((newStatus) => {
+ statusStorage.setState({
+ status: newStatus === 'success' ? 'success' : 'error',
+ })
+
+ return timer(2000).pipe(
+ tap(() => {
+ const result = walletRequestClient.getPendingRequests()
+ result.map((pendingItems) => {
+ statusStorage.setState({
+ status: pendingItems.length ? 'pending' : 'default',
+ })
+ })
+ }),
+ )
+ }),
+ )
+ .subscribe(),
+ )
+
+ if (dAppDefinitionAddress) {
+ gatewayClient.gatewayApi
+ .getEntityMetadataPage(dAppDefinitionAddress)
+ .map(
+ (details) =>
+ details?.items.find((item) => item.key === 'name')?.value?.typed
+ ?.value,
+ )
+ .map((dAppName) => {
+ subjects.dAppName.next(dAppName ?? 'Unnamed dApp')
+ })
+ }
+
+ return connectButtonApi
+}
diff --git a/src/connect-button/subjects.ts b/packages/dapp-toolkit/src/connect-button/subjects.ts
similarity index 81%
rename from src/connect-button/subjects.ts
rename to packages/dapp-toolkit/src/connect-button/subjects.ts
index f93d3c9e..99f74fe8 100644
--- a/src/connect-button/subjects.ts
+++ b/packages/dapp-toolkit/src/connect-button/subjects.ts
@@ -1,5 +1,6 @@
-import { Account, RequestItem } from '@radixdlt/connect-button'
+import type { Account, RequestItem } from 'radix-connect-common'
import { BehaviorSubject, ReplaySubject, Subject } from 'rxjs'
+import { isMobile } from '../helpers'
export type ConnectButtonSubjects = ReturnType
export const ConnectButtonSubjects = () => ({
@@ -12,10 +13,10 @@ export const ConnectButtonSubjects = () => ({
accounts: new BehaviorSubject([]),
onShowPopover: new Subject(),
status: new BehaviorSubject<'pending' | 'success' | 'default' | 'error'>(
- 'default'
+ 'default',
),
loggedInTimestamp: new BehaviorSubject(''),
- isMobile: new BehaviorSubject(false),
+ isMobile: new BehaviorSubject(isMobile()),
isWalletLinked: new BehaviorSubject(false),
isExtensionAvailable: new BehaviorSubject(false),
fullWidth: new BehaviorSubject(false),
@@ -28,5 +29,8 @@ export const ConnectButtonSubjects = () => ({
personaLabel: new BehaviorSubject(''),
personaData: new BehaviorSubject<{ value: string; field: string }[]>([]),
dAppName: new BehaviorSubject(''),
- onLinkClick: new Subject<{ type: 'account' | 'transaction'; data: string }>(),
+ onLinkClick: new Subject<{
+ type: 'account' | 'transaction' | 'setupGuide' | 'showQrCode'
+ data: string
+ }>(),
})
diff --git a/packages/dapp-toolkit/src/error.ts b/packages/dapp-toolkit/src/error.ts
new file mode 100644
index 00000000..13c2a86d
--- /dev/null
+++ b/packages/dapp-toolkit/src/error.ts
@@ -0,0 +1,44 @@
+export const ErrorType = {
+ rejectedByUser: 'rejectedByUser',
+ missingExtension: 'missingExtension',
+ canceledByUser: 'canceledByUser',
+ walletRequestValidation: 'walletRequestValidation',
+ walletResponseValidation: 'walletResponseValidation',
+ wrongNetwork: 'wrongNetwork',
+ failedToPrepareTransaction: 'failedToPrepareTransaction',
+ failedToCompileTransaction: 'failedToCompileTransaction',
+ failedToSignTransaction: 'failedToSignTransaction',
+ failedToSubmitTransaction: 'failedToSubmitTransaction',
+ failedToPollSubmittedTransaction: 'failedToPollSubmittedTransaction',
+ submittedTransactionWasDuplicate: 'submittedTransactionWasDuplicate',
+ submittedTransactionHasFailedTransactionStatus:
+ 'submittedTransactionHasFailedTransactionStatus',
+ submittedTransactionHasRejectedTransactionStatus:
+ 'submittedTransactionHasRejectedTransactionStatus',
+ failedToFindAccountWithEnoughFundsToLockFee:
+ 'failedToFindAccountWithEnoughFundsToLockFee',
+ wrongAccountType: 'wrongAccountType',
+ unknownWebsite: 'unknownWebsite',
+ radixJsonNotFound: 'radixJsonNotFound',
+ unknownDappDefinitionAddress: 'unknownDappDefinitionAddress',
+ invalidPersona: 'invalidPersona',
+} as const
+
+type ErrorType = keyof typeof ErrorType
+
+const defaultErrorMessage = new Map()
+ .set(ErrorType.missingExtension, 'extension could not be found')
+ .set(ErrorType.rejectedByUser, 'user rejected request')
+ .set(ErrorType.canceledByUser, 'user has canceled the request')
+
+export type SdkError = ReturnType
+
+export const SdkError = (
+ error: string,
+ interactionId: string,
+ message?: string,
+) => ({
+ error,
+ interactionId,
+ message: message || defaultErrorMessage.get(error) || '',
+})
diff --git a/packages/dapp-toolkit/src/gateway/gateway-api.ts b/packages/dapp-toolkit/src/gateway/gateway-api.ts
new file mode 100644
index 00000000..1763f5a6
--- /dev/null
+++ b/packages/dapp-toolkit/src/gateway/gateway-api.ts
@@ -0,0 +1,48 @@
+import { EntityMetadataItem, TransactionStatus } from './types'
+import { fetchWrapper } from '../helpers'
+import { __VERSION__ } from '../version'
+
+export type GatewayApiClient = ReturnType
+
+export const GatewayApiClient = ({
+ basePath,
+ dAppDefinitionAddress,
+ applicationName,
+ applicationVersion,
+}: {
+ basePath: string
+ dAppDefinitionAddress?: string
+ applicationVersion?: string
+ applicationName?: string
+}) => {
+ const fetchWithHeaders = (url: string, body: any) =>
+ fetchWrapper(
+ fetch(`${basePath}${url}`, {
+ method: 'POST',
+ body: JSON.stringify(body),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'RDX-Client-Name': '@radixdlt/radix-dapp-toolkit',
+ 'RDX-Client-Version': __VERSION__,
+ 'RDX-App-Name': applicationName ?? 'Unknown',
+ 'RDX-App-Version': applicationVersion ?? 'Unknown',
+ 'RDX-App-Dapp-Definition': dAppDefinitionAddress ?? 'Unknown',
+ } as Record,
+ }),
+ ).map((response) => response.data)
+
+ const getTransactionStatus = (transactionIntentHash: string) =>
+ fetchWithHeaders<{ status: TransactionStatus }>('/transaction/status', {
+ intent_hash: transactionIntentHash,
+ })
+
+ const getEntityMetadataPage = (address: string) =>
+ fetchWithHeaders<{
+ items: EntityMetadataItem[]
+ }>('/state/entity/page/metadata', { address })
+
+ return {
+ getTransactionStatus,
+ getEntityMetadataPage,
+ }
+}
diff --git a/src/gateway/gateway.ts b/packages/dapp-toolkit/src/gateway/gateway.ts
similarity index 60%
rename from src/gateway/gateway.ts
rename to packages/dapp-toolkit/src/gateway/gateway.ts
index 8cf146d9..38f72aae 100644
--- a/src/gateway/gateway.ts
+++ b/packages/dapp-toolkit/src/gateway/gateway.ts
@@ -1,24 +1,10 @@
-import { GatewayApiClient } from './gateway-api'
-import { Result, ResultAsync } from 'neverthrow'
-import {
- EntityMetadataItem,
- TransactionStatus,
- TransactionStatusResponse,
-} from '@radixdlt/babylon-gateway-api-sdk'
+import type { GatewayApiClient } from './gateway-api'
+import type { Result } from 'neverthrow'
+import { ResultAsync, err } from 'neverthrow'
import { filter, first, firstValueFrom, switchMap } from 'rxjs'
-import {
- ExponentialBackoff,
- ExponentialBackoffInput,
-} from './helpers/exponential-backoff'
-import { AppLogger, createSdkError, SdkError } from '@radixdlt/wallet-sdk'
-
-export const MetadataValue = (value?: EntityMetadataItem) => {
- const typed: any = value?.value?.typed
-
- return {
- stringified: typed?.value ? typed?.value || '' : typed?.values.join(', '),
- }
-}
+import { ExponentialBackoffInput, Logger, ExponentialBackoff } from '../helpers'
+import { SdkError } from '../error'
+import { TransactionStatus, TransactionStatusResponse } from './types'
export type GatewayClient = ReturnType
@@ -28,10 +14,12 @@ export const GatewayClient = ({
retryConfig,
}: {
gatewayApi: GatewayApiClient
- logger?: AppLogger
+ logger?: Logger
retryConfig?: ExponentialBackoffInput
}) => {
- const pollTransactionStatus = (transactionIntentHash: string) => {
+ const pollTransactionStatus = (
+ transactionIntentHash: string,
+ ): ResultAsync => {
const retry = ExponentialBackoff(retryConfig)
const completedTransactionStatus = new Set([
@@ -44,7 +32,8 @@ export const GatewayClient = ({
firstValueFrom(
retry.withBackoff$.pipe(
switchMap((result) => {
- if (result.isErr()) return [result]
+ if (result.isErr())
+ return [err(SdkError('failedToPollSubmittedTransaction', ''))]
logger?.debug(`pollingTxStatus retry #${result.value + 1}`)
@@ -59,17 +48,17 @@ export const GatewayClient = ({
})
.mapErr((response) => {
logger?.debug(response)
- return createSdkError('failedToPollSubmittedTransaction', '')
+ return SdkError('failedToPollSubmittedTransaction', '')
})
}),
filter(
(result): result is Result =>
- (result.isOk() && !!result.value) || result.isErr()
+ (result.isOk() && !!result.value) || result.isErr(),
),
- first()
- )
+ first(),
+ ),
),
- (error) => error as SdkError
+ (error) => error as SdkError,
).andThen((result) => result)
}
diff --git a/packages/dapp-toolkit/src/gateway/types.ts b/packages/dapp-toolkit/src/gateway/types.ts
new file mode 100644
index 00000000..2e7ce320
--- /dev/null
+++ b/packages/dapp-toolkit/src/gateway/types.ts
@@ -0,0 +1,106 @@
+export type TransactionStatus =
+ (typeof TransactionStatus)[keyof typeof TransactionStatus]
+export const TransactionStatus = {
+ Unknown: 'Unknown',
+ CommittedSuccess: 'CommittedSuccess',
+ CommittedFailure: 'CommittedFailure',
+ Pending: 'Pending',
+ Rejected: 'Rejected',
+} as const
+
+export type MetadataStringValue = {
+ type: 'String'
+ value: string
+}
+
+export type MetadataTypedValue = { type: 'String' } & MetadataStringValue
+
+export type EntityMetadataItemValue = {
+ typed: MetadataTypedValue
+}
+
+export type EntityMetadataItem = {
+ key: string
+
+ value: EntityMetadataItemValue
+
+ is_locked: boolean
+
+ last_updated_at_state_version: number
+}
+
+export type TransactionStatusResponse = {
+ status: TransactionStatus
+}
+
+export const RadixNetwork = {
+ Mainnet: 0x01,
+ Stokenet: 0x02,
+ Gilganet: 0x20,
+ Enkinet: 0x21,
+ Hammunet: 0x22,
+ Nergalnet: 0x23,
+ Mardunet: 0x24,
+ Dumunet: 0x25,
+} as const
+
+export type NetworkConfig = {
+ networkName: string
+ networkId: (typeof RadixNetwork)[keyof typeof RadixNetwork]
+ gatewayUrl: string
+ dashboardUrl: string
+}
+
+export const RadixNetworkConfig: Record = {
+ Mainnet: {
+ networkName: 'Mainnet',
+ networkId: RadixNetwork.Mainnet,
+ gatewayUrl: 'https://mainnet.radixdlt.com',
+ dashboardUrl: 'https://dashboard.radixdlt.com',
+ },
+ Stokenet: {
+ networkName: 'Stokenet',
+ networkId: RadixNetwork.Stokenet,
+ gatewayUrl: 'https://babylon-stokenet-gateway.radixdlt.com',
+ dashboardUrl: 'https://stokenet-dashboard.radixdlt.com',
+ },
+
+ Mardunet: {
+ networkName: 'Mardunet',
+ networkId: RadixNetwork.Mardunet,
+ gatewayUrl: 'https://mardunet-gateway.radixdlt.com',
+ dashboardUrl: 'https://mardunet-dashboard.rdx-works-main.extratools.works',
+ },
+ Gilganet: {
+ networkName: 'Gilganet',
+ networkId: RadixNetwork.Gilganet,
+ gatewayUrl: 'https://gilganet-gateway.radixdlt.com',
+ dashboardUrl: 'https://gilganet-dashboard.rdx-works-main.extratools.works',
+ },
+ Enkinet: {
+ networkName: 'Enkinet',
+ networkId: RadixNetwork.Enkinet,
+ gatewayUrl: 'https://enkinet-gateway.radixdlt.com',
+ dashboardUrl: 'https://enkinet-dashboard.rdx-works-main.extratools.works',
+ },
+ Hammunet: {
+ networkName: 'Hammunet',
+ networkId: RadixNetwork.Hammunet,
+ gatewayUrl: 'https://hammunet-gateway.radixdlt.com',
+ dashboardUrl: 'https://hammunet-dashboard.rdx-works-main.extratools.works',
+ },
+ Dumunet: {
+ networkName: 'Dumunet',
+ networkId: RadixNetwork.Dumunet,
+ gatewayUrl: 'https://dumunet-gateway.radixdlt.com',
+ dashboardUrl: 'https://dumunet-dashboard.rdx-works-main.extratools.works',
+ },
+}
+
+export const RadixNetworkConfigById = Object.values(RadixNetworkConfig).reduce(
+ (prev: Record, config) => {
+ prev[config.networkId] = config
+ return prev
+ },
+ {},
+)
diff --git a/src/gateway/helpers/exponential-backoff.ts b/packages/dapp-toolkit/src/helpers/exponential-backoff.ts
similarity index 65%
rename from src/gateway/helpers/exponential-backoff.ts
rename to packages/dapp-toolkit/src/helpers/exponential-backoff.ts
index 148a655b..a2a42f37 100644
--- a/src/gateway/helpers/exponential-backoff.ts
+++ b/packages/dapp-toolkit/src/helpers/exponential-backoff.ts
@@ -1,6 +1,7 @@
-import { createSdkError, SdkError } from '@radixdlt/wallet-sdk'
-import { err, ok, Result } from 'neverthrow'
-import { map, merge, Observable, of, Subject, switchMap, timer } from 'rxjs'
+import type { Result } from 'neverthrow'
+import { err, ok } from 'neverthrow'
+import type { Observable } from 'rxjs'
+import { map, merge, of, Subject, switchMap, timer } from 'rxjs'
export type ExponentialBackoffInput = {
multiplier?: number
@@ -24,24 +25,23 @@ export const ExponentialBackoff = ({
map(() => {
numberOfRetries = numberOfRetries + 1
return numberOfRetries
- })
- )
+ }),
+ ),
).pipe(
switchMap((numberOfRetries) => {
const delayTime = numberOfRetries * interval * multiplier
const delay = delayTime > maxDelayTime ? maxDelayTime : delayTime
return timer(delay).pipe(map(() => ok(numberOfRetries)))
- })
+ }),
)
- const withBackoffAndTimeout$: Observable> = timeout
- ? merge(
- backoff$,
- timer(timeout).pipe(
- map(() => err(createSdkError('failedToPollSubmittedTransaction', '')))
+ const withBackoffAndTimeout$: Observable> =
+ timeout
+ ? merge(
+ backoff$,
+ timer(timeout).pipe(map(() => err({ error: 'timeout' }))),
)
- )
- : backoff$
+ : backoff$
return { trigger, withBackoff$: withBackoffAndTimeout$ }
}
diff --git a/packages/dapp-toolkit/src/helpers/fetch-wrapper.ts b/packages/dapp-toolkit/src/helpers/fetch-wrapper.ts
new file mode 100644
index 00000000..1068f821
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/fetch-wrapper.ts
@@ -0,0 +1,39 @@
+import { ResultAsync, errAsync, okAsync } from 'neverthrow'
+import { parseJSON } from './parse-json'
+
+const typedError = (error: unknown) => error as E
+
+const resolveFetch = (fetchable: ReturnType) =>
+ ResultAsync.fromPromise(fetchable, typedError).mapErr((error) => ({
+ reason: 'FailedToFetch',
+ error,
+ status: 0,
+ }))
+
+export const fetchWrapper = (
+ fetchable: ReturnType,
+): ResultAsync<
+ { status: number; data: R },
+ { status: number; error?: Error; reason: string; data?: ER }
+> =>
+ resolveFetch(fetchable).andThen((response) =>
+ ResultAsync.fromPromise(response.text(), typedError)
+ .andThen((text) => (text ? parseJSON(text as string) : okAsync(text)))
+ .mapErr((error) => ({
+ status: response.status,
+ reason: 'FailedToParseResponseToJson',
+ error,
+ }))
+ .andThen((data) =>
+ response.ok
+ ? okAsync({
+ status: response.status,
+ data: data as R,
+ })
+ : errAsync({
+ status: response.status,
+ reason: 'RequestStatusNotOk',
+ data: data as ER,
+ }),
+ ),
+ )
diff --git a/packages/dapp-toolkit/src/helpers/index.ts b/packages/dapp-toolkit/src/helpers/index.ts
new file mode 100644
index 00000000..4cd5dda5
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/index.ts
@@ -0,0 +1,10 @@
+export * from './exponential-backoff'
+export * from './fetch-wrapper'
+export * from './is-mobile'
+export * from './logger'
+export * from './parse-json'
+export * from './remove-undefined'
+export * from './stringify'
+export * from './typed-error'
+export * from './unwrap-observable'
+export * from './validate-wallet-response'
diff --git a/packages/dapp-toolkit/src/helpers/is-deep-equal.spec.ts b/packages/dapp-toolkit/src/helpers/is-deep-equal.spec.ts
new file mode 100644
index 00000000..c768c4ed
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/is-deep-equal.spec.ts
@@ -0,0 +1,118 @@
+import { describe, it, expect } from 'vitest'
+import { isDeepEqual } from './is-deep-equal'
+
+describe('isDeepEqual', () => {
+ const truthyTestCases = [
+ [{}, {}],
+ [[], []],
+ [undefined, undefined],
+ [null, null],
+ [true, true],
+ [false, false],
+ ['abc', 'abc'],
+ [123, 123],
+ [{ a: 1 }, { a: 1 }],
+ [
+ {
+ walletData: {
+ accounts: [
+ {
+ address:
+ 'account_rdx12y4l35lh2543nff9pyyzvsh64ssu0dv6fq20gg8suslwmjvkylejgj',
+ label: 'First',
+ appearanceId: 0,
+ },
+ ],
+ personaData: [],
+ persona: {
+ identityAddress:
+ 'identity_rdx122dsgw5n3dqng989nrpa90ah96y8xyz9kxfwu7luttj47ggn799qdk',
+ label: 'SowaRdx',
+ },
+ proofs: [],
+ },
+ sharedData: {
+ persona: {
+ proof: true,
+ },
+ ongoingAccounts: {
+ numberOfAccounts: {
+ quantifier: 'atLeast',
+ quantity: 1,
+ },
+ proof: false,
+ },
+ },
+ loggedInTimestamp: '1703841637620',
+ },
+ {
+ walletData: {
+ accounts: [
+ {
+ label: 'First',
+ address:
+ 'account_rdx12y4l35lh2543nff9pyyzvsh64ssu0dv6fq20gg8suslwmjvkylejgj',
+
+ appearanceId: 0,
+ },
+ ],
+ personaData: [],
+ persona: {
+ label: 'SowaRdx',
+ identityAddress:
+ 'identity_rdx122dsgw5n3dqng989nrpa90ah96y8xyz9kxfwu7luttj47ggn799qdk',
+ },
+ proofs: [],
+ },
+ loggedInTimestamp: '1703841637620',
+ sharedData: {
+ ongoingAccounts: {
+ proof: false,
+ numberOfAccounts: {
+ quantifier: 'atLeast',
+ quantity: 1,
+ },
+ },
+ persona: {
+ proof: true,
+ },
+ },
+ },
+ ],
+ ]
+
+ const falsyTestCases = [
+ [{}, { a: 1 }],
+ [undefined, {}],
+ [null, {}],
+ ['abc', 123],
+ [1234, 1],
+ [123, {}],
+ ['123', 123],
+ [true, false],
+ [{ a: 1 }, { a: 1, b: 2 }],
+ [{ a: 1 }, { b: 1 }],
+ [{ a: 1 }, { a: 2 }],
+ ['a', []],
+ [{ a: 1, b: 2 }, { a: 1 }],
+ ]
+ it('should return true if two objects are deeply equal', () => {
+ truthyTestCases.forEach(([a, b]) => {
+ const comparison = isDeepEqual(a, b)
+ if (comparison === false) {
+ console.log('Failed with:', a, b)
+ }
+ expect(comparison).toBe(true)
+ })
+ })
+
+ it('should return false if two objects are not deeply equal', () => {
+ falsyTestCases.forEach(([a, b]) => {
+ const comparison = isDeepEqual(a, b)
+ if (comparison === true) {
+ console.log('Failed with:', a, b)
+ }
+ expect(comparison).toBe(false)
+ })
+ })
+})
diff --git a/packages/dapp-toolkit/src/helpers/is-deep-equal.ts b/packages/dapp-toolkit/src/helpers/is-deep-equal.ts
new file mode 100644
index 00000000..69853689
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/is-deep-equal.ts
@@ -0,0 +1,35 @@
+export const isDeepEqual = (a: any, b: any): boolean => {
+ const values = [null, undefined, false, true]
+ if (
+ values.includes(a) ||
+ values.includes(b) ||
+ typeof a === 'number' ||
+ typeof b === 'number'
+ ) {
+ return Object.is(a, b)
+ }
+
+ const aKeys = Object.keys(a)
+ const bKeys = Object.keys(b)
+
+ if (aKeys.length !== bKeys.length) return false
+
+ for (const key of aKeys) {
+ const value1 = a[key]
+ const value2 = b[key]
+
+ const isObjects = isObject(value1) && isObject(value2)
+
+ if (
+ (isObjects && !isDeepEqual(value1, value2)) ||
+ (!isObjects && value1 !== value2)
+ ) {
+ return false
+ }
+ }
+ return true
+}
+
+const isObject = (x: unknown): boolean => {
+ return x != null && typeof x === 'object'
+}
diff --git a/packages/dapp-toolkit/src/helpers/is-mobile.ts b/packages/dapp-toolkit/src/helpers/is-mobile.ts
new file mode 100644
index 00000000..81847657
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/is-mobile.ts
@@ -0,0 +1,6 @@
+import Bowser from 'bowser'
+
+export const isMobile = () => {
+ const userAgent = Bowser.parse(window.navigator.userAgent)
+ return userAgent.platform.type === 'mobile'
+}
diff --git a/packages/dapp-toolkit/src/helpers/logger.ts b/packages/dapp-toolkit/src/helpers/logger.ts
new file mode 100644
index 00000000..83118bc2
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/logger.ts
@@ -0,0 +1,9 @@
+import { Logger as TsLogger } from 'tslog'
+
+export type Logger = ReturnType
+export const Logger = (minLevel?: number) =>
+ new TsLogger({
+ minLevel: minLevel ?? 2,
+ prettyLogTemplate:
+ '{{hh}}:{{MM}}:{{ss}}:{{ms}}\t{{name}}\t{{logLevelName}}\t',
+ })
diff --git a/packages/dapp-toolkit/src/helpers/parse-json.ts b/packages/dapp-toolkit/src/helpers/parse-json.ts
new file mode 100644
index 00000000..37c03c7b
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/parse-json.ts
@@ -0,0 +1,13 @@
+import type { Result } from 'neverthrow'
+import { err, ok } from 'neverthrow'
+import { typedError } from './typed-error'
+
+export const parseJSON = >(
+ text: string,
+): Result => {
+ try {
+ return ok(JSON.parse(text))
+ } catch (error) {
+ return err(typedError(error))
+ }
+}
diff --git a/src/helpers/remove-undefined.ts b/packages/dapp-toolkit/src/helpers/remove-undefined.ts
similarity index 100%
rename from src/helpers/remove-undefined.ts
rename to packages/dapp-toolkit/src/helpers/remove-undefined.ts
diff --git a/src/helpers/stringify.ts b/packages/dapp-toolkit/src/helpers/stringify.ts
similarity index 68%
rename from src/helpers/stringify.ts
rename to packages/dapp-toolkit/src/helpers/stringify.ts
index a0a4c848..890eaf8d 100644
--- a/src/helpers/stringify.ts
+++ b/packages/dapp-toolkit/src/helpers/stringify.ts
@@ -1,4 +1,5 @@
-import { err, ok, Result } from 'neverthrow'
+import type { Result } from 'neverthrow'
+import { err, ok } from 'neverthrow'
export const stringify = (input: any): Result => {
try {
diff --git a/packages/dapp-toolkit/src/helpers/typed-error.ts b/packages/dapp-toolkit/src/helpers/typed-error.ts
new file mode 100644
index 00000000..44cf6e8f
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/typed-error.ts
@@ -0,0 +1 @@
+export const typedError = (error: unknown) => error as E
diff --git a/packages/dapp-toolkit/src/helpers/unwrap-observable.ts b/packages/dapp-toolkit/src/helpers/unwrap-observable.ts
new file mode 100644
index 00000000..8d65ec7e
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/unwrap-observable.ts
@@ -0,0 +1,11 @@
+import { Result, ResultAsync } from 'neverthrow'
+import { firstValueFrom, Observable } from 'rxjs'
+import { SdkError } from '../error'
+
+export const unwrapObservable = (
+ input: Observable>,
+): ResultAsync =>
+ ResultAsync.fromPromise(
+ firstValueFrom(input),
+ (error) => error as SdkError,
+ ).andThen((result) => result)
diff --git a/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts b/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts
new file mode 100644
index 00000000..2aee72f5
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/validate-wallet-response.spec.ts
@@ -0,0 +1,73 @@
+import { describe, it, expect } from 'vitest'
+import { validateWalletResponse } from './validate-wallet-response'
+
+describe('validateWalletResponse', () => {
+ it('should parse valid response', async () => {
+ const walletResponse = {
+ discriminator: 'success',
+ interactionId: 'ab0f0190-1ae1-424b-a2c5-c36838f5b136',
+ items: {
+ discriminator: 'authorizedRequest',
+ ongoingAccounts: {
+ accounts: [
+ {
+ appearanceId: 0,
+ label: 'S1',
+ address:
+ 'account_tdx_2_128w9r3yel9pgk9vkmlg0wy6nsna9uhtw6dsfgrs8vsklqttjgx9c05',
+ },
+ ],
+ },
+ auth: {
+ persona: {
+ label: 'S1',
+ identityAddress:
+ 'identity_tdx_2_1220nxz6va4u939286usk632ersvvd0y6m8xeyg6l09ays99env6u77',
+ },
+ challenge:
+ '3f30f8d67ca69af8b646170d6ddd0a16cb501dcb7d457d0b49ef78a5d1b4beac',
+ discriminator: 'loginWithChallenge',
+ proof: {
+ curve: 'curve25519',
+ signature:
+ 'd3d049ec2722126bee265798b2daee3de46e398fd964cb08659c5fc434b1117d9e86dcde0b85c29f75f651878fe24fe326dc6536355c03d659268f17ce117b0b',
+ publicKey:
+ 'eb670d10083535f9148ca065e05b8516e1284027a7ef6a37d5dd5ecd1f485bc5',
+ },
+ },
+ },
+ }
+
+ const result = await validateWalletResponse(walletResponse)
+
+ expect(result.isOk() && result.value).toEqual(walletResponse)
+ })
+
+ it('should return error for invalid response', async () => {
+ const walletResponse = {}
+
+ const result = await validateWalletResponse(walletResponse)
+
+ expect(result.isErr() && result.error).toEqual({
+ error: 'walletResponseValidation',
+ interactionId: '',
+ message: 'Invalid input',
+ })
+ })
+
+ it('should return error valid failure response', async () => {
+ const walletResponse = {
+ discriminator: 'failure',
+ interactionId: '8cefec84-542d-40af-8782-b89df05db8ac',
+ error: 'rejectedByUser',
+ }
+
+ const result = await validateWalletResponse(walletResponse)
+
+ expect(result.isErr() && result.error).toEqual({
+ discriminator: 'failure',
+ interactionId: '8cefec84-542d-40af-8782-b89df05db8ac',
+ error: 'rejectedByUser',
+ })
+ })
+})
diff --git a/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts b/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts
new file mode 100644
index 00000000..e9f8f072
--- /dev/null
+++ b/packages/dapp-toolkit/src/helpers/validate-wallet-response.ts
@@ -0,0 +1,27 @@
+import { Result, ResultAsync, errAsync, okAsync } from 'neverthrow'
+import {
+ WalletInteractionResponse,
+ WalletInteractionSuccessResponse,
+} from '../schemas'
+import { SdkError } from '../error'
+import { ValiError, parse } from 'valibot'
+
+export const validateWalletResponse = (
+ walletResponse: unknown,
+): ResultAsync => {
+ const fn = Result.fromThrowable(
+ (_) => parse(WalletInteractionResponse, _),
+ (error) => error as ValiError,
+ )
+
+ const result = fn(walletResponse)
+ if (result.isErr()) {
+ return errAsync(SdkError('walletResponseValidation', '', 'Invalid input'))
+ } else if (result.isOk()) {
+ return result.value.discriminator === 'success'
+ ? okAsync(result.value)
+ : errAsync(result.value as any)
+ }
+
+ return errAsync(SdkError('walletResponseValidation', ''))
+}
diff --git a/packages/dapp-toolkit/src/index.ts b/packages/dapp-toolkit/src/index.ts
new file mode 100644
index 00000000..692dba94
--- /dev/null
+++ b/packages/dapp-toolkit/src/index.ts
@@ -0,0 +1,8 @@
+export { Logger } from './helpers/logger'
+export { RadixDappToolkit } from './radix-dapp-toolkit'
+export { LocalStorageClient } from './storage/local-storage-client'
+export { RadixNetwork } from './gateway/types'
+export * from './state'
+export * from './schemas'
+export * from './_types'
+export * from './wallet-request'
diff --git a/packages/dapp-toolkit/src/radix-dapp-toolkit.spec.ts b/packages/dapp-toolkit/src/radix-dapp-toolkit.spec.ts
new file mode 100644
index 00000000..4b1c1662
--- /dev/null
+++ b/packages/dapp-toolkit/src/radix-dapp-toolkit.spec.ts
@@ -0,0 +1,12 @@
+import { RadixDappToolkit } from './radix-dapp-toolkit'
+import { describe, it } from 'vitest'
+
+describe('RadixDappToolkit', () => {
+ it('should bootstrap RDT', async () => {
+ RadixDappToolkit({
+ dAppDefinitionAddress:
+ 'account_tdx_c_1p9c4zhvusrae49fguwm2cuxvltqquzxqex8ddr32e30qjlesen',
+ networkId: 2,
+ })
+ })
+})
diff --git a/packages/dapp-toolkit/src/radix-dapp-toolkit.ts b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts
new file mode 100644
index 00000000..2db52ae2
--- /dev/null
+++ b/packages/dapp-toolkit/src/radix-dapp-toolkit.ts
@@ -0,0 +1,136 @@
+import { ConnectButtonClient } from './connect-button/connect-button-client'
+import {
+ ButtonApi,
+ RadixDappToolkitOptions,
+ SendTransactionInput,
+ WalletApi,
+} from './_types'
+import { LocalStorageClient } from './storage'
+import { GatewayClient } from './gateway/gateway'
+import { GatewayApiClient } from './gateway/gateway-api'
+import { WalletRequestClient } from './wallet-request'
+import { StateClient, WalletData } from './state'
+import { RadixNetworkConfigById } from './gateway/types'
+
+export type RadixDappToolkit = {
+ walletApi: WalletApi
+ buttonApi: ButtonApi
+ disconnect: () => void
+ destroy: () => void
+}
+
+export const RadixDappToolkit = (
+ options: RadixDappToolkitOptions,
+): RadixDappToolkit => {
+ const {
+ dAppDefinitionAddress,
+ networkId,
+ providers,
+ logger,
+ onDisconnect,
+ gatewayBaseUrl,
+ applicationName,
+ applicationVersion,
+ useCache = true,
+ enableMobile = false,
+ } = options || {}
+
+ const storageClient =
+ providers?.storageClient ??
+ LocalStorageClient(`rdt:${dAppDefinitionAddress}:${networkId}`)
+
+ const stateClient =
+ providers?.stateClient ??
+ StateClient({
+ logger,
+ providers: {
+ storageClient: storageClient.getPartition('state'),
+ },
+ })
+
+ const gatewayClient =
+ providers?.gatewayClient ??
+ GatewayClient({
+ logger,
+ gatewayApi: GatewayApiClient({
+ basePath:
+ gatewayBaseUrl ?? RadixNetworkConfigById[networkId].gatewayUrl,
+ dAppDefinitionAddress,
+ applicationName,
+ applicationVersion,
+ }),
+ })
+
+ const walletRequestClient =
+ providers?.walletRequestClient ??
+ WalletRequestClient({
+ logger,
+ useCache,
+ networkId,
+ dAppDefinitionAddress,
+ requestInterceptor: options.requestInterceptor,
+ providers: {
+ stateClient,
+ storageClient,
+ gatewayClient,
+ transports: options.providers?.transports,
+ },
+ })
+
+ const connectButtonClient =
+ providers?.connectButton ??
+ ConnectButtonClient({
+ logger,
+ networkId,
+ explorer: options.explorer,
+ enableMobile,
+ onDisconnect,
+ dAppDefinitionAddress,
+ providers: {
+ stateClient,
+ walletRequestClient,
+ gatewayClient,
+ storageClient: options.providers?.storageClient,
+ },
+ })
+
+ return {
+ walletApi: {
+ setRequestData: walletRequestClient.setRequestDataState,
+ sendRequest: () =>
+ walletRequestClient.sendRequest({
+ isConnect: false,
+ oneTime: false,
+ }),
+
+ provideChallengeGenerator: (fn: () => Promise) =>
+ walletRequestClient.provideChallengeGenerator(fn),
+ dataRequestControl: (fn: (walletData: WalletData) => Promise) => {
+ walletRequestClient.provideDataRequestControl(fn)
+ },
+ provideConnectResponseCallback:
+ walletRequestClient.provideConnectResponseCallback,
+ updateSharedData: () => walletRequestClient.updateSharedData(),
+ sendOneTimeRequest: walletRequestClient.sendOneTimeRequest,
+ sendTransaction: (input: SendTransactionInput) =>
+ walletRequestClient.sendTransaction(input),
+ walletData$: stateClient.walletData$,
+ getWalletData: stateClient.getWalletData,
+ } satisfies WalletApi,
+ buttonApi: {
+ setTheme: connectButtonClient.setTheme,
+ setMode: connectButtonClient.setMode,
+ status$: connectButtonClient.status$,
+ },
+ disconnect: () => {
+ walletRequestClient.disconnect()
+ connectButtonClient.disconnect()
+ if (onDisconnect) onDisconnect()
+ },
+ destroy: () => {
+ stateClient.destroy()
+ walletRequestClient.destroy()
+ connectButtonClient.destroy()
+ },
+ }
+}
diff --git a/packages/dapp-toolkit/src/schemas/index.ts b/packages/dapp-toolkit/src/schemas/index.ts
new file mode 100644
index 00000000..caf996d3
--- /dev/null
+++ b/packages/dapp-toolkit/src/schemas/index.ts
@@ -0,0 +1,575 @@
+import type { ResultAsync } from 'neverthrow'
+import {
+ array,
+ boolean,
+ literal,
+ number,
+ object,
+ optional,
+ minValue,
+ string,
+ union,
+ merge,
+ Output,
+ ValiError,
+ custom,
+} from 'valibot'
+
+/**
+ * Wallet schemas
+ */
+
+export type Account = Output
+export const Account = object({
+ address: string(),
+ label: string(),
+ appearanceId: number(),
+})
+
+export type Proof = Output
+export const Proof = object({
+ publicKey: string(),
+ signature: string(),
+ curve: union([literal('curve25519'), literal('secp256k1')]),
+})
+
+export type AccountProof = Output
+export const AccountProof = object({
+ accountAddress: string(),
+ proof: Proof,
+})
+
+export type Persona = Output
+export const Persona = object({ identityAddress: string(), label: string() })
+
+export const personaDataFullNameVariant = {
+ western: 'western',
+ eastern: 'eastern',
+} as const
+export type PersonaDataNameVariant = Output
+export const PersonaDataNameVariant = union([
+ literal(personaDataFullNameVariant.eastern),
+ literal(personaDataFullNameVariant.western),
+])
+
+export type PersonaDataName = Output
+export const PersonaDataName = object({
+ variant: PersonaDataNameVariant,
+ familyName: string(),
+ nickname: string(),
+ givenNames: string(),
+})
+
+export type NumberOfValues = Output
+export const NumberOfValues = object({
+ quantifier: union([literal('exactly'), literal('atLeast')]),
+ quantity: number([minValue(0, 'The number must be at least 0.')]),
+})
+
+export type AccountsRequestItem = Output
+export const AccountsRequestItem = object({
+ challenge: optional(string()),
+ numberOfAccounts: NumberOfValues,
+})
+
+export type AccountsRequestResponseItem = Output<
+ typeof AccountsRequestResponseItem
+>
+export const AccountsRequestResponseItem = object(
+ {
+ accounts: array(Account),
+ challenge: optional(string()),
+ proofs: optional(array(AccountProof)),
+ },
+ [
+ custom((data) => {
+ if (data.challenge || data?.proofs) {
+ return !!(data.challenge && data?.proofs?.length)
+ }
+ return true
+ }, 'missing challenge or proofs'),
+ ],
+)
+
+export type PersonaDataRequestItem = Output
+export const PersonaDataRequestItem = object({
+ isRequestingName: optional(boolean()),
+ numberOfRequestedEmailAddresses: optional(NumberOfValues),
+ numberOfRequestedPhoneNumbers: optional(NumberOfValues),
+})
+
+export type PersonaDataRequestResponseItem = Output<
+ typeof PersonaDataRequestResponseItem
+>
+export const PersonaDataRequestResponseItem = object({
+ name: optional(PersonaDataName),
+ emailAddresses: optional(array(string())),
+ phoneNumbers: optional(array(string())),
+})
+
+export type ResetRequestItem = Output
+export const ResetRequestItem = object({
+ accounts: boolean(),
+ personaData: boolean(),
+})
+
+export type LoginRequestResponseItem = Output
+export const LoginRequestResponseItem = object(
+ {
+ persona: Persona,
+ challenge: optional(string()),
+ proof: optional(Proof),
+ },
+ [
+ custom((data) => {
+ if (data.challenge || data.proof) {
+ return !!(data.challenge && data.proof)
+ }
+ return true
+ }, 'missing challenge or proof'),
+ ],
+)
+
+export type WalletUnauthorizedRequestItems = Output<
+ typeof WalletUnauthorizedRequestItems
+>
+export const WalletUnauthorizedRequestItems = object({
+ discriminator: literal('unauthorizedRequest'),
+ oneTimeAccounts: optional(AccountsRequestItem),
+ oneTimePersonaData: optional(PersonaDataRequestItem),
+})
+
+export type AuthUsePersonaRequestItem = Output
+export const AuthUsePersonaRequestItem = object({
+ discriminator: literal('usePersona'),
+ identityAddress: string(),
+})
+
+export type AuthLoginWithoutChallengeRequestItem = Output<
+ typeof AuthLoginWithoutChallengeRequestItem
+>
+export const AuthLoginWithoutChallengeRequestItem = object({
+ discriminator: literal('loginWithoutChallenge'),
+})
+
+export type AuthLoginWithChallengeRequestItem = Output<
+ typeof AuthLoginWithChallengeRequestItem
+>
+export const AuthLoginWithChallengeRequestItem = object({
+ discriminator: literal('loginWithChallenge'),
+ challenge: string(),
+})
+
+export const AuthLoginRequestItem = union([
+ AuthLoginWithoutChallengeRequestItem,
+ AuthLoginWithChallengeRequestItem,
+])
+export const AuthRequestItem = union([
+ AuthUsePersonaRequestItem,
+ AuthLoginRequestItem,
+])
+
+export type WalletAuthorizedRequestItems = Output<
+ typeof WalletAuthorizedRequestItems
+>
+export const WalletAuthorizedRequestItems = object({
+ discriminator: literal('authorizedRequest'),
+ auth: AuthRequestItem,
+ reset: optional(ResetRequestItem),
+ oneTimeAccounts: optional(AccountsRequestItem),
+ ongoingAccounts: optional(AccountsRequestItem),
+ oneTimePersonaData: optional(PersonaDataRequestItem),
+ ongoingPersonaData: optional(PersonaDataRequestItem),
+})
+
+export type WalletRequestItems = Output
+export const WalletRequestItems = union([
+ WalletUnauthorizedRequestItems,
+ WalletAuthorizedRequestItems,
+])
+
+export type SendTransactionItem = Output
+export const SendTransactionItem = object({
+ transactionManifest: string(),
+ version: number(),
+ blobs: optional(array(string())),
+ message: optional(string()),
+})
+
+export type WalletTransactionItems = Output
+export const WalletTransactionItems = object({
+ discriminator: literal('transaction'),
+ send: SendTransactionItem,
+})
+
+export type SendTransactionResponseItem = Output<
+ typeof SendTransactionResponseItem
+>
+export const SendTransactionResponseItem = object({
+ transactionIntentHash: string(),
+})
+
+export type WalletTransactionResponseItems = Output<
+ typeof WalletTransactionResponseItems
+>
+const WalletTransactionResponseItems = object({
+ discriminator: literal('transaction'),
+ send: SendTransactionResponseItem,
+})
+
+export type CancelRequest = Output
+export const CancelRequest = object({
+ discriminator: literal('cancelRequest'),
+})
+
+export type WalletInteractionItems = Output
+export const WalletInteractionItems = union([
+ WalletRequestItems,
+ WalletTransactionItems,
+ CancelRequest,
+])
+
+export type Metadata = Output
+export const Metadata = object({
+ version: literal(2),
+ networkId: number(),
+ dAppDefinitionAddress: string(),
+ origin: string(),
+})
+
+export type MetadataWithOrigin = Output
+export const MetadataWithOrigin = merge([
+ Metadata,
+ object({ origin: string() }),
+])
+
+export type WalletInteraction = Output
+export const WalletInteraction = object({
+ interactionId: string(),
+ metadata: Metadata,
+ items: WalletInteractionItems,
+})
+
+export type WalletInteractionWithOrigin = Output<
+ typeof WalletInteractionWithOrigin
+>
+
+export const WalletInteractionWithOrigin = merge([
+ WalletInteraction,
+ object({ metadata: MetadataWithOrigin }),
+])
+
+export type WalletUnauthorizedRequestResponseItems = Output<
+ typeof WalletUnauthorizedRequestResponseItems
+>
+const WalletUnauthorizedRequestResponseItems = object({
+ discriminator: literal('unauthorizedRequest'),
+ oneTimeAccounts: optional(AccountsRequestResponseItem),
+ oneTimePersonaData: optional(PersonaDataRequestResponseItem),
+})
+
+export type AuthLoginWithoutChallengeRequestResponseItem = Output<
+ typeof AuthLoginWithoutChallengeRequestResponseItem
+>
+export const AuthLoginWithoutChallengeRequestResponseItem = object({
+ discriminator: literal('loginWithoutChallenge'),
+ persona: Persona,
+})
+
+export type AuthLoginWithChallengeRequestResponseItem = Output<
+ typeof AuthLoginWithChallengeRequestResponseItem
+>
+export const AuthLoginWithChallengeRequestResponseItem = object({
+ discriminator: literal('loginWithChallenge'),
+ persona: Persona,
+ challenge: string(),
+ proof: Proof,
+})
+
+export const AuthLoginRequestResponseItem = union([
+ AuthLoginWithoutChallengeRequestResponseItem,
+ AuthLoginWithChallengeRequestResponseItem,
+])
+
+export type AuthUsePersonaRequestResponseItem = Output<
+ typeof AuthUsePersonaRequestResponseItem
+>
+const AuthUsePersonaRequestResponseItem = object({
+ discriminator: literal('usePersona'),
+ persona: Persona,
+})
+
+export type AuthRequestResponseItem = Output
+export const AuthRequestResponseItem = union([
+ AuthUsePersonaRequestResponseItem,
+ AuthLoginRequestResponseItem,
+])
+
+export type WalletAuthorizedRequestResponseItems = Output<
+ typeof WalletAuthorizedRequestResponseItems
+>
+export const WalletAuthorizedRequestResponseItems = object({
+ discriminator: literal('authorizedRequest'),
+ auth: AuthRequestResponseItem,
+ oneTimeAccounts: optional(AccountsRequestResponseItem),
+ ongoingAccounts: optional(AccountsRequestResponseItem),
+ oneTimePersonaData: optional(PersonaDataRequestResponseItem),
+ ongoingPersonaData: optional(PersonaDataRequestResponseItem),
+})
+
+export type WalletRequestResponseItems = Output<
+ typeof WalletRequestResponseItems
+>
+export const WalletRequestResponseItems = union([
+ WalletUnauthorizedRequestResponseItems,
+ WalletAuthorizedRequestResponseItems,
+])
+
+export type WalletInteractionResponseItems = Output<
+ typeof WalletInteractionResponseItems
+>
+const WalletInteractionResponseItems = union([
+ WalletRequestResponseItems,
+ WalletTransactionResponseItems,
+])
+
+export type WalletInteractionSuccessResponse = Output<
+ typeof WalletInteractionSuccessResponse
+>
+export const WalletInteractionSuccessResponse = object({
+ discriminator: literal('success'),
+ interactionId: string(),
+ items: WalletInteractionResponseItems,
+})
+
+export type WalletInteractionFailureResponse = Output<
+ typeof WalletInteractionFailureResponse
+>
+export const WalletInteractionFailureResponse = object({
+ discriminator: literal('failure'),
+ interactionId: string(),
+ error: string(),
+ message: optional(string()),
+})
+
+export type WalletInteractionResponse = Output
+export const WalletInteractionResponse = union([
+ WalletInteractionSuccessResponse,
+ WalletInteractionFailureResponse,
+])
+
+export const extensionInteractionDiscriminator = {
+ extensionStatus: 'extensionStatus',
+ openPopup: 'openPopup',
+} as const
+
+export const StatusExtensionInteraction = object({
+ interactionId: string(),
+ discriminator: literal(extensionInteractionDiscriminator.extensionStatus),
+})
+
+export type StatusExtensionInteraction = Output<
+ typeof StatusExtensionInteraction
+>
+
+export const OpenPopupExtensionInteraction = object({
+ interactionId: string(),
+ discriminator: literal(extensionInteractionDiscriminator.openPopup),
+})
+
+export type OpenPopupExtensionInteraction = Output<
+ typeof OpenPopupExtensionInteraction
+>
+
+export const ExtensionInteraction = union([
+ StatusExtensionInteraction,
+ OpenPopupExtensionInteraction,
+])
+
+export type ExtensionInteraction = Output
+
+export const messageLifeCycleEventType = {
+ extensionStatus: 'extensionStatus',
+ receivedByExtension: 'receivedByExtension',
+ receivedByWallet: 'receivedByWallet',
+ requestCancelSuccess: 'requestCancelSuccess',
+ requestCancelFail: 'requestCancelFail',
+} as const
+
+export const MessageLifeCycleExtensionStatusEvent = object({
+ eventType: literal(messageLifeCycleEventType.extensionStatus),
+ interactionId: string(),
+ isWalletLinked: boolean(),
+ isExtensionAvailable: boolean(),
+})
+
+export type MessageLifeCycleExtensionStatusEvent = Output<
+ typeof MessageLifeCycleExtensionStatusEvent
+>
+
+export const MessageLifeCycleEvent = object({
+ eventType: union([
+ literal(messageLifeCycleEventType.extensionStatus),
+ literal(messageLifeCycleEventType.receivedByExtension),
+ literal(messageLifeCycleEventType.receivedByWallet),
+ literal(messageLifeCycleEventType.requestCancelSuccess),
+ literal(messageLifeCycleEventType.requestCancelFail),
+ ]),
+ interactionId: string(),
+})
+
+export type MessageLifeCycleEvent = Output
+
+export type IncomingMessage = Output
+const IncomingMessage = union([
+ MessageLifeCycleEvent,
+ WalletInteractionResponse,
+])
+
+export const eventType = {
+ outgoingMessage: 'radix#chromeExtension#send',
+ incomingMessage: 'radix#chromeExtension#receive',
+} as const
+
+export type CallbackFns = {
+ eventCallback: (messageEvent: MessageLifeCycleEvent['eventType']) => void
+ requestControl: (api: {
+ cancelRequest: () => ResultAsync<
+ 'requestCancelSuccess',
+ 'requestCancelFail'
+ >
+ getRequest: () => WalletInteraction
+ }) => void
+}
+
+/**
+ * Signaling server schemas
+ */
+
+const Offer = literal('offer')
+const Answer = literal('answer')
+const IceCandidate = literal('iceCandidate')
+const IceCandidates = literal('iceCandidates')
+
+const Types = union([Offer, Answer, IceCandidate, IceCandidates])
+
+export const Sources = union([literal('wallet'), literal('extension')])
+
+export const SignalingServerMessage = object({
+ requestId: string(),
+ targetClientId: string(),
+ encryptedPayload: string(),
+ source: optional(Sources), // redundant, to be removed
+ connectionId: optional(string()), // redundant, to be removed
+})
+
+export const AnswerIO = merge([
+ SignalingServerMessage,
+ object({
+ method: Answer,
+ payload: object({
+ sdp: string(),
+ }),
+ }),
+])
+
+export const OfferIO = merge([
+ SignalingServerMessage,
+ object({
+ method: Offer,
+ payload: object({
+ sdp: string(),
+ }),
+ }),
+])
+
+export const IceCandidatePayloadIO = object({
+ candidate: string(),
+ sdpMid: string(),
+ sdpMLineIndex: number(),
+})
+
+export const IceCandidateIO = merge([
+ SignalingServerMessage,
+ object({
+ method: IceCandidate,
+ payload: IceCandidatePayloadIO,
+ }),
+])
+
+export const IceCandidatesIO = merge([
+ SignalingServerMessage,
+ object({
+ method: IceCandidates,
+ payload: array(IceCandidatePayloadIO),
+ }),
+])
+
+export type Answer = Output
+export type Offer = Output
+export type IceCandidate = Output
+export type IceCandidates = Output
+export type MessagePayloadTypes = Output
+export type MessageSources = Output
+
+export type DataTypes = Answer | IceCandidate | Offer | IceCandidates
+
+export type Confirmation = {
+ info: 'confirmation'
+ requestId: DataTypes['requestId']
+}
+
+export type RemoteData = {
+ info: 'remoteData'
+ remoteClientId: string
+ requestId: T['requestId']
+ data: T
+}
+
+export type RemoteClientDisconnected = {
+ info: 'remoteClientDisconnected'
+ remoteClientId: string
+}
+
+export type RemoteClientJustConnected = {
+ info: 'remoteClientJustConnected'
+ remoteClientId: string
+}
+
+export type RemoteClientIsAlreadyConnected = {
+ info: 'remoteClientIsAlreadyConnected'
+ remoteClientId: string
+}
+
+export type MissingRemoteClientError = {
+ info: 'missingRemoteClientError'
+ requestId: DataTypes['requestId']
+}
+
+export type InvalidMessageError = {
+ info: 'invalidMessageError'
+ error: string
+ data: string
+}
+
+export type ValidationError = {
+ info: 'validationError'
+ requestId: DataTypes['requestId']
+ error: ValiError
+}
+
+export type SignalingServerResponse =
+ | Confirmation
+ | RemoteData
+ | RemoteClientJustConnected
+ | RemoteClientIsAlreadyConnected
+ | RemoteClientDisconnected
+ | MissingRemoteClientError
+ | InvalidMessageError
+ | ValidationError
+
+export type SignalingServerErrorResponse =
+ | RemoteClientDisconnected
+ | MissingRemoteClientError
+ | InvalidMessageError
+ | ValidationError
diff --git a/packages/dapp-toolkit/src/schemas/schemas.spec.ts b/packages/dapp-toolkit/src/schemas/schemas.spec.ts
new file mode 100644
index 00000000..6a34bb39
--- /dev/null
+++ b/packages/dapp-toolkit/src/schemas/schemas.spec.ts
@@ -0,0 +1,82 @@
+import { describe, it, expect } from 'vitest'
+import {
+ AccountsRequestResponseItem,
+ LoginRequestResponseItem,
+ OfferIO,
+} from '.'
+import { parse } from 'valibot'
+
+describe('schemas', () => {
+ describe('AccountsRequestResponseItem', () => {
+ it('should parse valid schema', () => {
+ const result = parse(AccountsRequestResponseItem, {
+ accounts: [],
+ })
+
+ expect(result).toEqual({
+ accounts: [],
+ })
+ })
+
+ it('should return message for invalid schema', () => {
+ try {
+ parse(AccountsRequestResponseItem, {
+ accounts: [],
+ challenge: 'abc',
+ proofs: [],
+ })
+ } catch (e: any) {
+ expect(e.message).toBe('missing challenge or proofs')
+ }
+ })
+ })
+
+ describe('LoginRequestResponseItem', () => {
+ it('should parse valid schema', () => {
+ const result = parse(LoginRequestResponseItem, {
+ persona: {
+ identityAddress: 'a',
+ label: 'a',
+ },
+ })
+
+ expect(result).toEqual({
+ persona: {
+ identityAddress: 'a',
+ label: 'a',
+ },
+ })
+ })
+
+ it('should return message for invalid schema', () => {
+ try {
+ parse(LoginRequestResponseItem, {
+ persona: {
+ identityAddress: 'a',
+ label: 'a',
+ },
+ challenge: 'abc',
+ })
+ } catch (e: any) {
+ expect(e.message).toBe('missing challenge or proof')
+ }
+ })
+ })
+
+ describe('OfferIO', () => {
+ it('should parse valid schema', () => {
+ const value = {
+ requestId: 'abc',
+ targetClientId: 'ab',
+ encryptedPayload: 'ab',
+ method: 'offer',
+ payload: {
+ sdp: 'a',
+ },
+ }
+ const result = parse(OfferIO, value)
+
+ expect(result).toEqual(value)
+ })
+ })
+})
diff --git a/src/single-file.js b/packages/dapp-toolkit/src/single-file.js
similarity index 100%
rename from src/single-file.js
rename to packages/dapp-toolkit/src/single-file.js
diff --git a/packages/dapp-toolkit/src/state/index.ts b/packages/dapp-toolkit/src/state/index.ts
new file mode 100644
index 00000000..29589cab
--- /dev/null
+++ b/packages/dapp-toolkit/src/state/index.ts
@@ -0,0 +1,2 @@
+export * from './state'
+export * from './types'
diff --git a/packages/dapp-toolkit/src/state/state.ts b/packages/dapp-toolkit/src/state/state.ts
new file mode 100644
index 00000000..d3b08bdf
--- /dev/null
+++ b/packages/dapp-toolkit/src/state/state.ts
@@ -0,0 +1,91 @@
+import {
+ BehaviorSubject,
+ Subscription,
+ filter,
+ merge,
+ of,
+ switchMap,
+} from 'rxjs'
+import { RdtState, WalletData, walletDataDefault } from './types'
+import { Logger } from '../helpers'
+import { StorageProvider } from '../storage/local-storage-client'
+import { SdkError } from '../error'
+import { err, ok } from 'neverthrow'
+
+export type StateClient = ReturnType
+
+export const StateClient = (input: {
+ providers: {
+ storageClient: StorageProvider
+ }
+ logger?: Logger
+}) => {
+ const logger = input?.logger?.getSubLogger({ name: 'StateClient' })
+ const storageClient = input.providers.storageClient
+
+ const subscriptions = new Subscription()
+
+ const setState = (state: RdtState) => storageClient.setState(state)
+
+ const getState = () =>
+ storageClient
+ .getState()
+ .andThen((state) =>
+ state ? ok(state) : err(SdkError('StateNotFound', '')),
+ )
+
+ const patchState = (state: Partial) =>
+ getState().andThen((oldState) => setState({ ...oldState, ...state }))
+
+ const defaultState = {
+ walletData: walletDataDefault,
+ loggedInTimestamp: '',
+ sharedData: {},
+ } satisfies RdtState
+
+ const resetState = () => storageClient.setState(defaultState)
+
+ const initializeState = () =>
+ getState().mapErr(() => {
+ logger?.debug({
+ module: 'StateClient',
+ method: `initializeState.loadedCorruptedStateFromStorage`,
+ })
+ resetState()
+ })
+
+ initializeState()
+
+ const walletDataSubject = new BehaviorSubject(
+ undefined,
+ )
+
+ subscriptions.add(
+ merge(storageClient.storage$, of(null))
+ .pipe(
+ switchMap(() =>
+ getState().map((state) => {
+ walletDataSubject.next(state.walletData)
+ }),
+ ),
+ )
+ .subscribe(),
+ )
+
+ const walletData$ = walletDataSubject
+ .asObservable()
+ .pipe(filter((walletData): walletData is WalletData => !!walletData))
+
+ return {
+ setState,
+ patchState,
+ getState,
+ walletData$,
+ getWalletData: () => walletDataSubject.value,
+ reset: resetState,
+ destroy: () => {
+ subscriptions.unsubscribe()
+ },
+ store: storageClient,
+ }
+}
diff --git a/src/state/types.ts b/packages/dapp-toolkit/src/state/types.ts
similarity index 60%
rename from src/state/types.ts
rename to packages/dapp-toolkit/src/state/types.ts
index 804006d7..17389dfe 100644
--- a/src/state/types.ts
+++ b/packages/dapp-toolkit/src/state/types.ts
@@ -5,23 +5,24 @@ import {
PersonaDataName,
PersonaDataRequestItem,
Proof,
-} from '@radixdlt/wallet-sdk'
+} from '../schemas'
import {
array,
boolean,
- discriminatedUnion,
+ optional,
literal,
object,
+ variant,
string,
- z,
-} from 'zod'
+ Output,
+} from 'valibot'
export const proofType = {
persona: 'persona',
account: 'account',
} as const
-export type SignedChallengePersona = z.infer
+export type SignedChallengePersona = Output
export const SignedChallengePersona = object({
challenge: string(),
proof: Proof,
@@ -29,7 +30,7 @@ export const SignedChallengePersona = object({
type: literal(proofType.persona),
})
-export type SignedChallengeAccount = z.infer
+export type SignedChallengeAccount = Output
export const SignedChallengeAccount = object({
challenge: string(),
proof: Proof,
@@ -37,8 +38,8 @@ export const SignedChallengeAccount = object({
type: literal(proofType.account),
})
-export type SignedChallenge = z.infer
-export const SignedChallenge = discriminatedUnion('type', [
+export type SignedChallenge = Output
+export const SignedChallenge = variant('type', [
SignedChallengePersona,
SignedChallengeAccount,
])
@@ -58,41 +59,43 @@ export const WalletDataPersonaDataPhoneNumbersAddresses = object({
fields: array(string()),
})
-export type WalletDataPersonaData = z.infer
-export const WalletDataPersonaData = discriminatedUnion('entry', [
+export type WalletDataPersonaData = Output
+export const WalletDataPersonaData = variant('entry', [
WalletDataPersonaDataFullName,
WalletDataPersonaDataEmailAddresses,
WalletDataPersonaDataPhoneNumbersAddresses,
])
-export type WalletData = z.infer
+export type WalletData = Output
export const WalletData = object({
accounts: array(Account),
personaData: array(WalletDataPersonaData),
- persona: Persona.optional(),
+ persona: optional(Persona),
proofs: array(SignedChallenge),
})
-export type SharedData = z.infer
+export type SharedData = Output
export const SharedData = object({
- persona: object({ proof: boolean() }).optional(),
- ongoingAccounts: object({
- numberOfAccounts: NumberOfValues.optional(),
- proof: boolean(),
- }).optional(),
- ongoingPersonaData: PersonaDataRequestItem.optional(),
+ persona: optional(object({ proof: boolean() })),
+ ongoingAccounts: optional(
+ object({
+ numberOfAccounts: optional(NumberOfValues),
+ proof: boolean(),
+ }),
+ ),
+ ongoingPersonaData: optional(PersonaDataRequestItem),
})
-export type RdtState = z.infer
+export type RdtState = Output
export const RdtState = object({
loggedInTimestamp: string(),
walletData: WalletData,
sharedData: SharedData,
})
-export const walletDataDefault: WalletData = {
+export const walletDataDefault = {
accounts: [],
personaData: [],
proofs: [],
persona: undefined,
-}
+} satisfies WalletData
diff --git a/packages/dapp-toolkit/src/storage/index.ts b/packages/dapp-toolkit/src/storage/index.ts
new file mode 100644
index 00000000..950fe181
--- /dev/null
+++ b/packages/dapp-toolkit/src/storage/index.ts
@@ -0,0 +1 @@
+export * from './local-storage-client'
diff --git a/packages/dapp-toolkit/src/storage/local-storage-client.ts b/packages/dapp-toolkit/src/storage/local-storage-client.ts
new file mode 100644
index 00000000..c6cc259e
--- /dev/null
+++ b/packages/dapp-toolkit/src/storage/local-storage-client.ts
@@ -0,0 +1,183 @@
+import { err, Ok, ok, Result, ResultAsync } from 'neverthrow'
+import { typedError } from '../helpers/typed-error'
+import { parseJSON } from '../helpers/parse-json'
+import { filter, fromEvent, map, mergeMap } from 'rxjs'
+import { stringify } from '../helpers'
+
+type NetworkId = number
+type PartitionKey =
+ | 'sessions'
+ | 'identities'
+ | 'requests'
+ | 'state'
+ | 'connectButtonStatus'
+type dAppDefinitionAddress = string
+
+export type StorageChange = {
+ key: string
+ partition: string
+ newValue: Record | undefined
+ oldValue: Record | undefined
+}
+
+export type StorageProvider = ReturnType<
+ typeof LocalStorageClient
+>
+
+export const LocalStorageClient = (
+ key: `rdt:${dAppDefinitionAddress}:${NetworkId}`,
+ partitionKey?: PartitionKey,
+) => {
+ const storageKey = partitionKey ? `${key}:${partitionKey}` : key
+
+ const getDataAsync = (): Promise =>
+ new Promise((resolve, reject) => {
+ try {
+ resolve(localStorage.getItem(storageKey))
+ } catch (error) {
+ reject(error)
+ }
+ })
+
+ const setDataAsync = (value: string): Promise =>
+ new Promise((resolve, reject) => {
+ try {
+ localStorage.setItem(storageKey, value)
+ resolve()
+ } catch (error) {
+ reject(error)
+ }
+ })
+
+ const getItems = (): ResultAsync, Error> =>
+ ResultAsync.fromPromise(getDataAsync(), typedError).andThen((data) =>
+ data ? parseJSON(data) : ok({}),
+ )
+
+ const getItemById = (id: string): ResultAsync =>
+ ResultAsync.fromPromise(getDataAsync(), typedError)
+ .andThen((data) => (data ? parseJSON(data) : ok(undefined)))
+ .map((items) => (items ? items[id] : undefined))
+
+ const removeItemById = (id: string): ResultAsync =>
+ getItems().andThen((items) => {
+ const { [id]: _, ...newItems } = items
+ return setItems(newItems)
+ })
+
+ const patchItem = (id: string, patch: Partial): ResultAsync =>
+ getItemById(id).andThen((item) => {
+ return item
+ ? setItems({ [id]: { ...item, ...patch } })
+ : err(new Error('Item not found'))
+ })
+
+ const setItems = (item: Record): ResultAsync =>
+ getItems().andThen((data) =>
+ stringify({ ...data, ...item }).asyncAndThen((serialized) => {
+ const result = ResultAsync.fromPromise(
+ setDataAsync(serialized),
+ typedError,
+ ).map(() => {
+ window.dispatchEvent(
+ new StorageEvent('storage', {
+ key: storageKey,
+ oldValue: JSON.stringify(data),
+ newValue: serialized,
+ }),
+ )
+ })
+ return result
+ }),
+ )
+
+ const getItemList = (): ResultAsync =>
+ getItems().map(Object.values)
+
+ const getState = (): ResultAsync =>
+ ResultAsync.fromPromise(getDataAsync(), typedError).andThen((data) =>
+ data ? parseJSON(data) : ok(undefined),
+ )
+
+ const setState = (newValue: T): ResultAsync =>
+ getState().andThen((oldValue) =>
+ stringify({ ...(oldValue ?? {}), ...newValue }).asyncAndThen(
+ (serialized) => {
+ const result = ResultAsync.fromPromise(
+ setDataAsync(serialized),
+ typedError,
+ ).map(() => {
+ window.dispatchEvent(
+ new StorageEvent('storage', {
+ key: storageKey,
+ oldValue: JSON.stringify(oldValue),
+ newValue: serialized,
+ }),
+ )
+ })
+ return result
+ },
+ ),
+ )
+
+ const patchState = (
+ newValue: Partial,
+ ): ResultAsync =>
+ getState()
+ .mapErr(() => ({ reason: 'FailedToReadFromLocalStorage' }))
+ .andThen((oldState) =>
+ oldState
+ ? setState({ ...oldState, ...newValue }).mapErr(() => ({
+ reason: 'FailedToWriteToLocalStorage',
+ }))
+ : err({ reason: 'PatchingStateFailed' }),
+ )
+
+ const getPartition = (partitionKey: PartitionKey) =>
+ LocalStorageClient(key, partitionKey)
+
+ const storage$ = fromEvent(window, 'storage').pipe(
+ filter((item) => item.key === storageKey),
+ mergeMap((event) => {
+ const { key, newValue, oldValue } = event
+
+ if (!key) return []
+
+ const [rdt, accountDefinition, networkId, partition] = key.split(':')
+
+ if (rdt === 'rdt' && accountDefinition && networkId) {
+ const oldValueResult = oldValue ? parseJSON(oldValue) : ok(undefined)
+ const newValueResult = newValue ? parseJSON(newValue) : ok(undefined)
+
+ return [
+ Result.combine([oldValueResult, newValueResult]).map(
+ ([oldValue, newValue]) => ({
+ key,
+ partition,
+ newValue,
+ oldValue,
+ }),
+ ),
+ ]
+ }
+ return []
+ }),
+ filter((result): result is Ok, never> => result.isOk()),
+ map(({ value }) => value),
+ )
+
+ return {
+ getItems,
+ getItemById,
+ removeItemById,
+ patchItem,
+ setItems,
+ getItemList,
+ getPartition,
+ setState,
+ getState,
+ patchState,
+ clear: () => localStorage.removeItem(storageKey),
+ storage$,
+ }
+}
diff --git a/src/test-helpers/context.ts b/packages/dapp-toolkit/src/test-helpers/context.ts
similarity index 82%
rename from src/test-helpers/context.ts
rename to packages/dapp-toolkit/src/test-helpers/context.ts
index 339e5224..31fc8dda 100644
--- a/src/test-helpers/context.ts
+++ b/packages/dapp-toolkit/src/test-helpers/context.ts
@@ -1,9 +1,8 @@
-import { mockDeep, type DeepMockProxy } from 'jest-mock-extended'
+import { mockDeep, type DeepMockProxy } from 'vitest-mock-extended'
import { StateClient } from '../state/state'
import { ConnectButtonClient } from '../connect-button/connect-button-client'
-import { WalletClient } from '../wallet/wallet-client'
+import { WalletRequestClient } from '../wallet-request/wallet-request'
import { GatewayClient } from '../gateway/gateway'
-import { WalletSdk, createLogger } from '@radixdlt/wallet-sdk'
import { GatewayApiClient } from '../gateway/gateway-api'
import { RequestItemClient } from '../request-items/request-item-client'
import { StorageProvider } from '../_types'
@@ -13,14 +12,15 @@ import { RequestItemSubjects } from '../request-items/subjects'
import { okAsync } from 'neverthrow'
import { InMemoryClient } from '../storage/in-memory-storage-client'
import { of } from 'rxjs'
+import { WalletRequestSdk } from '../wallet-request'
export type Context = {
stateClient: StateClient
connectButton: ConnectButtonClient
- walletClient: WalletClient
+ walletRequestClient: WalletRequestClient
gatewayClient: GatewayClient
gatewayApiClient: GatewayApiClient
- walletSdk: WalletSdk
+ walletRequestSdk: WalletRequestSdk
requestItemClient: RequestItemClient
storageClient: StorageProvider
}
@@ -30,18 +30,16 @@ export type MockContext = {
connectButtonSubjects: ConnectButtonSubjects
stateSubjects: StateSubjects
stateClient: StateClient
- walletClient: WalletClient
+ walletRequestClient: WalletRequestClient
requestItemClient: RequestItemClient
storageClient: StorageProvider
gatewayClient: GatewayClient
gatewayApiClient: DeepMockProxy
- walletSdk: DeepMockProxy
+ walletRequestSdk: DeepMockProxy
connectButton: DeepMockProxy
}
export const createMockContext = (): MockContext => {
- const logger = createLogger(2)
-
const stateSubjects = StateSubjects()
const connectButtonSubjects = ConnectButtonSubjects()
const requestItemSubjects = RequestItemSubjects()
@@ -49,7 +47,7 @@ export const createMockContext = (): MockContext => {
const gatewayApiClientMock = mockDeep()
const connectButtonMock = mockDeep()
const walletSdkMock = {
- ...mockDeep(),
+ ...mockDeep(),
extensionStatus$: of({
isWalletLinked: true,
isExtensionAvailable: true,
@@ -59,24 +57,20 @@ export const createMockContext = (): MockContext => {
const storageClient = InMemoryClient()
const stateClient = StateClient(`rdt:test:12`, storageClient, {
subjects: stateSubjects,
- logger,
})
const requestItemClient = RequestItemClient(`rdt:test:12`, storageClient, {
subjects: requestItemSubjects,
- logger,
})
const gatewayClient = GatewayClient({
gatewayApi: gatewayApiClientMock,
- logger,
})
- const walletClient = WalletClient({
- logger,
+ const walletRequestClient = WalletRequestClient({
requestItemClient,
gatewayClient,
- walletSdk: walletSdkMock as any,
+ walletRequestSdk: walletSdkMock as any,
onCancelRequestItem$:
connectButtonSubjects.onCancelRequestItem.asObservable(),
})
@@ -95,12 +89,7 @@ export const createMockContext = (): MockContext => {
connectButtonSubjects.onLinkClick.asObservable() as any
gatewayApiClientMock.getEntityDetails.mockReturnValue(
- okAsync({
- address: 'test',
- metadata: {
- items: [],
- },
- }) as any
+ okAsync(undefined) as any,
)
return {
@@ -109,11 +98,11 @@ export const createMockContext = (): MockContext => {
connectButtonSubjects,
requestItemClient,
requestItemSubjects,
- walletClient,
+ walletRequestClient,
gatewayClient,
gatewayApiClient: gatewayApiClientMock,
connectButton: connectButtonMock,
- walletSdk: walletSdkMock as any,
+ walletRequestSdk: walletSdkMock as any,
storageClient,
}
}
diff --git a/src/test-helpers/delay-async.ts b/packages/dapp-toolkit/src/test-helpers/delay-async.ts
similarity index 100%
rename from src/test-helpers/delay-async.ts
rename to packages/dapp-toolkit/src/test-helpers/delay-async.ts
diff --git a/packages/dapp-toolkit/src/version.ts b/packages/dapp-toolkit/src/version.ts
new file mode 100644
index 00000000..18570b7d
--- /dev/null
+++ b/packages/dapp-toolkit/src/version.ts
@@ -0,0 +1 @@
+export const __VERSION__ = '2.0.0'
diff --git a/packages/dapp-toolkit/src/wallet-request/crypto/curve25519.ts b/packages/dapp-toolkit/src/wallet-request/crypto/curve25519.ts
new file mode 100644
index 00000000..5fdc20bd
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/crypto/curve25519.ts
@@ -0,0 +1,33 @@
+import { x25519 } from '@noble/curves/ed25519'
+import { Buffer } from 'buffer'
+import { Result, err, ok } from 'neverthrow'
+
+const toHex = (input: Uint8Array) => Buffer.from(input).toString('hex')
+
+export type KeyPairProvider = (privateKeyHex?: string) => {
+ getPublicKey: () => string
+ getPrivateKey: () => string
+ calculateSharedSecret: (publicKeyHex: string) => Result
+}
+
+export type Curve25519 = ReturnType
+
+export const Curve25519: KeyPairProvider = (
+ privateKeyHex = toHex(x25519.utils.randomPrivateKey()),
+) => {
+ const getPrivateKey = () => privateKeyHex
+
+ const getPublicKey = () => toHex(x25519.getPublicKey(privateKeyHex))
+
+ const calculateSharedSecret = (
+ publicKeyHex: string,
+ ): Result => {
+ try {
+ return ok(toHex(x25519.getSharedSecret(privateKeyHex, publicKeyHex)))
+ } catch (error) {
+ return err(error as Error)
+ }
+ }
+
+ return { getPublicKey, getPrivateKey, calculateSharedSecret }
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/crypto/index.ts b/packages/dapp-toolkit/src/wallet-request/crypto/index.ts
new file mode 100644
index 00000000..3f26b3df
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/crypto/index.ts
@@ -0,0 +1 @@
+export * from './curve25519'
diff --git a/src/data-request/builders/accounts.ts b/packages/dapp-toolkit/src/wallet-request/data-request/builders/accounts.ts
similarity index 87%
rename from src/data-request/builders/accounts.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/builders/accounts.ts
index 1377553d..8c065b15 100644
--- a/src/data-request/builders/accounts.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/builders/accounts.ts
@@ -1,6 +1,6 @@
-import { NumberOfValues } from '@radixdlt/wallet-sdk'
import { produce } from 'immer'
-import { boolean, object, z } from 'zod'
+import { boolean, object, Output, optional } from 'valibot'
+import { NumberOfValues } from '../../../schemas'
export type AccountsRequestBuilder = {
atLeast: (n: number) => AccountsRequestBuilder
@@ -13,12 +13,12 @@ export type OneTimeAccountsRequestBuilder = {
exactly: (n: number) => OneTimeAccountsRequestBuilder
withProof: (value?: boolean) => OneTimeAccountsRequestBuilder
}
-export type AccountsDataRequest = z.infer
+export type AccountsDataRequest = Output
export const AccountsDataRequestSchema = object({
numberOfAccounts: NumberOfValues,
- withProof: boolean().optional(),
- reset: boolean().optional(),
+ withProof: optional(boolean()),
+ reset: optional(boolean()),
})
export const accounts: () => AccountsRequestBuilder = () => {
diff --git a/src/data-request/builders/index.ts b/packages/dapp-toolkit/src/wallet-request/data-request/builders/index.ts
similarity index 100%
rename from src/data-request/builders/index.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/builders/index.ts
diff --git a/src/data-request/builders/persona-data.ts b/packages/dapp-toolkit/src/wallet-request/data-request/builders/persona-data.ts
similarity index 82%
rename from src/data-request/builders/persona-data.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/builders/persona-data.ts
index 335288f1..ca5d42bf 100644
--- a/src/data-request/builders/persona-data.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/builders/persona-data.ts
@@ -1,6 +1,6 @@
-import { NumberOfValues } from '@radixdlt/wallet-sdk'
import { produce } from 'immer'
-import { boolean, object, z } from 'zod'
+import { boolean, object, Output, partial } from 'valibot'
+import { NumberOfValues } from '../../../schemas'
export type PersonaDataRequestBuilder = {
fullName: (value?: boolean) => PersonaDataRequestBuilder
@@ -13,14 +13,16 @@ export type OneTimePersonaDataRequestBuilder = {
emailAddresses: (value?: boolean) => PersonaDataRequestBuilder
phoneNumbers: (value?: boolean) => PersonaDataRequestBuilder
}
-export type PersonaDataRequest = z.infer
+export type PersonaDataRequest = Output
-export const PersonaDataRequestSchema = object({
- fullName: boolean(),
- emailAddresses: NumberOfValues,
- phoneNumbers: NumberOfValues,
- reset: boolean(),
-}).partial()
+export const PersonaDataRequestSchema = partial(
+ object({
+ fullName: boolean(),
+ emailAddresses: NumberOfValues,
+ phoneNumbers: NumberOfValues,
+ reset: boolean(),
+ }),
+)
export const personaData = (initialData: PersonaDataRequest = {}) => {
let data: PersonaDataRequest = produce(initialData, () => {})
@@ -34,7 +36,7 @@ export const personaData = (initialData: PersonaDataRequest = {}) => {
}
const createNumberOfValuesOptions = (
- key: 'emailAddresses' | 'phoneNumbers'
+ key: 'emailAddresses' | 'phoneNumbers',
) => ({
atLeast: (n: number) => {
data = produce(data, (draft) => {
diff --git a/src/data-request/builders/persona.ts b/packages/dapp-toolkit/src/wallet-request/data-request/builders/persona.ts
similarity index 80%
rename from src/data-request/builders/persona.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/builders/persona.ts
index 336f02b2..13247fe5 100644
--- a/src/data-request/builders/persona.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/builders/persona.ts
@@ -1,19 +1,19 @@
import { produce } from 'immer'
-import { boolean, object, z } from 'zod'
+import { boolean, object, Output, optional } from 'valibot'
export type PersonaRequestBuilder = {
withProof: (value?: boolean) => PersonaRequestBuilder
}
-export type PersonaRequest = z.infer
+export type PersonaRequest = Output
const schema = object({
- withProof: boolean().optional(),
+ withProof: optional(boolean()),
})
export const persona = (
initialData: PersonaRequest = {
withProof: false,
- }
+ },
) => {
let data: PersonaRequest = produce(initialData, () => {})
diff --git a/src/data-request/data-request.spec.ts b/packages/dapp-toolkit/src/wallet-request/data-request/data-request-state.spec.ts
similarity index 95%
rename from src/data-request/data-request.spec.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/data-request-state.spec.ts
index 2c3760c2..feebfff0 100644
--- a/src/data-request/data-request.spec.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/data-request-state.spec.ts
@@ -2,8 +2,9 @@ import { accounts } from './builders/accounts'
import { persona } from './builders/persona'
import { personaData } from './builders/persona-data'
import { DataRequestStateClient } from './data-request-state'
+import { describe, beforeEach, it, expect } from 'vitest'
-describe('dataRequest', () => {
+describe('DataRequestStateClient', () => {
let dataRequest: DataRequestStateClient
beforeEach(() => {
@@ -74,7 +75,7 @@ describe('dataRequest', () => {
.fullName(true)
.emailAddresses(true)
.phoneNumbers(true)
- .reset()
+ .reset(),
)
expect(dataRequest.getState().personaData).toEqual({
fullName: true,
diff --git a/src/data-request/data-request-state.ts b/packages/dapp-toolkit/src/wallet-request/data-request/data-request-state.ts
similarity index 92%
rename from src/data-request/data-request-state.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/data-request-state.ts
index b3cc2d96..13af5ed7 100644
--- a/src/data-request/data-request-state.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/data-request-state.ts
@@ -1,5 +1,5 @@
import { BehaviorSubject } from 'rxjs'
-import { DataRequestBuilderItem, DataRequestState } from './builders'
+import type { DataRequestBuilderItem, DataRequestState } from './builders'
import { produce } from 'immer'
export type DataRequestStateClient = ReturnType
@@ -12,13 +12,13 @@ export const DataRequestStateClient = (initialState: DataRequestState) => {
const toDataRequestState = (...items: unknown[]): DataRequestState =>
items
- .filter((item: any): item is any => typeof item._toObject == 'function')
+ .filter((item: any): item is any => typeof item._toObject === 'function')
.reduce(
(acc, item) => ({
...acc,
...item._toObject(),
}),
- {}
+ {},
)
const setState = (...items: DataRequestBuilderItem[]) => {
@@ -39,7 +39,7 @@ export const DataRequestStateClient = (initialState: DataRequestState) => {
keys.forEach((key) => {
delete draft[key]
})
- })
+ }),
)
}
diff --git a/src/data-request/helpers/can-data-request-be-resolved-by-rdt-state.ts b/packages/dapp-toolkit/src/wallet-request/data-request/helpers/can-data-request-be-resolved-by-rdt-state.ts
similarity index 84%
rename from src/data-request/helpers/can-data-request-be-resolved-by-rdt-state.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/helpers/can-data-request-be-resolved-by-rdt-state.ts
index 9c12d219..2a3af72b 100644
--- a/src/data-request/helpers/can-data-request-be-resolved-by-rdt-state.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/helpers/can-data-request-be-resolved-by-rdt-state.ts
@@ -1,13 +1,13 @@
import {
WalletAuthorizedRequestItems,
WalletUnauthorizedRequestItems,
-} from '@radixdlt/wallet-sdk'
-import { RdtState } from '../../state/types'
-import isEqual from 'lodash.isequal'
+} from '../../../schemas'
+import { RdtState } from '../../../state/types'
+import { isDeepEqual } from '../../../helpers/is-deep-equal'
export const canDataRequestBeResolvedByRdtState = (
dataRequest: WalletUnauthorizedRequestItems | WalletAuthorizedRequestItems,
- state: RdtState
+ state: RdtState,
) => {
if (dataRequest.discriminator === 'authorizedRequest') {
const isReset =
@@ -37,9 +37,9 @@ export const canDataRequestBeResolvedByRdtState = (
}
if (dataRequest.ongoingPersonaData) {
- rdtStateSatisfiesRequest = isEqual(
+ rdtStateSatisfiesRequest = isDeepEqual(
dataRequest.ongoingPersonaData,
- state.sharedData?.ongoingPersonaData
+ state.sharedData?.ongoingPersonaData,
)
}
diff --git a/packages/dapp-toolkit/src/wallet-request/data-request/helpers/index.ts b/packages/dapp-toolkit/src/wallet-request/data-request/helpers/index.ts
new file mode 100644
index 00000000..06481cd8
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/helpers/index.ts
@@ -0,0 +1,2 @@
+export * from './can-data-request-be-resolved-by-rdt-state'
+export * from './to-wallet-request'
diff --git a/src/data-request/helpers/to-wallet-request.ts b/packages/dapp-toolkit/src/wallet-request/data-request/helpers/to-wallet-request.ts
similarity index 74%
rename from src/data-request/helpers/to-wallet-request.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/helpers/to-wallet-request.ts
index 0eeb87e7..78f18258 100644
--- a/src/data-request/helpers/to-wallet-request.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/helpers/to-wallet-request.ts
@@ -1,23 +1,21 @@
import { produce } from 'immer'
-import {
- TransformRdtDataRequestToWalletRequestInput,
- transformRdtDataRequestToWalletRequest,
-} from '../transformations/rdt-to-wallet'
-import { DataRequestState } from '../builders'
-import { StateClient } from '../../state/state'
+import type { TransformRdtDataRequestToWalletRequestInput } from '../transformations/rdt-to-wallet'
+import { transformRdtDataRequestToWalletRequest } from '../transformations/rdt-to-wallet'
+import type { DataRequestState } from '../builders'
+import { WalletData } from '../../../state'
export const toWalletRequest = ({
dataRequestState,
isConnect,
challenge,
oneTime,
- stateClient,
+ walletData,
}: {
dataRequestState: DataRequestState
isConnect: boolean
oneTime: boolean
challenge?: string
- stateClient: StateClient
+ walletData: WalletData
}) =>
transformRdtDataRequestToWalletRequest(
isConnect,
@@ -44,9 +42,9 @@ export const toWalletRequest = ({
}
if (!oneTime) {
- const persona = stateClient.getState().walletData.persona
+ const persona = walletData.persona
- if (stateClient.getState().walletData.persona) draft.persona = persona
+ if (walletData.persona) draft.persona = persona
if (dataRequestState.persona?.withProof)
draft.persona = { ...(draft.persona ?? {}), challenge }
@@ -54,5 +52,5 @@ export const toWalletRequest = ({
if (Object.values(dataRequestState).length === 0)
draft.persona = { challenge: undefined }
}
- })
+ }),
)
diff --git a/packages/dapp-toolkit/src/wallet-request/data-request/index.ts b/packages/dapp-toolkit/src/wallet-request/data-request/index.ts
new file mode 100644
index 00000000..9babbd20
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/index.ts
@@ -0,0 +1,4 @@
+export * from './builders'
+export * from './helpers'
+export * from './transformations'
+export * from './data-request-state'
diff --git a/src/data-request/transformations/index.ts b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/index.ts
similarity index 66%
rename from src/data-request/transformations/index.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/transformations/index.ts
index ac501e7b..3d15a6de 100644
--- a/src/data-request/transformations/index.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/index.ts
@@ -1,3 +1,4 @@
-export * from './rdt-to-wallet'
-export * from './shared-data'
-export * from './wallet-to-rdt'
+export * from './rdt-to-wallet'
+export * from './shared-data'
+export * from './wallet-to-rdt'
+export * from './wallet-data-to-connect-button'
diff --git a/src/data-request/transformations/rdt-to-wallet.ts b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/rdt-to-wallet.ts
similarity index 79%
rename from src/data-request/transformations/rdt-to-wallet.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/transformations/rdt-to-wallet.ts
index 220aaf75..fb937a4c 100644
--- a/src/data-request/transformations/rdt-to-wallet.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/rdt-to-wallet.ts
@@ -1,41 +1,48 @@
-import {
+import type {
AuthLoginWithChallengeRequestItem,
AuthLoginWithoutChallengeRequestItem,
AuthUsePersonaRequestItem,
- NumberOfValues,
WalletAuthorizedRequestItems,
WalletUnauthorizedRequestItems,
-} from '@radixdlt/wallet-sdk'
+} from '../../../schemas'
+import { NumberOfValues } from '../../../schemas'
import { produce } from 'immer'
-import { Result, ok } from 'neverthrow'
-import { boolean, object, string, z } from 'zod'
+import type { Result } from 'neverthrow'
+import { ok } from 'neverthrow'
+import { boolean, object, string, Output, optional } from 'valibot'
-export type TransformRdtDataRequestToWalletRequestInput = z.infer<
+export type TransformRdtDataRequestToWalletRequestInput = Output<
typeof TransformRdtDataRequestToWalletRequestInput
>
export const TransformRdtDataRequestToWalletRequestInput = object({
- accounts: object({
- numberOfAccounts: NumberOfValues,
- reset: boolean(),
- oneTime: boolean(),
- challenge: string().optional(),
- }).optional(),
- personaData: object({
- fullName: boolean().optional(),
- phoneNumbers: NumberOfValues.optional(),
- emailAddresses: NumberOfValues.optional(),
- reset: boolean(),
- oneTime: boolean().optional(),
- }).optional(),
- persona: object({
- identityAddress: string().optional(),
- label: string().optional(),
- challenge: string().optional(),
- }).optional(),
+ accounts: optional(
+ object({
+ numberOfAccounts: NumberOfValues,
+ reset: boolean(),
+ oneTime: boolean(),
+ challenge: optional(string()),
+ }),
+ ),
+ personaData: optional(
+ object({
+ fullName: optional(boolean()),
+ phoneNumbers: optional(NumberOfValues),
+ emailAddresses: optional(NumberOfValues),
+ reset: boolean(),
+ oneTime: optional(boolean()),
+ }),
+ ),
+ persona: optional(
+ object({
+ identityAddress: optional(string()),
+ label: optional(string()),
+ challenge: optional(string()),
+ }),
+ ),
})
const isAuthorized = (
- input: TransformRdtDataRequestToWalletRequestInput
+ input: TransformRdtDataRequestToWalletRequestInput,
): boolean => {
const { persona, accounts, personaData } = input
@@ -55,7 +62,7 @@ const isAuthorized = (
}
const createLoginRequestItem = (
- input: TransformRdtDataRequestToWalletRequestInput
+ input: TransformRdtDataRequestToWalletRequestInput,
) => {
if (input.persona?.challenge) {
return {
@@ -79,7 +86,7 @@ const createLoginRequestItem = (
const withAccountRequestItem =
(input: TransformRdtDataRequestToWalletRequestInput) =>
(
- requestItems: T
+ requestItems: T,
) => {
const updatedRequestItems = { ...requestItems }
const { accounts } = input
@@ -108,7 +115,7 @@ const withAccountRequestItem =
const withPersonaDataRequestItem =
(input: TransformRdtDataRequestToWalletRequestInput) =>
(
- requestItems: T
+ requestItems: T,
) => {
const updatedRequestItems = { ...requestItems }
@@ -156,7 +163,7 @@ const withResetRequestItem =
}
const createUnauthorizedRequestItems = (
- input: TransformRdtDataRequestToWalletRequestInput
+ input: TransformRdtDataRequestToWalletRequestInput,
): Result =>
ok({
discriminator: 'unauthorizedRequest',
@@ -165,7 +172,7 @@ const createUnauthorizedRequestItems = (
.map(withPersonaDataRequestItem(input))
const createAuthorizedRequestItems = (
- input: TransformRdtDataRequestToWalletRequestInput
+ input: TransformRdtDataRequestToWalletRequestInput,
): Result =>
ok({
discriminator: 'authorizedRequest',
@@ -177,7 +184,7 @@ const createAuthorizedRequestItems = (
const transformConnectRequest = (
isConnect: boolean,
- input: TransformRdtDataRequestToWalletRequestInput
+ input: TransformRdtDataRequestToWalletRequestInput,
): Result =>
ok(
isConnect
@@ -192,12 +199,12 @@ const transformConnectRequest = (
draft.personaData.reset = false
}
})
- : input
+ : input,
)
export const transformRdtDataRequestToWalletRequest = (
isConnect: boolean,
- input: TransformRdtDataRequestToWalletRequestInput
+ input: TransformRdtDataRequestToWalletRequestInput,
): Result<
WalletUnauthorizedRequestItems | WalletAuthorizedRequestItems,
never
@@ -205,5 +212,5 @@ export const transformRdtDataRequestToWalletRequest = (
transformConnectRequest(isConnect, input).andThen((transformed) =>
isAuthorized(transformed)
? createAuthorizedRequestItems(transformed)
- : createUnauthorizedRequestItems(transformed)
+ : createUnauthorizedRequestItems(transformed),
)
diff --git a/src/data-request/transformations/shared-data.ts b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/shared-data.ts
similarity index 80%
rename from src/data-request/transformations/shared-data.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/transformations/shared-data.ts
index 8b2d4b75..71a8858e 100644
--- a/src/data-request/transformations/shared-data.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/shared-data.ts
@@ -1,12 +1,13 @@
-import { WalletSdk } from '@radixdlt/wallet-sdk'
import { produce } from 'immer'
-import { SharedData } from '../../state/types'
-import { DataRequestState } from '../builders'
+import type { SharedData } from '../../../state/types'
+import type { DataRequestState } from '../builders'
+import { WalletInteraction } from '../../../schemas'
export const transformWalletRequestToSharedData = (
- walletDataRequest: Parameters[0],
- sharedData: SharedData
+ walletInteraction: WalletInteraction,
+ sharedData: SharedData,
): SharedData => {
+ const { items: walletDataRequest } = walletInteraction
if (walletDataRequest.discriminator === 'authorizedRequest')
return produce({}, (draft: SharedData) => {
draft.persona = { proof: false }
@@ -35,7 +36,7 @@ export const transformWalletRequestToSharedData = (
}
export const transformSharedDataToDataRequestState = (
- sharedData: SharedData
+ sharedData: SharedData,
): DataRequestState =>
produce({}, (draft: DataRequestState) => {
if (sharedData.ongoingAccounts) {
@@ -53,13 +54,11 @@ export const transformSharedDataToDataRequestState = (
sharedData.ongoingPersonaData.numberOfRequestedPhoneNumbers,
emailAddresses:
sharedData.ongoingPersonaData.numberOfRequestedEmailAddresses,
- reset: false,
+ reset: true,
}
}
if (sharedData.persona) {
- draft.persona = {
- withProof: false,
- }
+ draft.persona = { withProof: !!sharedData.persona.proof }
}
})
diff --git a/src/data-request/transformations/wallet-data-to-connect-button.ts b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/wallet-data-to-connect-button.ts
similarity index 87%
rename from src/data-request/transformations/wallet-data-to-connect-button.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/transformations/wallet-data-to-connect-button.ts
index 189ffc35..63841fd3 100644
--- a/src/data-request/transformations/wallet-data-to-connect-button.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/wallet-data-to-connect-button.ts
@@ -1,11 +1,11 @@
-import { WalletData } from '../../state/types'
+import type { WalletData } from '../../../state/types'
export const transformWalletDataToConnectButton = (walletData: WalletData) => {
const accounts = walletData.accounts ?? []
const personaLabel = walletData?.persona?.label ?? ''
const connected = !!walletData?.persona
- const personaData = walletData.personaData
- .map((item) => {
+ const personaData = walletData?.personaData
+ ?.map((item) => {
if (item.entry === 'fullName') {
const { variant, givenNames, familyName, nickname } = item.fields
const value =
@@ -34,11 +34,11 @@ export const transformWalletDataToConnectButton = (walletData: WalletData) => {
})
.filter(
(
- item
+ item,
): item is {
value: string
field: string
- } => !!item && !!item.value.trim()
+ } => !!item && !!item.value.trim(),
)
return { accounts, personaLabel, connected, personaData }
diff --git a/src/data-request/transformations/wallet-to-rdt.ts b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/wallet-to-rdt.ts
similarity index 91%
rename from src/data-request/transformations/wallet-to-rdt.ts
rename to packages/dapp-toolkit/src/wallet-request/data-request/transformations/wallet-to-rdt.ts
index f84f70c4..1a276f84 100644
--- a/src/data-request/transformations/wallet-to-rdt.ts
+++ b/packages/dapp-toolkit/src/wallet-request/data-request/transformations/wallet-to-rdt.ts
@@ -1,16 +1,13 @@
-import {
+import type {
Account,
PersonaDataRequestResponseItem,
WalletAuthorizedRequestResponseItems,
WalletUnauthorizedRequestResponseItems,
-} from '@radixdlt/wallet-sdk'
+} from '../../../schemas'
import { produce } from 'immer'
-import { Result, ok } from 'neverthrow'
-import {
- SignedChallengeAccount,
- WalletData,
- proofType,
-} from '../../state/types'
+import type { ResultAsync } from 'neverthrow'
+import { okAsync } from 'neverthrow'
+import { SignedChallengeAccount, WalletData, proofType } from '../../../state'
export type WalletDataRequestResponse =
| WalletAuthorizedRequestResponseItems
@@ -36,7 +33,7 @@ const withAccounts =
}
const withPersonaDataEntries = (
- input: PersonaDataRequestResponseItem
+ input: PersonaDataRequestResponseItem,
): WalletData['personaData'] => {
const entries: WalletData['personaData'] = []
@@ -85,8 +82,8 @@ const withPersona =
})
const withProofs =
- (input: WalletDataRequestResponse) => (walletData: WalletData) => {
- return produce(walletData, (draft) => {
+ (input: WalletDataRequestResponse) => (walletData: WalletData) =>
+ produce(walletData, (draft) => {
draft.proofs = []
if (input.discriminator === 'authorizedRequest') {
if (input.auth.discriminator === 'loginWithChallenge')
@@ -108,7 +105,7 @@ const withProofs =
address: accountAddress,
challenge,
type: proofType.account,
- })
+ }),
)
draft.proofs.push(...accountProofs)
@@ -125,7 +122,7 @@ const withProofs =
address: accountAddress,
challenge,
type: proofType.account,
- })
+ }),
)
draft.proofs.push(...accountProofs)
}
@@ -142,18 +139,17 @@ const withProofs =
address: accountAddress,
challenge,
type: proofType.account,
- })
+ }),
)
draft.proofs.push(...accountProofs)
}
}
})
- }
export const transformWalletResponseToRdtWalletData = (
- response: WalletDataRequestResponse
-): Result =>
- ok({
+ response: WalletDataRequestResponse,
+): ResultAsync =>
+ okAsync({
accounts: [],
personaData: [],
proofs: [],
diff --git a/packages/dapp-toolkit/src/wallet-request/encryption/encryption.ts b/packages/dapp-toolkit/src/wallet-request/encryption/encryption.ts
new file mode 100644
index 00000000..93e3a231
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/encryption/encryption.ts
@@ -0,0 +1,73 @@
+import { ResultAsync } from 'neverthrow'
+import { typedError } from '../../helpers/typed-error'
+import type { SealedBoxProps } from './helpers/sealbox'
+import { Buffer } from 'buffer'
+
+export type EncryptionClient = ReturnType
+export const EncryptionClient = () => {
+ const cryptoDecrypt = (data: Buffer, encryptionKey: CryptoKey, iv: Buffer) =>
+ ResultAsync.fromPromise(
+ crypto.subtle.decrypt({ name: 'AES-GCM', iv }, encryptionKey, data),
+ typedError,
+ ).map(Buffer.from)
+
+ const cryptoEncrypt = (data: Buffer, encryptionKey: CryptoKey, iv: Buffer) =>
+ ResultAsync.fromPromise(
+ crypto.subtle.encrypt(
+ {
+ name: 'AES-GCM',
+ iv,
+ },
+ encryptionKey,
+ data,
+ ),
+ typedError,
+ ).map(Buffer.from)
+
+ const getKey = (encryptionKey: Buffer) =>
+ ResultAsync.fromPromise(
+ crypto.subtle.importKey(
+ 'raw',
+ encryptionKey,
+ {
+ name: 'AES-GCM',
+ length: 256,
+ },
+ false,
+ ['encrypt', 'decrypt'],
+ ),
+ typedError,
+ )
+
+ const combineIVandCipherText = (iv: Buffer, ciphertext: Buffer): Buffer =>
+ Buffer.concat([iv, ciphertext])
+
+ const decrypt = (
+ data: Buffer,
+ encryptionKey: Buffer,
+ iv: Buffer,
+ ): ResultAsync =>
+ getKey(encryptionKey).andThen((cryptoKey) =>
+ cryptoDecrypt(data, cryptoKey, iv),
+ )
+
+ const encrypt = (
+ data: Buffer,
+ encryptionKey: Buffer,
+ iv = createIV(),
+ ): ResultAsync<
+ Omit,
+ Error
+ > =>
+ getKey(encryptionKey)
+ .andThen((cryptoKey) => cryptoEncrypt(data, cryptoKey, iv))
+ .map((ciphertext) => ({
+ combined: combineIVandCipherText(iv, ciphertext),
+ iv,
+ ciphertext,
+ }))
+
+ const createIV = () => Buffer.from(crypto.getRandomValues(new Uint8Array(12)))
+
+ return { encrypt, decrypt, createIV }
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/encryption/helpers/buffer-reader.ts b/packages/dapp-toolkit/src/wallet-request/encryption/helpers/buffer-reader.ts
new file mode 100644
index 00000000..afb6b44a
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/encryption/helpers/buffer-reader.ts
@@ -0,0 +1,60 @@
+import type { Result } from 'neverthrow'
+import { err, ok } from 'neverthrow'
+import { Buffer } from 'buffer'
+
+type BufferReader = {
+ finishedParsing: () => Result
+ readNextBuffer: (byteCount: number) => Result
+ remainingBytes: () => Result
+}
+
+const createBufferReader = (buffer: Buffer): BufferReader => {
+ let offset = 0
+ let bytesLeftToRead = buffer.length
+
+ const readNextBuffer = (byteCount: number): Result => {
+ if (byteCount < 0) return err(Error(`'byteCount' must not be negative`))
+ if (byteCount === 0) {
+ return ok(Buffer.alloc(0))
+ }
+ if (offset + byteCount > buffer.length)
+ return err(Error(`Out of buffer's boundary`))
+
+ const bufToReturn = Buffer.alloc(byteCount)
+ buffer.copy(bufToReturn, 0, offset, offset + byteCount)
+
+ if (bufToReturn.length !== byteCount) {
+ return err(Error(`Incorrect length of newly read buffer...`))
+ }
+
+ offset += byteCount
+ bytesLeftToRead -= byteCount
+
+ return ok(bufToReturn)
+ }
+
+ const finishedParsing = (): Result => {
+ if (bytesLeftToRead < 0) {
+ return err(Error(`Incorrect implementation, read too many bytes.`))
+ }
+ return ok(bytesLeftToRead === 0)
+ }
+
+ return {
+ readNextBuffer,
+ finishedParsing,
+ remainingBytes: () =>
+ finishedParsing().andThen((finished) => {
+ if (finished) return ok(Buffer.alloc(0))
+
+ const leftBuf = Buffer.alloc(bytesLeftToRead)
+ buffer.copy(leftBuf, 0, offset)
+ return ok(leftBuf)
+ }),
+ }
+}
+
+export const readBuffer = (
+ buffer: Buffer,
+): ((byteCount: number) => Result) =>
+ createBufferReader(buffer).readNextBuffer
diff --git a/packages/dapp-toolkit/src/wallet-request/encryption/helpers/index.ts b/packages/dapp-toolkit/src/wallet-request/encryption/helpers/index.ts
new file mode 100644
index 00000000..fd153485
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/encryption/helpers/index.ts
@@ -0,0 +1,2 @@
+export * from './buffer-reader'
+export * from './sealbox'
diff --git a/packages/dapp-toolkit/src/wallet-request/encryption/helpers/sealbox.ts b/packages/dapp-toolkit/src/wallet-request/encryption/helpers/sealbox.ts
new file mode 100644
index 00000000..b21c8016
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/encryption/helpers/sealbox.ts
@@ -0,0 +1,48 @@
+import { Buffer } from 'buffer'
+import { Result } from 'neverthrow'
+import { readBuffer } from './buffer-reader'
+
+export type SealedBoxProps = {
+ ciphertext: Buffer
+ iv: Buffer
+ authTag: Buffer
+ combined: Buffer
+ ciphertextAndAuthTag: Buffer
+}
+
+const combineSealboxToBuffer = ({
+ iv,
+ ciphertext,
+ authTag,
+}: Pick): Buffer =>
+ Buffer.concat([iv, ciphertext, authTag])
+
+const combineCiphertextAndAuthtag = ({
+ ciphertext,
+ authTag,
+}: Pick): Buffer =>
+ Buffer.concat([ciphertext, authTag])
+
+export const transformBufferToSealbox = (
+ buffer: Buffer,
+): Result => {
+ const readNextBuffer = readBuffer(buffer)
+
+ const nonceLength = 12
+ const authTagLength = 16
+
+ return Result.combine([
+ readNextBuffer(nonceLength),
+ readNextBuffer(buffer.length - nonceLength - authTagLength),
+ readNextBuffer(authTagLength),
+ ]).map(([iv, ciphertext, authTag]: Buffer[]) => ({
+ iv,
+ ciphertext,
+ authTag,
+ combined: combineSealboxToBuffer({ iv, ciphertext, authTag }),
+ ciphertextAndAuthTag: combineCiphertextAndAuthtag({
+ ciphertext,
+ authTag,
+ }),
+ }))
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/encryption/index.ts b/packages/dapp-toolkit/src/wallet-request/encryption/index.ts
new file mode 100644
index 00000000..23156d1f
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/encryption/index.ts
@@ -0,0 +1,2 @@
+export * from './helpers'
+export * from './encryption'
diff --git a/packages/dapp-toolkit/src/wallet-request/identity/identity.ts b/packages/dapp-toolkit/src/wallet-request/identity/identity.ts
new file mode 100644
index 00000000..258f58bc
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/identity/identity.ts
@@ -0,0 +1,64 @@
+import { err, ok, okAsync } from 'neverthrow'
+import { StorageProvider } from '../../storage/local-storage-client'
+import type { KeyPairProvider } from '../crypto'
+
+export const IdentityKind = {
+ dApp: 'dApp',
+} as const
+export type IdentityKind = (typeof IdentityKind)[keyof typeof IdentityKind]
+export type IdentitySecret = { secret: string; createdAt: number }
+export type IdentityStore = {
+ [IdentityKind.dApp]: IdentitySecret
+}
+
+export type IdentityClient = ReturnType
+export const IdentityClient = (input: {
+ providers: {
+ storageClient: StorageProvider
+ KeyPairClient: KeyPairProvider
+ }
+}) => {
+ const { storageClient, KeyPairClient } = input.providers
+
+ const keyPairFromSecret = (input: string) => ok(KeyPairClient(input))
+
+ const getIdentity = (kind: IdentityKind) =>
+ storageClient
+ .getItemById(kind)
+ .andThen((identity) =>
+ identity ? keyPairFromSecret(identity.secret) : okAsync(undefined),
+ )
+
+ const createIdentity = (kind: IdentityKind) =>
+ ok(KeyPairClient()).asyncAndThen((keyPair) =>
+ storageClient
+ .setItems({
+ [kind]: {
+ secret: keyPair.getPrivateKey(),
+ createdAt: Date.now(),
+ },
+ })
+ .map(() => keyPair),
+ )
+
+ const getOrCreateIdentity = (kind: IdentityKind) =>
+ getIdentity(kind).andThen((keyPair) =>
+ keyPair ? okAsync(keyPair) : createIdentity(kind),
+ )
+
+ const deriveSharedSecret = (kind: IdentityKind, publicKey: string) =>
+ getIdentity(kind)
+ .mapErr(() => ({ reason: 'couldNotDeriveSharedSecret' }))
+ .andThen((identity) =>
+ identity
+ ? identity.calculateSharedSecret(publicKey).mapErr(() => ({
+ reason: 'FailedToDeriveSharedSecret',
+ }))
+ : err({ reason: 'DappIdentityNotFound' }),
+ )
+
+ return {
+ get: (kind: IdentityKind) => getOrCreateIdentity(kind),
+ deriveSharedSecret,
+ }
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/identity/tests/ecdh-key-exchange.test.ts b/packages/dapp-toolkit/src/wallet-request/identity/tests/ecdh-key-exchange.test.ts
new file mode 100644
index 00000000..998232b4
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/identity/tests/ecdh-key-exchange.test.ts
@@ -0,0 +1,15 @@
+import { describe, it, expect } from 'vitest'
+import { testVectors } from './test-vectors/shared-secret-derivation'
+import { Curve25519 } from '../../crypto'
+
+describe('ECDH key exchange', () => {
+ it('should calculate shared secret', () => {
+ for (const { privateKey1, publicKey2, sharedSecret } of testVectors) {
+ expect(
+ Curve25519(privateKey1)
+ .calculateSharedSecret(publicKey2)
+ ._unsafeUnwrap(),
+ ).toBe(sharedSecret)
+ }
+ })
+})
diff --git a/packages/dapp-toolkit/src/wallet-request/identity/tests/test-vectors/shared-secret-derivation.ts b/packages/dapp-toolkit/src/wallet-request/identity/tests/test-vectors/shared-secret-derivation.ts
new file mode 100644
index 00000000..9e363213
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/identity/tests/test-vectors/shared-secret-derivation.ts
@@ -0,0 +1,122 @@
+export const testVectors = [
+ {
+ privateKey1:
+ 'f88ba5bd75d92e9d9e2ca5e033e0df30b5d2037265ebd8a89e792e1b3c1a597b',
+ publicKey1:
+ '47f648fd909bdf8910e0a0f26aa424add445ed50dc24d7b71e96966292d23b54',
+ privateKey2:
+ '480032c1a072c054e467d74de3317fe8edf7a5474b506dbef72c4a7a0f426a6c',
+ publicKey2:
+ 'd75c0da37485122bdfd27998015791d34b27f5b1d0ff013b826002e3c825613a',
+ sharedSecret:
+ '1ce63dd27a4c1a65a4d4b8e6e9c7e3b486d4c44f247dbb08a47501d80799ea4a',
+ },
+ {
+ privateKey1:
+ 'e00d611e55082faca1c18cc35f1db9871a7cd9ebf5dd4ac5a7b5bde9c608f646',
+ publicKey1:
+ '3c6c22a393762e70fafcab149ef4b4cb730bdfd4c34c44490b697a8ecdb62e4b',
+ privateKey2:
+ '409efa7d7f55402c0a3d966bf2bdb3813ef82ca8d69f11edaf153a31121e3b46',
+ publicKey2:
+ '3b432de67106d808e2618492c3fa8a2c62e4541600b03b19ef6ad8c0a923a45f',
+ sharedSecret:
+ '31a1bdd2e5e5e8d2fb4c5aed7d42076216543b6debe6427868a0d67de1e25112',
+ },
+ {
+ privateKey1:
+ '7034294f6c96cdfef60c49f63c979e13a2526ef55844d23899afc4b5d0391c6e',
+ publicKey1:
+ '6a692c52266a048d4141aeb8e429ab45a54a5a97a1a616e6381758a203f7a814',
+ privateKey2:
+ '587c3d754780951801818145e83a8ce234af8e51218b94062efabafb08848345',
+ publicKey2:
+ '793638fae78fd3ae006295fe935243f4cde600897595b3fe8efe0df22061f830',
+ sharedSecret:
+ '0dc255663605768d1fe8e119eac29f37eed2eb06c546d505fe03fd949dad203f',
+ },
+ {
+ privateKey1:
+ '40744d2a93a6c1819165326d0fb3fca37ba4d18d31a4151b0755de3e4f5d7a6b',
+ publicKey1:
+ 'dbdd1f8b4d6cf5080295e6357dd0593d174b164db384eea81b5f5f64182c2037',
+ privateKey2:
+ '70e8784815a656fb1517a99f2ba5feb7688accb59f9d5de156c585920a70e747',
+ publicKey2:
+ '1beec61acc925777849084d7ee391684d38f098fa915453f28e57030abadd557',
+ sharedSecret:
+ 'a41c50f907e0372fbf192f3c7586bb3d532205cd199df0845f828d9c55c99c1c',
+ },
+ {
+ privateKey1:
+ '783c0fd4460bf247f419e54bddf17cc8e64034d24403dc72856ba85849fc6c53',
+ publicKey1:
+ 'a826d7cdf3228ff077f2b6a4984facf366e243099b1ad67f40384f5b7592ba57',
+ privateKey2:
+ 'd024d9b48f90b94e71a322345cf85f8131383f42fd4b784dea0c036834305852',
+ publicKey2:
+ 'e9eb0baa91c72e30d01c01158b559da2bedd7b42a2672b1c589585750f3de658',
+ sharedSecret:
+ '7624bd005c3c231b4e6194cd6ad5cfdef7a41ee02a94ab42a0d95399a10c0d0e',
+ },
+ {
+ privateKey1:
+ 'a0f35c9fda5bff0ca179aeda3d9218ccf0b6d66340732a1a102baeb9f5850840',
+ publicKey1:
+ 'de4c5057794515268a825548ce46d84b0b740d962e52e4752cbcc57f7148ed08',
+ privateKey2:
+ 'a0699ec7de6b6adda9aa253740855559dab601431ef5703beed1d83a7352cc6f',
+ publicKey2:
+ '4a60f1d8af2b9ea5a78010eb095d8d8c65ae6dee7dba23b8868ca1801855425e',
+ sharedSecret:
+ '6badb61762c79c58aeba50408dbb696b81c34e72c062ecbbfa484f574f645c4e',
+ },
+ {
+ privateKey1:
+ '70d1b30b5be3ee6bc45ecbfff3e46f964401d3ab451fa635055ac1427ce9f47f',
+ publicKey1:
+ 'fa3b138174d6461eecb28c7b7a70345aceee6521ae65a146987bd5b264f86465',
+ privateKey2:
+ '002dff97f81ea720a9d92cee45582d26b15498ea1507b332d7566e12b159bc51',
+ publicKey2:
+ 'cc66c66e8a830ad2003fddd5e9a723b4b558c77f5b9f66641c87cdd369f5cd4c',
+ sharedSecret:
+ 'd4c7fb9ce163b82ee64894fe1ad420464f8d4b842608f70475141b9152f2683d',
+ },
+ {
+ privateKey1:
+ 'c8c8674e9a0dd7ab221940bb0d3fe320a4482af9575b2ccffc4f11296487c150',
+ publicKey1:
+ '95a74d959aa2089d480581c92141216ef62e69252ce0a59f5a93ab4a7b01892e',
+ privateKey2:
+ '68d341eff2b8b089d3ea1e28d2ee99c9368093852aa0de1475a9743a8d0a535c',
+ publicKey2:
+ '8f618f9eee8eb6492ba5fcc2d5c8fd31432aac8e0281f1c9afc44a8b33261724',
+ sharedSecret:
+ '61e93b25df04d60dfca080d806e7c237b847cb922ff09652d2bea91060d78a77',
+ },
+ {
+ privateKey1:
+ '008daa0d6b658330f888e3718398bcfacc2808da7d5bf4a805985b13398f3e5d',
+ publicKey1:
+ '00ae3c420e1650602392c3e1feccac423c653f0f119ae2e6a6ac0909f19df03c',
+ privateKey2:
+ '80067cf3cb55d2e51a9ec8a5e93ffb56ef9defa0d3f936c146498e0cb59e3169',
+ publicKey2:
+ 'f38c1c87c273b2ec5a957b2f0112de3c711e06eee1d16186ecfd9e4f2730286f',
+ sharedSecret:
+ '99691583adda005d1ef709bc0b3725e5cb2359bbdd898e984eeac08ccb228941',
+ },
+ {
+ privateKey1:
+ '70e81bc806680a779dee42cc5fcfa36442fb3dab2d4c4c8a89a86c1871df806d',
+ publicKey1:
+ 'd66b689713219f94f819ab39f009a315613bf722faf45d6a4c3d3f13f5f15a15',
+ privateKey2:
+ '48271a4aa53202da847ed8175feabd4ffcf0b3052a86daaad527180fcaf6226f',
+ publicKey2:
+ '6e5a9113da4446a46b9b8e2d5616a32602655a5bc9d5e997b5a0e8fc5cf6dd64',
+ sharedSecret:
+ '67d975dedf4ef073c9083222499fd987511f4b8d4055cf7d38e2f5d08627232f',
+ },
+]
diff --git a/packages/dapp-toolkit/src/wallet-request/index.ts b/packages/dapp-toolkit/src/wallet-request/index.ts
new file mode 100644
index 00000000..e0f97476
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/index.ts
@@ -0,0 +1,8 @@
+export * from './crypto'
+export * from './data-request'
+export * from './identity/identity'
+export * from './request-items'
+export * from './session/session'
+export * from './transport'
+export * from './wallet-request-sdk'
+export * from './wallet-request'
diff --git a/packages/dapp-toolkit/src/wallet-request/request-items/index.ts b/packages/dapp-toolkit/src/wallet-request/request-items/index.ts
new file mode 100644
index 00000000..4d120f4c
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/request-items/index.ts
@@ -0,0 +1,2 @@
+export * from './request-item-client'
+export * from './subjects'
diff --git a/packages/dapp-toolkit/src/wallet-request/request-items/request-item-client.ts b/packages/dapp-toolkit/src/wallet-request/request-items/request-item-client.ts
new file mode 100644
index 00000000..9d112650
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/request-items/request-item-client.ts
@@ -0,0 +1,129 @@
+import type { RequestItem, RequestStatusTypes } from 'radix-connect-common'
+import { Subscription } from 'rxjs'
+import { RequestItemSubjects } from './subjects'
+import { Logger } from '../../helpers'
+import { ErrorType } from '../../error'
+import { WalletInteraction } from '../../schemas'
+import { StorageProvider } from '../../storage'
+import { ResultAsync, errAsync } from 'neverthrow'
+export type RequestItemClientInput = {
+ logger?: Logger
+ subjects?: RequestItemSubjects
+ providers: { storageClient: StorageProvider }
+}
+export type RequestItemClient = ReturnType
+export const RequestItemClient = (input: RequestItemClientInput) => {
+ const logger = input?.logger?.getSubLogger({ name: 'RequestItemClient' })
+ const subscriptions = new Subscription()
+ const subjects = input.subjects || RequestItemSubjects()
+ const storageClient = input.providers.storageClient
+
+ storageClient.getItemList().map((items) => {
+ logger?.debug({ method: 'initRequestItems', items })
+ subjects.items.next(items)
+ })
+
+ const createItem = ({
+ type,
+ walletInteraction,
+ isOneTimeRequest,
+ }: {
+ type: RequestItem['type']
+ walletInteraction: WalletInteraction
+ isOneTimeRequest: boolean
+ }): RequestItem => ({
+ type,
+ status: 'pending',
+ createdAt: Date.now(),
+ interactionId: walletInteraction.interactionId,
+ showCancel: true,
+ walletInteraction,
+ isOneTimeRequest,
+ })
+
+ const add = (value: {
+ type: RequestItem['type']
+ walletInteraction: WalletInteraction
+ isOneTimeRequest: boolean
+ }) => {
+ const item = createItem(value)
+ logger?.debug({
+ method: 'addRequestItem',
+ item,
+ })
+ return storageClient
+ .setItems({ [item.interactionId]: item })
+ .map(() => item)
+ }
+
+ const patch = (id: string, partialValue: Partial) => {
+ logger?.debug({
+ method: 'patchRequestItemStatus',
+ item: { id, ...partialValue },
+ })
+ return storageClient.patchItem(id, partialValue)
+ }
+
+ const cancel = (id: string) => {
+ logger?.debug({ method: 'cancelRequestItem', id })
+ return patch(id, { status: 'fail', error: ErrorType.canceledByUser })
+ }
+
+ const updateStatus = ({
+ id,
+ status,
+ error,
+ transactionIntentHash,
+ }: {
+ id: string
+ status: RequestStatusTypes
+ error?: string
+ transactionIntentHash?: string
+ }): ResultAsync => {
+ return storageClient
+ .getItemById(id)
+ .mapErr(() => ({ reason: 'couldNotReadFromStore' }))
+ .andThen((item) => {
+ if (item) {
+ const updated = {
+ ...item,
+ status,
+ } as RequestItem
+ if (updated.status === 'fail') {
+ updated.error = error!
+ }
+ if (
+ updated.status === 'success' &&
+ updated.type === 'sendTransaction'
+ ) {
+ updated.transactionIntentHash = transactionIntentHash!
+ }
+ logger?.debug({ method: 'updateRequestItemStatus', updated })
+ return storageClient
+ .setItems({ [id]: updated })
+ .mapErr(() => ({ reason: 'couldNotWriteToStore' }))
+ }
+ return errAsync({ reason: 'itemNotFound' })
+ })
+ }
+
+ const getPendingItems = () =>
+ storageClient
+ .getItems()
+ .map((items) =>
+ Object.values(items).filter((item) => !item.walletResponse),
+ )
+
+ return {
+ add,
+ cancel,
+ updateStatus,
+ patch,
+ getById: (id: string) => storageClient.getItemById(id),
+ getPendingItems,
+ store: storageClient,
+ destroy: () => {
+ subscriptions.unsubscribe()
+ },
+ }
+}
diff --git a/src/request-items/subjects.ts b/packages/dapp-toolkit/src/wallet-request/request-items/subjects.ts
similarity index 88%
rename from src/request-items/subjects.ts
rename to packages/dapp-toolkit/src/wallet-request/request-items/subjects.ts
index d27f2543..0872374a 100644
--- a/src/request-items/subjects.ts
+++ b/packages/dapp-toolkit/src/wallet-request/request-items/subjects.ts
@@ -1,4 +1,4 @@
-import { RequestItem } from '@radixdlt/connect-button'
+import { RequestItem } from 'radix-connect-common'
import { BehaviorSubject, Subject } from 'rxjs'
export type RequestItemChange = {
diff --git a/packages/dapp-toolkit/src/wallet-request/session/session.ts b/packages/dapp-toolkit/src/wallet-request/session/session.ts
new file mode 100644
index 00000000..b867e45f
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/session/session.ts
@@ -0,0 +1,118 @@
+import type { ResultAsync } from 'neverthrow'
+import { errAsync, okAsync } from 'neverthrow'
+import type { IdentityClient } from '../identity/identity'
+import { StorageProvider } from '../../storage/local-storage-client'
+
+type Status = (typeof Status)[keyof typeof Status]
+const Status = { Pending: 'Pending', Active: 'Active' } as const
+
+export type PendingSession = {
+ status: typeof Status.Pending
+ createdAt: number
+ sessionId: string
+ sentToWallet: boolean
+}
+
+export type ActiveSession = {
+ status: typeof Status.Active
+ walletIdentity: string
+ createdAt: number
+ sharedSecret: string
+ sessionId: string
+}
+
+export type Session = PendingSession | ActiveSession
+
+export type SessionClient = ReturnType
+export const SessionClient = (input: {
+ providers: {
+ storageClient: StorageProvider
+ identityClient: IdentityClient
+ }
+}) => {
+ const storageClient = input.providers.storageClient
+ const identityClient = input.providers.identityClient
+
+ const findActiveSession = (): ResultAsync<
+ ActiveSession | undefined,
+ { reason: string }
+ > =>
+ storageClient
+ .getItems()
+ .mapErr(() => ({ reason: 'couldNotReadFromStore' }))
+ .map((sessions) => {
+ const activeSession = Object.values(sessions).find(
+ (session): session is ActiveSession =>
+ session.status === Status.Active,
+ )
+ return activeSession
+ })
+
+ const getSessionById = (sessionId: string) =>
+ storageClient.getItemById(sessionId)
+
+ const createSession = (): ResultAsync => {
+ const sessionId = crypto.randomUUID()
+ const newSession: PendingSession = {
+ sessionId,
+ status: Status.Pending,
+ createdAt: Date.now(),
+ sentToWallet: false,
+ }
+
+ return storageClient
+ .setItems({ [sessionId]: newSession })
+ .map(() => newSession)
+ }
+
+ const patchSession = (sessionId: string, value: Partial) =>
+ storageClient.patchItem(sessionId, value)
+
+ const convertToActiveSession = (
+ sessionId: string,
+ walletIdentity: string,
+ ): ResultAsync =>
+ storageClient
+ .getItemById(sessionId)
+ .mapErr(() => ({ reason: 'readFromStorageError' }))
+ .andThen((session) =>
+ session && session.status === Status.Pending
+ ? identityClient
+ .deriveSharedSecret('dApp', walletIdentity)
+ .andThen((sharedSecret) =>
+ storageClient
+ .setItems({
+ [sessionId]: {
+ ...session,
+ status: Status.Active,
+ walletIdentity,
+ sharedSecret,
+ },
+ })
+ .map(() => ({
+ ...session,
+ status: Status.Active,
+ walletIdentity,
+ sharedSecret,
+ }))
+ .mapErr(() => ({ reason: 'writeToStorageError' })),
+ )
+ : errAsync({ reason: 'sessionNotPending' }),
+ )
+
+ const getCurrentSession = (): ResultAsync =>
+ findActiveSession().andThen((activeSession) =>
+ activeSession
+ ? okAsync(activeSession)
+ : createSession().mapErr(() => ({ reason: 'couldNotCreateSession' })),
+ )
+
+ return {
+ getCurrentSession,
+ convertToActiveSession,
+ findActiveSession,
+ store: storageClient,
+ getSessionById,
+ patchSession,
+ }
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/connector-extension-client.ts b/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/connector-extension-client.ts
new file mode 100644
index 00000000..e6d9afc6
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/connector-extension-client.ts
@@ -0,0 +1,279 @@
+import { ConnectorExtensionSubjects } from './subjects'
+
+import { Err, Result, ResultAsync, err, ok } from 'neverthrow'
+import {
+ Subject,
+ Subscription,
+ filter,
+ first,
+ firstValueFrom,
+ map,
+ merge,
+ mergeMap,
+ of,
+ race,
+ share,
+ shareReplay,
+ switchMap,
+ takeUntil,
+ tap,
+ timer,
+} from 'rxjs'
+import { Logger, isMobile, unwrapObservable } from '../../../helpers'
+import {
+ CallbackFns,
+ IncomingMessage,
+ MessageLifeCycleExtensionStatusEvent,
+ WalletInteraction,
+ WalletInteractionResponse,
+ eventType,
+} from '../../../schemas'
+import { SdkError } from '../../../error'
+import { RequestItemClient } from '../../request-items'
+
+export type ConnectorExtensionClient = ReturnType<
+ typeof ConnectorExtensionClient
+>
+
+export const ConnectorExtensionClient = (input: {
+ subjects?: ConnectorExtensionSubjects
+ logger?: Logger
+ extensionDetectionTime?: number
+ providers: {
+ requestItemClient: RequestItemClient
+ }
+}) => {
+ const logger = input?.logger?.getSubLogger({
+ name: 'ConnectorExtensionClient',
+ })
+ const subjects = input?.subjects ?? ConnectorExtensionSubjects()
+ const subscription = new Subscription()
+ const extensionDetectionTime = input?.extensionDetectionTime ?? 100
+ const requestItemClient = input.providers.requestItemClient
+
+ subscription.add(
+ subjects.incomingMessageSubject
+ .pipe(
+ tap((message) => {
+ logger?.debug({
+ method: 'incomingMessageSubject',
+ message,
+ })
+ if ('eventType' in message) {
+ subjects.messageLifeCycleEventSubject.next(message)
+ } else {
+ subjects.responseSubject.next(message)
+ }
+ }),
+ )
+ .subscribe(),
+ )
+ subscription.add(
+ subjects.outgoingMessageSubject
+ .pipe(
+ tap((payload) => {
+ logger?.debug({
+ method: 'outgoingMessageSubject',
+ payload,
+ })
+ window.dispatchEvent(
+ new CustomEvent(eventType.outgoingMessage, {
+ detail: payload,
+ }),
+ )
+ }),
+ )
+ .subscribe(),
+ )
+
+ const handleIncomingMessage = (event: Event) => {
+ const message = (event as CustomEvent).detail
+ subjects.incomingMessageSubject.next(message)
+ }
+
+ addEventListener(eventType.incomingMessage, handleIncomingMessage)
+
+ const sendWalletInteraction = (
+ walletInteraction: WalletInteraction,
+ callbackFns: Partial,
+ ): ResultAsync => {
+ const cancelRequestSubject = new Subject>()
+
+ const walletResponse$ = subjects.responseSubject.pipe(
+ filter(
+ (response) =>
+ response.interactionId === walletInteraction.interactionId,
+ ),
+ mergeMap(
+ (walletResponse): ResultAsync =>
+ requestItemClient
+ .patch(walletResponse.interactionId, {
+ walletResponse,
+ })
+ .mapErr(() =>
+ SdkError('requestItemPatchError', walletResponse.interactionId),
+ )
+ .map(() => walletResponse),
+ ),
+ )
+
+ const cancelResponse$ = subjects.messageLifeCycleEventSubject.pipe(
+ filter(
+ ({ interactionId, eventType }) =>
+ walletInteraction.interactionId === interactionId &&
+ ['requestCancelSuccess', 'requestCancelFail'].includes(eventType),
+ ),
+ map((message) => {
+ const error = SdkError('canceledByUser', message.interactionId)
+ logger?.debug(`🔵⬆️❌ walletRequestCanceled`, error)
+ return message
+ }),
+ )
+
+ const sendCancelRequest = () => {
+ subjects.outgoingMessageSubject.next({
+ interactionId: walletInteraction.interactionId,
+ items: { discriminator: 'cancelRequest' },
+ metadata: walletInteraction.metadata,
+ })
+
+ setTimeout(() => {
+ cancelRequestSubject.next(
+ err(SdkError('canceledByUser', walletInteraction.interactionId)),
+ )
+ })
+
+ return ResultAsync.fromSafePromise(
+ firstValueFrom(
+ merge(
+ walletResponse$.pipe(map(() => 'requestCancelFail')),
+ cancelResponse$.pipe(map(({ eventType }) => eventType)),
+ ),
+ ),
+ )
+ }
+
+ if (callbackFns.requestControl)
+ callbackFns.requestControl({
+ cancelRequest: () =>
+ sendCancelRequest().andThen(
+ (eventType): Result<'requestCancelSuccess', 'requestCancelFail'> =>
+ eventType === 'requestCancelSuccess'
+ ? ok('requestCancelSuccess')
+ : err('requestCancelFail'),
+ ),
+ getRequest: () => walletInteraction,
+ })
+
+ const walletResponseOrCancelRequest$ = merge(
+ walletResponse$,
+ cancelRequestSubject,
+ ).pipe(first())
+
+ const messageLifeCycleEvent$ = subjects.messageLifeCycleEventSubject.pipe(
+ filter(
+ ({ interactionId }) =>
+ walletInteraction.interactionId === interactionId,
+ ),
+ tap((event) => {
+ if (callbackFns.eventCallback)
+ callbackFns.eventCallback(event.eventType)
+ }),
+ takeUntil(walletResponse$),
+ share(),
+ )
+
+ const messageEventSubscription = messageLifeCycleEvent$.subscribe()
+
+ const missingExtensionError$ = timer(extensionDetectionTime).pipe(
+ map(() =>
+ err(SdkError('missingExtension', walletInteraction.interactionId)),
+ ),
+ )
+
+ const extensionMissingError$ = merge(
+ missingExtensionError$,
+ messageLifeCycleEvent$,
+ ).pipe(
+ first(),
+ filter((value): value is Err => !('eventType' in value)),
+ )
+
+ const sendWalletRequest$ = of(walletInteraction).pipe(
+ tap((message) => {
+ subjects.outgoingMessageSubject.next(message)
+ }),
+ filter((_): _ is never => false),
+ )
+
+ return unwrapObservable(
+ merge(
+ walletResponseOrCancelRequest$,
+ extensionMissingError$,
+ sendWalletRequest$,
+ ).pipe(
+ tap(() => {
+ messageEventSubscription.unsubscribe()
+ }),
+ ),
+ )
+ }
+
+ const extensionStatusEvent$ = subjects.messageLifeCycleEventSubject.pipe(
+ filter(
+ (event): event is MessageLifeCycleExtensionStatusEvent =>
+ event.eventType === 'extensionStatus',
+ ),
+ )
+
+ const extensionStatus$ = of(true).pipe(
+ tap(() => {
+ subjects.outgoingMessageSubject.next({
+ interactionId: crypto.randomUUID(),
+ discriminator: 'extensionStatus',
+ })
+ }),
+ switchMap(() =>
+ race(
+ extensionStatusEvent$,
+ merge(
+ extensionStatusEvent$,
+ timer(extensionDetectionTime).pipe(
+ map(
+ () =>
+ ({
+ eventType: 'extensionStatus',
+ isWalletLinked: false,
+ isExtensionAvailable: false,
+ }) as MessageLifeCycleExtensionStatusEvent,
+ ),
+ ),
+ ),
+ ),
+ ),
+ shareReplay(1),
+ )
+
+ return {
+ isSupported: () => !isMobile(),
+ send: sendWalletInteraction,
+ isAvailable$: extensionStatus$.pipe(
+ map(({ isExtensionAvailable }) => isExtensionAvailable),
+ ),
+ isLinked$: extensionStatus$.pipe(
+ map(({ isWalletLinked }) => isWalletLinked),
+ ),
+ showQrCode: () => {
+ window.dispatchEvent(
+ new CustomEvent(eventType.outgoingMessage, {
+ detail: { discriminator: 'openPopup' },
+ }),
+ )
+ },
+ disconnect: () => {},
+ destroy: () => {
+ subscription.unsubscribe()
+ removeEventListener(eventType.incomingMessage, handleIncomingMessage)
+ },
+ }
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/index.ts b/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/index.ts
new file mode 100644
index 00000000..1acd0cf5
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/index.ts
@@ -0,0 +1,2 @@
+export * from './connector-extension-client'
+export * from './subjects'
diff --git a/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/subjects.ts b/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/subjects.ts
new file mode 100644
index 00000000..712618e7
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/transport/connector-extension/subjects.ts
@@ -0,0 +1,25 @@
+import { Subject } from 'rxjs'
+import type {
+ ExtensionInteraction,
+ MessageLifeCycleEvent,
+ MessageLifeCycleExtensionStatusEvent,
+ WalletInteraction,
+ WalletInteractionResponse,
+} from '../../../schemas'
+
+export type ConnectorExtensionSubjects = ReturnType<
+ typeof ConnectorExtensionSubjects
+>
+
+export const ConnectorExtensionSubjects = () => ({
+ outgoingMessageSubject: new Subject<
+ WalletInteraction | ExtensionInteraction
+ >(),
+ incomingMessageSubject: new Subject<
+ | MessageLifeCycleEvent
+ | MessageLifeCycleExtensionStatusEvent
+ | WalletInteractionResponse
+ >(),
+ responseSubject: new Subject(),
+ messageLifeCycleEventSubject: new Subject(),
+})
diff --git a/packages/dapp-toolkit/src/wallet-request/transport/index.ts b/packages/dapp-toolkit/src/wallet-request/transport/index.ts
new file mode 100644
index 00000000..409ff2c3
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/transport/index.ts
@@ -0,0 +1 @@
+export * from './connector-extension'
diff --git a/packages/dapp-toolkit/src/wallet-request/wallet-request-sdk.ts b/packages/dapp-toolkit/src/wallet-request/wallet-request-sdk.ts
new file mode 100644
index 00000000..20e53cdf
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/wallet-request-sdk.ts
@@ -0,0 +1,126 @@
+import { ResultAsync, errAsync, okAsync } from 'neverthrow'
+import { TransportProvider } from '../_types'
+import { Logger, validateWalletResponse } from '../helpers'
+import {
+ Metadata,
+ CallbackFns,
+ WalletInteractionItems,
+ WalletInteraction,
+ WalletInteractionResponse,
+} from '../schemas'
+import { parse } from 'valibot'
+import { SdkError } from '../error'
+
+export type WalletRequestSdkInput = {
+ networkId: number
+ dAppDefinitionAddress: string
+ logger?: Logger
+ origin?: string
+ requestInterceptor?: (
+ walletInteraction: WalletInteraction,
+ ) => Promise
+ providers: {
+ transports: TransportProvider[]
+ }
+}
+export type WalletRequestSdk = ReturnType
+
+export const WalletRequestSdk = (input: WalletRequestSdkInput) => {
+ const metadata = {
+ version: 2,
+ dAppDefinitionAddress: input.dAppDefinitionAddress,
+ networkId: input.networkId,
+ origin: input.origin || window.location.origin,
+ } as Metadata
+
+ parse(Metadata, metadata)
+
+ const logger = input?.logger?.getSubLogger({ name: 'WalletSdk' })
+ const availableTransports = input.providers.transports
+
+ const requestInterceptorDefault = async (
+ walletInteraction: WalletInteraction,
+ ) => walletInteraction
+
+ const requestInterceptor =
+ input.requestInterceptor ?? requestInterceptorDefault
+
+ logger?.debug({ metadata })
+
+ const createWalletInteraction = (
+ items: WalletInteractionItems,
+ interactionId = crypto.randomUUID(),
+ ): WalletInteraction => ({
+ items,
+ interactionId,
+ metadata,
+ })
+
+ const withInterceptor = (
+ payload: WalletInteraction,
+ ): ResultAsync =>
+ ResultAsync.fromPromise(requestInterceptor(payload), (error: any) =>
+ SdkError('requestInterceptorError', payload.interactionId, error.message),
+ )
+
+ const getTransportClient = (
+ interactionId: string,
+ ): ResultAsync => {
+ const transportClient = availableTransports.find((transportClient) =>
+ transportClient.isSupported(),
+ )
+
+ return transportClient
+ ? okAsync(transportClient)
+ : errAsync({
+ error: 'SupportedTransportNotFound',
+ interactionId,
+ message: 'No supported transport found',
+ })
+ }
+
+ const request = (
+ {
+ interactionId = crypto.randomUUID(),
+ items,
+ }: Pick & { interactionId?: string },
+ callbackFns: Partial = {},
+ ): ResultAsync =>
+ withInterceptor({
+ items,
+ interactionId,
+ metadata,
+ }).andThen((walletInteraction) =>
+ getTransportClient(walletInteraction.interactionId).andThen(
+ (transportClient) =>
+ transportClient
+ .send(walletInteraction, callbackFns)
+ .andThen(validateWalletResponse),
+ ),
+ )
+
+ const sendTransaction = (
+ {
+ interactionId = crypto.randomUUID(),
+ items,
+ }: { interactionId?: string; items: WalletInteraction['items'] },
+ callbackFns: Partial = {},
+ ): ResultAsync =>
+ withInterceptor({
+ interactionId,
+ items,
+ metadata,
+ }).andThen((walletInteraction) =>
+ getTransportClient(interactionId).andThen((transportClient) =>
+ transportClient
+ .send(walletInteraction, callbackFns)
+ .andThen(validateWalletResponse),
+ ),
+ )
+
+ return {
+ request,
+ sendTransaction,
+ createWalletInteraction,
+ }
+}
diff --git a/packages/dapp-toolkit/src/wallet-request/wallet-request.ts b/packages/dapp-toolkit/src/wallet-request/wallet-request.ts
new file mode 100644
index 00000000..0eee9ab4
--- /dev/null
+++ b/packages/dapp-toolkit/src/wallet-request/wallet-request.ts
@@ -0,0 +1,627 @@
+import { WalletRequestSdk } from './wallet-request-sdk'
+import {
+ Subject,
+ Subscription,
+ filter,
+ firstValueFrom,
+ map,
+ merge,
+ mergeMap,
+ of,
+ switchMap,
+ tap,
+} from 'rxjs'
+import type { GatewayClient } from '../gateway/gateway'
+import { RequestItemClient } from './request-items/request-item-client'
+import type { Logger } from '../helpers'
+import { TransactionStatus } from '../gateway/types'
+import { Result, ResultAsync, err, ok, okAsync } from 'neverthrow'
+import {
+ MessageLifeCycleEvent,
+ WalletInteraction,
+ WalletInteractionResponse,
+} from '../schemas'
+import { SdkError } from '../error'
+import {
+ DataRequestBuilderItem,
+ DataRequestState,
+ DataRequestStateClient,
+ canDataRequestBeResolvedByRdtState,
+ toWalletRequest,
+ transformSharedDataToDataRequestState,
+ transformWalletRequestToSharedData,
+ transformWalletResponseToRdtWalletData,
+} from './data-request'
+import { StorageProvider } from '../storage'
+import { StateClient, WalletData } from '../state'
+import {
+ AwaitedWalletDataRequestResult,
+ TransportProvider,
+ WalletDataRequestResult,
+} from '../_types'
+import { ConnectorExtensionClient } from './transport'
+import { RequestItem } from 'radix-connect-common'
+
+type SendTransactionInput = {
+ transactionManifest: string
+ version?: number
+ blobs?: string[]
+ message?: string
+ onTransactionId?: (transactionId: string) => void
+}
+
+export type WalletRequestClient = ReturnType
+export const WalletRequestClient = (input: {
+ logger?: Logger
+ origin?: string
+ networkId: number
+ useCache: boolean
+ requestInterceptor?: (input: WalletInteraction) => Promise
+ dAppDefinitionAddress: string
+ providers: {
+ stateClient: StateClient
+ storageClient: StorageProvider
+ gatewayClient: GatewayClient
+ transports?: TransportProvider[]
+ dataRequestStateClient?: DataRequestStateClient
+ requestItemClient?: RequestItemClient
+ walletRequestSdk?: WalletRequestSdk
+ }
+}) => {
+ const useCache = input.useCache
+ const networkId = input.networkId
+ const cancelRequestSubject = new Subject()
+ const interactionStatusChangeSubject = new Subject<'fail' | 'success'>()
+ const gatewayClient = input.providers.gatewayClient
+ const dAppDefinitionAddress = input.dAppDefinitionAddress
+ const logger = input.logger?.getSubLogger({ name: 'WalletRequestClient' })
+
+ const stateClient = input.providers.stateClient
+ const storageClient = input.providers.storageClient
+
+ const dataRequestStateClient =
+ input.providers.dataRequestStateClient ?? DataRequestStateClient({})
+
+ const requestItemClient =
+ input.providers.requestItemClient ??
+ RequestItemClient({
+ logger,
+ providers: {
+ storageClient: storageClient.getPartition('requests'),
+ },
+ })
+
+ const transports = input.providers.transports ?? [
+ ConnectorExtensionClient({
+ logger,
+ providers: { requestItemClient },
+ }),
+ ]
+
+ const walletRequestSdk =
+ input.providers.walletRequestSdk ??
+ WalletRequestSdk({
+ logger,
+ networkId,
+ origin: input.origin,
+ dAppDefinitionAddress,
+ requestInterceptor: input.requestInterceptor,
+ providers: { transports },
+ })
+
+ const cancelRequestControl = (id: string) => {
+ const messageLifeCycleEvent = new Subject<
+ MessageLifeCycleEvent['eventType']
+ >()
+ return {
+ eventCallback: (event) => {
+ messageLifeCycleEvent.next(event)
+ },
+ requestControl: ({ cancelRequest, getRequest }) => {
+ firstValueFrom(
+ messageLifeCycleEvent.pipe(
+ filter((event) => event === 'receivedByWallet'),
+ map(() => getRequest()),
+ tap((request) => {
+ if (request.items.discriminator === 'transaction')
+ requestItemClient.patch(id, { showCancel: false })
+ }),
+ ),
+ )
+ firstValueFrom(
+ cancelRequestSubject.pipe(
+ filter((requestItemId) => requestItemId === id),
+ switchMap(() =>
+ requestItemClient.cancel(id).andThen(() => cancelRequest()),
+ ),
+ ),
+ )
+ },
+ } satisfies Parameters[1]
+ }
+
+ let challengeGeneratorFn: () => Promise = () => Promise.resolve('')
+
+ let connectResponseCallback:
+ | ((result: AwaitedWalletDataRequestResult) => any)
+ | undefined
+
+ let dataRequestControl: (
+ walletData: WalletData,
+ ) => ResultAsync
+
+ const isChallengeNeeded = (dataRequestState: DataRequestState) =>
+ dataRequestState.accounts?.withProof || dataRequestState.persona?.withProof
+
+ const getChallenge = (
+ dataRequestState: DataRequestState,
+ ): ResultAsync => {
+ if (!isChallengeNeeded(dataRequestState)) return okAsync(undefined)
+
+ return ResultAsync.fromPromise(challengeGeneratorFn(), () =>
+ SdkError('ChallengeGeneratorError', '', 'failed to generate challenge'),
+ )
+ }
+
+ const provideConnectResponseCallback = (
+ fn: (result: AwaitedWalletDataRequestResult) => any,
+ ) => {
+ connectResponseCallback = (result) => fn(result)
+ }
+
+ const provideDataRequestControl = (
+ fn: (walletData: WalletData) => Promise,
+ ) => {
+ dataRequestControl = (walletData: WalletData) =>
+ ResultAsync.fromPromise(fn(walletData), () => ({
+ error: 'LoginRejectedByDapp',
+ message: 'Login rejected by dApp',
+ }))
+ }
+
+ const sendOneTimeRequest = (...items: DataRequestBuilderItem[]) =>
+ sendRequest({
+ dataRequestState: dataRequestStateClient.toDataRequestState(...items),
+ isConnect: false,
+ oneTime: true,
+ })
+
+ const resolveWalletResponse = (
+ walletInteraction: WalletInteraction,
+ walletInteractionResponse: WalletInteractionResponse,
+ ) => {
+ if (
+ walletInteractionResponse.discriminator === 'success' &&
+ walletInteractionResponse.items.discriminator === 'authorizedRequest'
+ ) {
+ return ResultAsync.combine([
+ transformWalletResponseToRdtWalletData(walletInteractionResponse.items),
+ stateClient.store.getState(),
+ ]).andThen(([walletData, state]) => {
+ return stateClient.store
+ .setState({
+ loggedInTimestamp: Date.now().toString(),
+ walletData,
+ sharedData: transformWalletRequestToSharedData(
+ walletInteraction,
+ state!.sharedData,
+ ),
+ })
+ .andThen(() =>
+ requestItemClient.updateStatus({
+ id: walletInteractionResponse.interactionId,
+ status: 'success',
+ }),
+ )
+ })
+ }
+
+ return requestItemClient.updateStatus({
+ id: walletInteractionResponse.interactionId,
+ status: 'fail',
+ })
+ }
+
+ const sendDataRequest = (walletInteraction: WalletInteraction) => {
+ return walletRequestSdk
+ .request(
+ walletInteraction,
+ cancelRequestControl(walletInteraction.interactionId),
+ )
+ .map((response: WalletInteractionResponse) => {
+ logger?.debug({ method: 'sendDataRequest.successResponse', response })
+
+ return response
+ })
+ .mapErr((error) => {
+ logger?.debug({ method: 'sendDataRequest.errorResponse', error })
+
+ requestItemClient.updateStatus({
+ id: walletInteraction.interactionId,
+ status: 'fail',
+ error: error.error,
+ })
+
+ return error
+ })
+ }
+
+ const sendRequest = ({
+ isConnect,
+ oneTime,
+ dataRequestState,
+ }: {
+ dataRequestState: DataRequestState
+ isConnect: boolean
+ oneTime: boolean
+ }): WalletDataRequestResult => {
+ return ResultAsync.combine([
+ getChallenge(dataRequestState),
+ stateClient.getState().mapErr(() => SdkError('FailedToReadRdtState', '')),
+ ]).andThen(([challenge, state]) =>
+ toWalletRequest({
+ dataRequestState,
+ isConnect,
+ oneTime,
+ challenge,
+ walletData: state.walletData,
+ })
+ .mapErr(() => SdkError('FailedToTransformWalletRequest', ''))
+ .asyncAndThen((walletDataRequest) => {
+ const walletInteraction: WalletInteraction =
+ walletRequestSdk.createWalletInteraction(walletDataRequest)
+
+ if (
+ canDataRequestBeResolvedByRdtState(walletDataRequest, state) &&
+ useCache
+ )
+ return okAsync(state.walletData)
+
+ const isLoginRequest =
+ !state.walletData.persona &&
+ walletDataRequest.discriminator === 'authorizedRequest'
+
+ return requestItemClient
+ .add({
+ type: isLoginRequest ? 'loginRequest' : 'dataRequest',
+ walletInteraction,
+ isOneTimeRequest: oneTime,
+ })
+ .mapErr(({ message }) =>
+ SdkError(
+ 'FailedToCreateRequestItem',
+ walletInteraction.interactionId,
+ message,
+ ),
+ )
+ .andThen(() =>
+ sendDataRequest(walletInteraction)
+ .andThen((walletInteractionResponse) => {
+ if (
+ walletInteractionResponse.discriminator === 'success' &&
+ walletInteractionResponse.items.discriminator !==
+ 'transaction'
+ )
+ return ok(walletInteractionResponse.items)
+
+ return err(
+ SdkError(
+ 'WalletResponseFailure',
+ walletInteractionResponse.interactionId,
+ 'expected data response',
+ ),
+ )
+ })
+ .andThen(transformWalletResponseToRdtWalletData)
+ .andThen((transformedWalletResponse) => {
+ if (dataRequestControl)
+ return dataRequestControl(transformedWalletResponse)
+ .andThen(() =>
+ requestItemClient
+ .updateStatus({
+ id: walletInteraction.interactionId,
+ status: 'success',
+ })
+ .mapErr((error) =>
+ SdkError(
+ error.reason,
+ walletInteraction.interactionId,
+ ),
+ )
+ .map(() => transformedWalletResponse),
+ )
+ .mapErr((error) => {
+ requestItemClient.updateStatus({
+ id: walletInteraction.interactionId,
+ status: 'fail',
+ error: error.error,
+ })
+ return SdkError(
+ error.error,
+ walletInteraction.interactionId,
+ )
+ })
+
+ return requestItemClient
+ .updateStatus({
+ id: walletInteraction.interactionId,
+ status: 'success',
+ })
+ .map(() => transformedWalletResponse)
+ .mapErr((error) =>
+ SdkError(error.reason, walletInteraction.interactionId),
+ )
+ })
+ .map((transformedWalletResponse) => {
+ interactionStatusChangeSubject.next('success')
+
+ if (!oneTime)
+ stateClient.setState({
+ loggedInTimestamp: Date.now().toString(),
+ walletData: transformedWalletResponse,
+ sharedData: transformWalletRequestToSharedData(
+ walletInteraction,
+ state.sharedData,
+ ),
+ })
+
+ return transformedWalletResponse
+ })
+ .mapErr((err) => {
+ interactionStatusChangeSubject.next('fail')
+ return err
+ }),
+ )
+ }),
+ )
+ }
+
+ const setRequestDataState = (...items: DataRequestBuilderItem[]) => {
+ dataRequestStateClient.setState(...items)
+ return {
+ sendRequest: () =>
+ sendRequest({
+ dataRequestState: dataRequestStateClient.getState(),
+ isConnect: false,
+ oneTime: false,
+ }),
+ }
+ }
+
+ const updateSharedData = () =>
+ stateClient.store
+ .getState()
+ .mapErr((err) => {
+ logger?.error(err)
+ return {
+ error: 'FailedToReadRdtState',
+ message: 'failed to read rdt state',
+ jsError: err,
+ }
+ })
+ .andThen((state) =>
+ sendRequest({
+ dataRequestState: transformSharedDataToDataRequestState(
+ state!.sharedData,
+ ),
+ isConnect: false,
+ oneTime: false,
+ }),
+ )
+
+ const subscriptions = new Subscription()
+
+ const requestItemStore$ = merge(requestItemClient.store.storage$, of(null))
+
+ const requestItems$ = requestItemStore$.pipe(
+ switchMap(() => requestItemClient.store.getItemList()),
+ map((result) => {
+ if (result.isOk()) return result.value
+ }),
+ filter((items): items is RequestItem[] => !!items),
+ )
+
+ subscriptions.add(
+ requestItems$
+ .pipe(
+ mergeMap((items) => {
+ const unresolvedItems = items
+ .filter((item) => item.status === 'pending' && item.walletResponse)
+ .map((item) =>
+ resolveWalletResponse(
+ item.walletInteraction,
+ item.walletResponse,
+ ),
+ )
+
+ return ResultAsync.combineWithAllErrors(unresolvedItems)
+ }),
+ )
+ .subscribe(),
+ )
+
+ const sendTransaction = (
+ value: SendTransactionInput,
+ ): ResultAsync<
+ {
+ transactionIntentHash: string
+ status: TransactionStatus
+ },
+ SdkError
+ > => {
+ const walletInteraction = walletRequestSdk.createWalletInteraction({
+ discriminator: 'transaction',
+ send: {
+ blobs: value.blobs,
+ transactionManifest: value.transactionManifest,
+ message: value.message,
+ version: value.version ?? 1,
+ },
+ })
+
+ requestItemClient.add({
+ type: 'sendTransaction',
+ walletInteraction,
+ isOneTimeRequest: false,
+ })
+
+ return walletRequestSdk
+ .sendTransaction(
+ walletInteraction,
+ cancelRequestControl(walletInteraction.interactionId),
+ )
+ .mapErr((response): SdkError => {
+ requestItemClient.updateStatus({
+ id: walletInteraction.interactionId,
+ status: 'fail',
+ error: response.error,
+ })
+ logger?.debug({ method: 'sendTransaction.errorResponse', response })
+ return response
+ })
+ .andThen(
+ (response): Result<{ transactionIntentHash: string }, SdkError> => {
+ logger?.debug({ method: 'sendTransaction.successResponse', response })
+ if (
+ response.discriminator === 'success' &&
+ response.items.discriminator === 'transaction'
+ )
+ return ok(response.items.send)
+
+ if (response.discriminator === 'failure')
+ return err(
+ SdkError(
+ response.error,
+ response.interactionId,
+ response.message,
+ ),
+ )
+
+ return err(SdkError('WalletResponseFailure', response.interactionId))
+ },
+ )
+ .andThen(({ transactionIntentHash }) => {
+ if (value.onTransactionId) value.onTransactionId(transactionIntentHash)
+ return gatewayClient
+ .pollTransactionStatus(transactionIntentHash)
+ .map((transactionStatusResponse) => ({
+ transactionIntentHash,
+ status: transactionStatusResponse.status,
+ }))
+ })
+ .andThen((response) => {
+ const failedTransactionStatus: TransactionStatus[] = [
+ TransactionStatus.Rejected,
+ TransactionStatus.CommittedFailure,
+ ]
+
+ const isFailedTransaction = failedTransactionStatus.includes(
+ response.status,
+ )
+
+ logger?.debug({
+ method: 'sendTransaction.pollTransactionStatus.completed',
+ response,
+ })
+
+ const status = isFailedTransaction ? 'fail' : 'success'
+
+ return requestItemClient
+ .updateStatus({
+ id: walletInteraction.interactionId,
+ status,
+ transactionIntentHash: response.transactionIntentHash,
+ })
+ .mapErr(() =>
+ SdkError(
+ 'FailedToUpdateRequestItem',
+ walletInteraction.interactionId,
+ ),
+ )
+ .andThen(() => {
+ interactionStatusChangeSubject.next(status)
+ return isFailedTransaction
+ ? err(
+ SdkError(
+ 'TransactionNotSuccessful',
+ walletInteraction.interactionId,
+ ),
+ )
+ : ok(response)
+ })
+ })
+ }
+
+ const getTransport = () =>
+ transports.find((transport) => transport.isSupported())
+
+ const getPendingRequests = () =>
+ requestItemClient.store
+ .getItemList()
+ .map((items) => items.filter((item) => item.status === 'pending'))
+
+ const cancelRequest = (id: string) => {
+ cancelRequestSubject.next(id)
+ requestItemClient.cancel(id)
+ interactionStatusChangeSubject.next('fail')
+ }
+
+ const provideChallengeGenerator = (fn: () => Promise) => {
+ challengeGeneratorFn = fn
+ }
+
+ const disconnect = () => {
+ requestItemClient.store.getItemList().map((items) => {
+ items.forEach((item) => {
+ if (item.showCancel) cancelRequestSubject.next(item.interactionId)
+ })
+ })
+
+ stateClient.reset()
+ requestItemClient.store.clear()
+ }
+
+ const destroy = () => {
+ stateClient.destroy()
+ requestItemClient.destroy()
+ input.providers.transports?.forEach((transport) => transport.destroy())
+
+ subscriptions.unsubscribe()
+ }
+
+ return {
+ sendRequest: (input: { isConnect: boolean; oneTime: boolean }) => {
+ const result = sendRequest({
+ isConnect: input.isConnect,
+ oneTime: input.oneTime,
+ dataRequestState: dataRequestStateClient.getState(),
+ })
+
+ if (connectResponseCallback)
+ result
+ .map((result) => {
+ connectResponseCallback!(ok(result))
+ })
+ .mapErr((error) => {
+ connectResponseCallback!(err(error))
+ })
+
+ return result
+ },
+ sendTransaction,
+ cancelRequest,
+ requestItemClient,
+ provideChallengeGenerator,
+ provideDataRequestControl,
+ provideConnectResponseCallback,
+ sendOneTimeRequest,
+ setRequestDataState,
+ getPendingRequests,
+ getTransport,
+ updateSharedData,
+ interactionStatusChange$: interactionStatusChangeSubject.asObservable(),
+ requestItems$,
+ disconnect,
+ destroy,
+ }
+}
diff --git a/packages/dapp-toolkit/tsconfig.json b/packages/dapp-toolkit/tsconfig.json
new file mode 100644
index 00000000..329fe918
--- /dev/null
+++ b/packages/dapp-toolkit/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "module": "es2020",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "target": "ES2021",
+ "moduleResolution": "Bundler",
+ "esModuleInterop": true,
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noImplicitReturns": false,
+ "noFallthroughCasesInSwitch": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "forceConsistentCasingInFileNames": true,
+ "useDefineForClassFields": false,
+ "skipLibCheck": true
+ }
+}
diff --git a/packages/dapp-toolkit/tsup.config.ts b/packages/dapp-toolkit/tsup.config.ts
new file mode 100644
index 00000000..2db96c6a
--- /dev/null
+++ b/packages/dapp-toolkit/tsup.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'tsup'
+
+export default defineConfig({
+ entry: ['src/index.ts'],
+ dts: true,
+ format: ['esm', 'cjs'],
+ noExternal: ['@radixdlt/connect-button'],
+})
diff --git a/vite-single-file.config.ts b/packages/dapp-toolkit/vite-single-file.config.ts
similarity index 100%
rename from vite-single-file.config.ts
rename to packages/dapp-toolkit/vite-single-file.config.ts
diff --git a/packages/dapp-toolkit/vitest.config.ts b/packages/dapp-toolkit/vitest.config.ts
new file mode 100644
index 00000000..95c5d26e
--- /dev/null
+++ b/packages/dapp-toolkit/vitest.config.ts
@@ -0,0 +1,8 @@
+///
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ environment: 'jsdom',
+ },
+})
diff --git a/src/connect-button/connect-button-client.ts b/src/connect-button/connect-button-client.ts
deleted file mode 100644
index 44d857ef..00000000
--- a/src/connect-button/connect-button-client.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-import {
- filter,
- fromEvent,
- map,
- merge,
- of,
- Subscription,
- switchMap,
- tap,
-} from 'rxjs'
-import { Logger } from 'tslog'
-import {
- Account,
- ConnectButton,
- RadixButtonStatus,
- RadixButtonTheme,
- RequestItem,
-} from '@radixdlt/connect-button'
-import { ConnectButtonProvider } from '../_types'
-import { ConnectButtonSubjects } from './subjects'
-
-export const isMobile = () =>
- /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
- globalThis.navigator ? globalThis.navigator.userAgent : ''
- )
-
-export type ConnectButtonClient = ReturnType
-
-export const ConnectButtonClient = (input: {
- onConnect?: (done: (input?: { challenge: string }) => void) => void
- subjects?: ConnectButtonSubjects
- logger?: Logger
-}): ConnectButtonProvider => {
- if (import.meta.env.MODE !== 'test') import('@radixdlt/connect-button')
- const subjects = input.subjects || ConnectButtonSubjects()
- const logger = input.logger
- const onConnectDefault = (done: (input?: { challenge: string }) => void) => {
- done()
- }
- const onConnect = input.onConnect || onConnectDefault
-
- const getConnectButtonElement = (): ConnectButton | null =>
- document.querySelector('radix-connect-button')
-
- const subscriptions = new Subscription()
-
- subscriptions.add(
- merge(
- fromEvent(document, 'onRender'),
- of(getConnectButtonElement()).pipe(filter((e) => !!e))
- )
- .pipe(
- map(() => getConnectButtonElement()),
- filter((element): element is ConnectButton => !!element),
- switchMap((connectButtonElement) => {
- logger?.debug(`connectButtonDiscovered`)
-
- const onConnect$ = fromEvent(connectButtonElement, 'onConnect').pipe(
- tap(() => {
- onConnect((value) => {
- if (
- !connectButtonElement.isWalletLinked ||
- !connectButtonElement.isExtensionAvailable
- )
- return
-
- subjects.onConnect.next(value)
- })
- })
- )
-
- const onDisconnect$ = fromEvent(
- connectButtonElement,
- 'onDisconnect'
- ).pipe(
- tap(() => {
- subjects.onDisconnect.next()
- })
- )
-
- const onLinkClick$ = fromEvent<
- CustomEvent<{ type: 'account' | 'transaction'; data: string }>
- >(connectButtonElement, 'onLinkClick').pipe(
- tap((ev) => {
- subjects.onLinkClick.next(ev.detail)
- })
- )
-
- const onDestroy$ = fromEvent(connectButtonElement, 'onDestroy').pipe(
- tap(() => {
- logger?.debug(`connectButtonRemovedFromDOM`)
- })
- )
-
- const onCancelRequestItem$ = fromEvent(
- connectButtonElement,
- 'onCancelRequestItem'
- ).pipe(
- tap((event) => {
- const id = (event as CustomEvent<{ id: string }>).detail.id
- logger?.debug(`onCancelRequestItem`, { id })
- subjects.onCancelRequestItem.next(id)
- })
- )
-
- const onUpdateSharedData$ = fromEvent(
- connectButtonElement,
- 'onUpdateSharedData'
- ).pipe(
- tap(() => {
- logger?.debug(`onUpdateSharedData`)
- subjects.onUpdateSharedData.next()
- })
- )
-
- const onShowPopover$ = fromEvent(
- connectButtonElement,
- 'onShowPopover'
- ).pipe(
- tap(() => {
- subjects.onShowPopover.next()
- })
- )
-
- const status$ = subjects.status.pipe(
- tap((value) => (connectButtonElement.status = value))
- )
-
- const mode$ = subjects.mode.pipe(
- tap((value) => (connectButtonElement.mode = value))
- )
-
- const connected$ = subjects.connected.pipe(
- tap((value) => {
- connectButtonElement.connected = value
- })
- )
-
- const isMobile$ = subjects.isMobile.pipe(
- tap((value) => {
- connectButtonElement.isMobile = value
- })
- )
-
- const isWalletLinked$ = subjects.isWalletLinked.pipe(
- tap((value) => {
- connectButtonElement.isWalletLinked = value
- })
- )
-
- const isExtensionAvailable$ = subjects.isExtensionAvailable.pipe(
- tap((value) => {
- connectButtonElement.isExtensionAvailable = value
- })
- )
-
- const loggedInTimestamp$ = subjects.loggedInTimestamp.pipe(
- tap((value) => {
- connectButtonElement.loggedInTimestamp = value
- })
- )
-
- const activeTab$ = subjects.activeTab.pipe(
- tap((value) => {
- connectButtonElement.activeTab = value
- })
- )
-
- const requestItems$ = subjects.requestItems.pipe(
- tap((items) => {
- connectButtonElement.requestItems = items
- })
- )
-
- const accounts$ = subjects.accounts.pipe(
- tap((items) => {
- connectButtonElement.accounts = items
- })
- )
-
- const personaData$ = subjects.personaData.pipe(
- tap((items) => {
- // @ts-ignore: TODO: update interface in connect-button
- connectButtonElement.personaData = items
- })
- )
-
- const personaLabel$ = subjects.personaLabel.pipe(
- tap((items) => {
- connectButtonElement.personaLabel = items
- })
- )
-
- const dAppName$ = subjects.dAppName.pipe(
- tap((value) => {
- connectButtonElement.dAppName = value
- })
- )
-
- const theme$ = subjects.theme.pipe(
- tap((value) => {
- connectButtonElement.theme = value
- })
- )
-
- return merge(
- onConnect$,
- status$,
- theme$,
- mode$,
- connected$,
- requestItems$,
- loggedInTimestamp$,
- isMobile$,
- activeTab$,
- isWalletLinked$,
- isExtensionAvailable$,
- onDisconnect$,
- onCancelRequestItem$,
- accounts$,
- personaData$,
- personaLabel$,
- onDestroy$,
- onUpdateSharedData$,
- onShowPopover$,
- dAppName$,
- onLinkClick$
- )
- })
- )
- .subscribe()
- )
-
- return {
- status$: subjects.status.asObservable(),
- onConnect$: subjects.onConnect.asObservable(),
- onDisconnect$: subjects.onDisconnect.asObservable(),
- onShowPopover$: subjects.onShowPopover.asObservable(),
- onUpdateSharedData$: subjects.onUpdateSharedData.asObservable(),
- onCancelRequestItem$: subjects.onCancelRequestItem.asObservable(),
- onLinkClick$: subjects.onLinkClick.asObservable(),
- setStatus: (value: RadixButtonStatus) => subjects.status.next(value),
- setTheme: (value: RadixButtonTheme) => subjects.theme.next(value),
- setMode: (value: 'light' | 'dark') => subjects.mode.next(value),
- setActiveTab: (value: 'sharing' | 'requests') =>
- subjects.activeTab.next(value),
- setIsMobile: (value: boolean) => subjects.isMobile.next(value),
- setIsWalletLinked: (value: boolean) => subjects.isWalletLinked.next(value),
- setIsExtensionAvailable: (value: boolean) =>
- subjects.isExtensionAvailable.next(value),
- setLoggedInTimestamp: (value: string) =>
- subjects.loggedInTimestamp.next(value),
- setConnected: (value: boolean) => subjects.connected.next(value),
- setRequestItems: (items: RequestItem[]) =>
- subjects.requestItems.next(items),
- setAccounts: (accounts: Account[]) => subjects.accounts.next(accounts),
- setPersonaData: (personaData: { value: string; field: string }[]) =>
- subjects.personaData.next(personaData),
- setPersonaLabel: (personaLabel: string) =>
- subjects.personaLabel.next(personaLabel),
- setDappName: (dAppName: string) => subjects.dAppName.next(dAppName),
- disconnect: () => {
- subjects.connected.next(false)
- subjects.status.next('default')
- },
- destroy: () => {
- subscriptions.unsubscribe()
- },
- }
-}
diff --git a/src/data-request/data-request.ts b/src/data-request/data-request.ts
deleted file mode 100644
index 4a3bc513..00000000
--- a/src/data-request/data-request.ts
+++ /dev/null
@@ -1,235 +0,0 @@
-import { ResultAsync, err, ok, okAsync } from 'neverthrow'
-import { DataRequestState, DataRequestBuilderItem } from './builders'
-import { StateClient } from '../state/state'
-import { RequestItemClient } from '../request-items/request-item-client'
-import { WalletClient } from '../wallet/wallet-client'
-import { transformWalletResponseToRdtWalletData } from './transformations/wallet-to-rdt'
-import { toWalletRequest } from './helpers/to-wallet-request'
-import { canDataRequestBeResolvedByRdtState } from './helpers/can-data-request-be-resolved-by-rdt-state'
-import {
- transformSharedDataToDataRequestState,
- transformWalletRequestToSharedData,
-} from './transformations/shared-data'
-import { DataRequestStateClient } from './data-request-state'
-import { WalletData } from '../state/types'
-import {
- AwaitedWalletDataRequestResult,
- RequestInterceptorFactoryOutput,
- WalletDataRequestResult,
-} from '../_types'
-
-export type DataRequestClient = ReturnType
-
-export const DataRequestClient = ({
- stateClient,
- requestItemClient,
- walletClient,
- useCache,
- dataRequestStateClient,
- requestInterceptor,
-}: {
- stateClient: StateClient
- requestItemClient: RequestItemClient
- walletClient: WalletClient
- dataRequestStateClient: DataRequestStateClient
- useCache: boolean
- requestInterceptor: RequestInterceptorFactoryOutput
-}) => {
- let challengeGenerator:
- | (() => ResultAsync)
- | undefined
-
- let connectResponseCallback:
- | ((result: AwaitedWalletDataRequestResult) => any)
- | undefined
-
- let dataRequestControl: (
- walletData: WalletData
- ) => ResultAsync
-
- const isChallengeNeeded = (dataRequestState: DataRequestState) =>
- dataRequestState.accounts?.withProof || dataRequestState.persona?.withProof
-
- const getChallenge = (
- dataRequestState: DataRequestState
- ): ResultAsync => {
- if (!isChallengeNeeded(dataRequestState)) return okAsync(undefined)
- if (!challengeGenerator)
- throw new Error('Expected proof but no challenge generator provided')
-
- return challengeGenerator()
- }
-
- const provideChallengeGenerator = (fn: () => Promise) => {
- challengeGenerator = () =>
- ResultAsync.fromPromise(fn(), () => ({
- error: 'GenerateChallengeError',
- message: 'Failed to generate challenge',
- }))
- }
-
- const provideConnectResponseCallback = (
- fn: (result: AwaitedWalletDataRequestResult) => any
- ) => {
- connectResponseCallback = (result) => fn(result)
- }
-
- const provideDataRequestControl = (
- fn: (walletData: WalletData) => Promise
- ) => {
- dataRequestControl = (walletData: WalletData) =>
- ResultAsync.fromPromise(fn(walletData), () => ({
- error: 'LoginRejectedByDapp',
- message: 'Login rejected by dApp',
- }))
- }
-
- const sendOneTimeRequest = (...items: DataRequestBuilderItem[]) =>
- sendRequest({
- dataRequestState: dataRequestStateClient.toDataRequestState(...items),
- isConnect: false,
- oneTime: true,
- })
-
- const sendRequest = ({
- isConnect,
- oneTime,
- dataRequestState,
- }: {
- dataRequestState: DataRequestState
- isConnect: boolean
- oneTime: boolean
- }): WalletDataRequestResult =>
- ok(dataRequestState)
- .asyncAndThen((dataRequestState) =>
- getChallenge(dataRequestState).andThen((challenge) =>
- toWalletRequest({
- dataRequestState,
- isConnect,
- challenge,
- oneTime,
- stateClient,
- })
- )
- )
- .andThen((walletDataRequest) => {
- const state = stateClient.getState()
- if (
- canDataRequestBeResolvedByRdtState(walletDataRequest, state) &&
- useCache
- )
- return okAsync(state.walletData)
-
- const isLoginRequest =
- !stateClient.getState().walletData.persona &&
- walletDataRequest.discriminator === 'authorizedRequest'
-
- return requestInterceptor({
- type: 'dataRequest',
- payload: walletDataRequest,
- })
- .map((walletDataRequest) => {
- const { id } = requestItemClient.add(
- isLoginRequest ? 'loginRequest' : 'dataRequest'
- )
- return { walletDataRequest, id }
- })
- .andThen(({ walletDataRequest, id }) =>
- walletClient
- .request(walletDataRequest, id)
- .mapErr(
- ({ error, message }): { error: string; message?: string } => ({
- error: error,
- message: message,
- })
- )
- .andThen(transformWalletResponseToRdtWalletData)
- .andThen((response) => {
- if (dataRequestControl)
- return dataRequestControl(response)
- .map(() => {
- requestItemClient.updateStatus({ id, status: 'success' })
- return response
- })
- .mapErr((error) => {
- requestItemClient.updateStatus({
- id,
- status: 'fail',
- error: error.error,
- })
- return error
- })
-
- requestItemClient.updateStatus({ id, status: 'success' })
- return ok(response)
- })
- .map((walletData) => {
- if (!oneTime)
- stateClient.setState({
- loggedInTimestamp: Date.now().toString(),
- walletData,
- sharedData: transformWalletRequestToSharedData(
- walletDataRequest,
- stateClient.getState().sharedData
- ),
- })
-
- return walletData
- })
- )
- })
-
- const setState = (...items: DataRequestBuilderItem[]) => {
- dataRequestStateClient.setState(...items)
- return {
- sendRequest: () =>
- sendRequest({
- dataRequestState: dataRequestStateClient.getState(),
- isConnect: false,
- oneTime: false,
- }),
- }
- }
-
- const updateSharedData = () =>
- sendRequest({
- dataRequestState: transformSharedDataToDataRequestState(
- stateClient.getState().sharedData
- ),
- isConnect: false,
- oneTime: false,
- })
-
- return {
- provideChallengeGenerator,
- provideDataRequestControl,
- provideConnectResponseCallback,
- sendOneTimeRequest,
- setState,
- sendRequest: ({
- isConnect,
- oneTime,
- }: {
- isConnect: boolean
- oneTime: boolean
- }) => {
- const result = sendRequest({
- isConnect,
- oneTime,
- dataRequestState: dataRequestStateClient.getState(),
- })
-
- if (connectResponseCallback)
- result
- .map((result) => {
- connectResponseCallback!(ok(result))
- })
- .mapErr((error) => {
- connectResponseCallback!(err(error))
- })
-
- return result
- },
- updateSharedData,
- }
-}
diff --git a/src/gateway/gateway-api.ts b/src/gateway/gateway-api.ts
deleted file mode 100644
index 5bc21c28..00000000
--- a/src/gateway/gateway-api.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import { GatewayApiClient as BabylonGatewayApiClient } from '@radixdlt/babylon-gateway-api-sdk'
-import { ResultAsync } from 'neverthrow'
-import { errorIdentity } from '../helpers/error-identity'
-
-export type GatewayApiClient = ReturnType
-
-export const GatewayApiClient = ({
- basePath,
- dAppDefinitionAddress,
- applicationName,
- applicationVersion,
-}: {
- basePath: string
- dAppDefinitionAddress?: string
- applicationVersion?: string
- applicationName?: string
-}) => {
- const { transaction, state, status } = BabylonGatewayApiClient.initialize({
- basePath,
- applicationName: applicationName || dAppDefinitionAddress || 'unknown',
- applicationVersion,
- applicationDappDefinitionAddress: dAppDefinitionAddress,
- })
-
- const getTransactionStatus = (transactionIntentHash: string) =>
- ResultAsync.fromPromise(
- transaction.getStatus(transactionIntentHash),
- errorIdentity
- )
-
- const getTransactionDetails = (transactionIntentHash: string) =>
- ResultAsync.fromPromise(
- transaction.getCommittedDetails(transactionIntentHash),
- errorIdentity
- )
-
- const getEntityDetails = (address: string) =>
- ResultAsync.fromPromise(
- state.getEntityDetailsVaultAggregated(address),
- errorIdentity
- )
-
- const getEntitiesDetails = (addresses: string[]) =>
- ResultAsync.fromPromise(
- state.getEntityDetailsVaultAggregated(addresses),
- errorIdentity
- )
-
- const getEntityNonFungibleIds = ({
- accountAddress,
- nftAddress,
- vaultAddress,
- }: {
- accountAddress: string
- nftAddress: string
- vaultAddress: string
- }) =>
- ResultAsync.fromPromise(
- state.innerClient.entityNonFungibleIdsPage({
- stateEntityNonFungibleIdsPageRequest: {
- address: accountAddress,
- vault_address: vaultAddress,
- resource_address: nftAddress,
- },
- }),
- errorIdentity
- )
-
- const getNetworkConfiguration = () =>
- ResultAsync.fromPromise(status.getNetworkConfiguration(), errorIdentity)
-
- return {
- getTransactionStatus,
- getTransactionDetails,
- getEntityDetails,
- getEntitiesDetails,
- getEntityNonFungibleIds,
- getNetworkConfiguration,
- transactionApi: transaction,
- stateApi: state,
- statusApi: status,
- }
-}
diff --git a/src/helpers/error-identity.ts b/src/helpers/error-identity.ts
deleted file mode 100644
index d71777e5..00000000
--- a/src/helpers/error-identity.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const errorIdentity = (e: unknown) => e as Error
diff --git a/src/helpers/parse-json.ts b/src/helpers/parse-json.ts
deleted file mode 100644
index dbc1b5ae..00000000
--- a/src/helpers/parse-json.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { err, ok, Result } from 'neverthrow'
-import { errorIdentity } from './error-identity'
-
-export const parseJSON = >(
- text: string
-): Result => {
- try {
- return ok(JSON.parse(text))
- } catch (error) {
- return err(errorIdentity(error))
- }
-}
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index 7d83ca87..00000000
--- a/src/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export * from './radix-dapp-toolkit'
-export * from './_types'
-export * from './data-request/builders'
-export * from '@radixdlt/wallet-sdk'
-export * from '@radixdlt/babylon-gateway-api-sdk'
-export * from './state/types'
-export * from './data-request/builders'
-export * from './data-request/data-request-state'
-export * from './gateway/gateway'
-export { GatewayApiClient as RdtGatewayApiClient } from './gateway/gateway-api'
-export * from './gateway/helpers/exponential-backoff'
diff --git a/src/radix-dapp-toolkit.spec.ts b/src/radix-dapp-toolkit.spec.ts
deleted file mode 100644
index 979f2f0c..00000000
--- a/src/radix-dapp-toolkit.spec.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-// @ts-nocheck
-import { RadixDappToolkit } from './radix-dapp-toolkit'
-import { createLogger } from '@radixdlt/wallet-sdk'
-import { Context, MockContext, createMockContext } from './test-helpers/context'
-import { delayAsync } from './test-helpers/delay-async'
-
-let onInitSpy = jest.fn()
-let onStateChangeSpy = jest.fn()
-let mockProviders: MockContext
-let providers: Context
-const logger = createLogger(2)
-
-describe('RadixDappToolkit', () => {
- const createRdt = (
- ...params: Partial>
- ) => {
- const options = {
- dAppDefinitionAddress:
- 'account_tdx_c_1p9c4zhvusrae49fguwm2cuxvltqquzxqex8ddr32e30qjlesen',
- networkId: 12,
- providers: params[0]?.providers ?? mockProviders,
- logger,
- } satisfies Parameters[0]
-
- return RadixDappToolkit(options)
- }
-
- beforeEach(() => {
- onInitSpy.mockReset()
- onStateChangeSpy.mockReset()
- mockProviders = createMockContext()
- providers = mockProviders as unknown as Context
- })
-
- it('should bootstrap RDT', async () => {
- const rdt = createRdt()
-
- await delayAsync(0)
-
- expect(mockProviders.connectButton.setDappName).toBeCalledWith(
- 'Unnamed dApp'
- )
- })
-
- it('should emit stateEntityDetails response', async () => {
- const rdt = createRdt()
- const spy = jest.fn()
- rdt.dAppDefinitionAccount.entityDetails$.subscribe(spy)
-
- await delayAsync(0)
-
- expect(spy).toHaveBeenCalledWith({
- address: 'test',
- metadata: {
- items: [],
- },
- })
- })
-
- it('should set dAppDefinitionAccount.entityDetails ', async () => {
- const rdt = createRdt()
-
- expect(rdt.dAppDefinitionAccount.entityDetails).toBe(undefined)
- await delayAsync(0)
- expect(rdt.dAppDefinitionAccount.entityDetails).toEqual({
- address: 'test',
- metadata: {
- items: [],
- },
- })
- })
-})
diff --git a/src/radix-dapp-toolkit.ts b/src/radix-dapp-toolkit.ts
deleted file mode 100644
index b45fb114..00000000
--- a/src/radix-dapp-toolkit.ts
+++ /dev/null
@@ -1,390 +0,0 @@
-import { StateClient } from './state/state'
-import {
- ConnectButtonClient,
- isMobile,
-} from './connect-button/connect-button-client'
-import { WalletClient } from './wallet/wallet-client'
-import { WalletSdk } from '@radixdlt/wallet-sdk'
-import { GatewayApiClient } from './gateway/gateway-api'
-import { GatewayClient, MetadataValue } from './gateway/gateway'
-import {
- BehaviorSubject,
- Observable,
- Subscription,
- filter,
- merge,
- of,
- switchMap,
- tap,
- timer,
-} from 'rxjs'
-
-import { RequestItemClient } from './request-items/request-item-client'
-import { LocalStorageClient } from './storage/local-storage-client'
-import { DataRequestClient } from './data-request/data-request'
-import { transformWalletDataToConnectButton } from './data-request/transformations/wallet-data-to-connect-button'
-import { DataRequestStateClient } from './data-request/data-request-state'
-import {
- RadixNetworkConfigById,
- StateEntityDetailsVaultResponseItem,
-} from '@radixdlt/babylon-gateway-api-sdk'
-import {
- ButtonApi,
- GatewayApi,
- RadixDappToolkitOptions,
- SendTransactionInput,
- WalletApi,
- WalletRequest,
- requestInterceptorFactory,
- RequestInterceptor,
-} from './_types'
-import { mergeMap, withLatestFrom } from 'rxjs/operators'
-import { WalletData } from './state/types'
-
-export type RadixDappToolkit = {
- dAppDefinitionAccount: {
- entityDetails$: Observable
- entityDetails: StateEntityDetailsVaultResponseItem | undefined
- }
- walletApi: WalletApi
- gatewayApi: GatewayApi
- buttonApi: ButtonApi
- disconnect: () => void
- destroy: () => void
-}
-
-export const RadixDappToolkit = (
- options: RadixDappToolkitOptions
-): RadixDappToolkit => {
- const {
- dAppDefinitionAddress,
- networkId,
- providers,
- logger,
- onDisconnect,
- explorer,
- gatewayBaseUrl,
- applicationName,
- applicationVersion,
- useCache = true,
- requestInterceptor = (async ({ payload }: WalletRequest) =>
- payload) as RequestInterceptor,
- } = options || {}
-
- const storageKey = `rdt:${dAppDefinitionAddress}:${networkId}`
- const dAppDefinitionAddressSubject = new BehaviorSubject(
- dAppDefinitionAddress
- )
- const stateEntityDetailsSubject = new BehaviorSubject<
- StateEntityDetailsVaultResponseItem | undefined
- >(undefined)
- const subscriptions = new Subscription()
-
- const connectButtonClient =
- providers?.connectButton ??
- ConnectButtonClient({
- logger,
- })
-
- connectButtonClient.setIsMobile(isMobile())
-
- const gatewayClient =
- providers?.gatewayClient ??
- GatewayClient({
- logger,
- gatewayApi: GatewayApiClient({
- basePath:
- gatewayBaseUrl ?? RadixNetworkConfigById[networkId].gatewayUrl,
- dAppDefinitionAddress,
- applicationName,
- applicationVersion,
- }),
- })
-
- const storageClient = providers?.storageClient ?? LocalStorageClient()
-
- const walletSdk =
- providers?.walletSdk ??
- WalletSdk({
- logger,
- networkId,
- dAppDefinitionAddress,
- })
-
- const requestItemClient =
- providers?.requestItemClient ??
- RequestItemClient(storageKey, storageClient, {
- logger,
- })
-
- const walletClient =
- providers?.walletClient ??
- WalletClient({
- logger,
- onCancelRequestItem$: connectButtonClient.onCancelRequestItem$,
- walletSdk,
- gatewayClient,
- requestItemClient,
- })
-
- const stateClient =
- providers?.stateClient ??
- StateClient(storageKey, storageClient, {
- logger,
- })
-
- const dataRequestStateClient =
- providers?.dataRequestStateClient ?? DataRequestStateClient({})
-
- const withInterceptor = requestInterceptorFactory(requestInterceptor)
-
- const dataRequestClient =
- providers?.dataRequestClient ??
- DataRequestClient({
- stateClient,
- requestItemClient,
- walletClient,
- useCache,
- dataRequestStateClient,
- requestInterceptor: withInterceptor,
- })
-
- const disconnect = () => {
- requestItemClient.items$.value.forEach((item) => {
- if (item.showCancel) walletClient.cancelRequest(item.id)
- })
- stateClient.reset()
- walletClient.resetRequestItems()
- connectButtonClient.disconnect()
- if (onDisconnect) onDisconnect()
- }
-
- subscriptions.add(
- dAppDefinitionAddressSubject
- .pipe(
- filter((address) => !!address),
- switchMap((address) =>
- gatewayClient.gatewayApi
- .getEntityDetails(address)
- .map((details) => {
- stateEntityDetailsSubject.next(details)
- return MetadataValue(
- details?.metadata.items.find((item) => item.key === 'name')
- ).stringified
- })
- .map((dAppName) => {
- connectButtonClient.setDappName(dAppName ?? 'Unnamed dApp')
- })
- )
- )
- .subscribe()
- )
-
- subscriptions.add(
- walletClient.extensionStatus$
- .pipe(
- tap((result) => {
- connectButtonClient.setIsExtensionAvailable(
- result.isExtensionAvailable
- )
- connectButtonClient.setIsWalletLinked(result.isWalletLinked)
- })
- )
-
- .subscribe()
- )
-
- subscriptions.add(
- connectButtonClient.onConnect$
- .pipe(
- switchMap(() => {
- stateClient.reset()
- return dataRequestClient.sendRequest({
- isConnect: true,
- oneTime: false,
- })
- })
- )
- .subscribe()
- )
-
- subscriptions.add(
- connectButtonClient.onLinkClick$
- .pipe(
- tap(({ type, data }) => {
- if (['account', 'transaction'].includes(type)) {
- const { baseUrl, transactionPath, accountsPath } = explorer ?? {
- baseUrl: RadixNetworkConfigById[networkId].dashboardUrl,
- transactionPath: '/transaction/',
- accountsPath: '/account/',
- }
- if (!baseUrl || !window) return
-
- const url = `${baseUrl}${
- type === 'transaction' ? transactionPath : accountsPath
- }${data}`
-
- window.open(url)
- } else if (type === 'setupGuide')
- window.open('https://wallet.radixdlt.com')
- else if (type === 'showQrCode') {
- walletSdk.openPopup()
- }
- })
- )
- .subscribe()
- )
-
- subscriptions.add(
- connectButtonClient.onShowPopover$
- .pipe(
- withLatestFrom(walletClient.requestItems$),
- tap(([_, items]) => {
- if (items.filter((item) => item.status === 'pending').length > 0) {
- connectButtonClient.setActiveTab('requests')
- }
- })
- )
- .subscribe()
- )
-
- subscriptions.add(
- connectButtonClient.onDisconnect$.pipe(tap(disconnect)).subscribe()
- )
-
- subscriptions.add(
- stateClient.state$
- .pipe(
- tap((state) => {
- const { personaData, accounts, personaLabel, connected } =
- transformWalletDataToConnectButton(state.walletData)
- connectButtonClient.setLoggedInTimestamp(state.loggedInTimestamp)
- connectButtonClient.setAccounts(accounts)
- connectButtonClient.setPersonaData(personaData)
- connectButtonClient.setPersonaLabel(personaLabel)
- connectButtonClient.setConnected(connected)
- })
- )
- .subscribe()
- )
-
- subscriptions.add(
- walletClient.requestItems$
- .pipe(tap((items) => connectButtonClient.setRequestItems(items)))
- .subscribe()
- )
-
- subscriptions.add(
- requestItemClient.change$
- .pipe(
- withLatestFrom(requestItemClient.items$),
- tap(([, items]) => {
- const hasPendingItem = items.find((item) => item.status === 'pending')
-
- if (hasPendingItem) {
- connectButtonClient.setStatus('pending')
- }
- }),
- mergeMap(([change]) => {
- const newStatus = change.newValue?.status
- const oldStatus = change.oldValue?.status
-
- if (
- oldStatus === 'pending' &&
- (newStatus === 'success' || newStatus === 'fail')
- ) {
- connectButtonClient.setStatus(
- newStatus === 'success' ? 'success' : 'error'
- )
-
- return timer(2000).pipe(
- withLatestFrom(walletClient.requestItems$),
- tap(([_, items]) => {
- const pendingItem = items.find(
- (item) => item.status === 'pending'
- )
- connectButtonClient.setStatus(
- pendingItem ? 'pending' : 'default'
- )
- })
- )
- }
-
- return of()
- })
- )
- .subscribe()
- )
-
- subscriptions.add(
- merge(connectButtonClient.onUpdateSharedData$)
- .pipe(switchMap(() => dataRequestClient.updateSharedData()))
- .subscribe()
- )
-
- const gatewayApi = {
- state: gatewayClient.gatewayApi.stateApi,
- status: gatewayClient.gatewayApi.statusApi,
- transaction: gatewayClient.gatewayApi.transactionApi,
- }
-
- const walletApi = {
- setRequestData: dataRequestClient.setState,
- sendRequest: () =>
- dataRequestClient.sendRequest({
- isConnect: false,
- oneTime: false,
- }),
- provideChallengeGenerator: (
- input: Parameters[0]
- ) => dataRequestClient.provideChallengeGenerator(input),
- dataRequestControl: (fn: (walletData: WalletData) => Promise) => {
- dataRequestClient.provideDataRequestControl(fn)
- },
- provideConnectResponseCallback:
- dataRequestClient.provideConnectResponseCallback,
- updateSharedData: () => dataRequestClient.updateSharedData(),
- sendOneTimeRequest: dataRequestClient.sendOneTimeRequest,
- sendTransaction: (input: SendTransactionInput) =>
- withInterceptor({
- type: 'sendTransaction',
- payload: input,
- }).andThen(walletClient.sendTransaction),
- walletData$: stateClient.walletData$,
- getWalletData: () => stateClient.getWalletData(),
- }
-
- const buttonApi = {
- setTheme: connectButtonClient.setTheme,
- setMode: connectButtonClient.setMode,
- status$: connectButtonClient.status$,
- }
-
- const destroy = () => {
- stateClient.destroy()
- walletClient.destroy()
- subscriptions.unsubscribe()
- connectButtonClient.destroy()
- }
-
- return {
- dAppDefinitionAccount: {
- entityDetails$: stateEntityDetailsSubject
- .asObservable()
- .pipe(
- filter(
- (details): details is StateEntityDetailsVaultResponseItem =>
- !!details
- )
- ),
- get entityDetails() {
- return stateEntityDetailsSubject.value
- },
- },
- walletApi,
- gatewayApi,
- buttonApi,
- disconnect,
- destroy,
- }
-}
diff --git a/src/request-items/request-item-client.ts b/src/request-items/request-item-client.ts
deleted file mode 100644
index 4b016fd0..00000000
--- a/src/request-items/request-item-client.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-import { RequestItem, RequestStatusTypes } from '@radixdlt/connect-button'
-import { map, Subscription, tap } from 'rxjs'
-import { Logger } from 'tslog'
-import { RequestItemSubjects } from './subjects'
-import { errorType } from '@radixdlt/wallet-sdk'
-import { StorageProvider } from '../_types'
-
-export type RequestItemClient = ReturnType
-export const RequestItemClient = (
- storageKey: string,
- storageClient: StorageProvider,
- input: {
- subjects?: RequestItemSubjects
- logger?: Logger
- }
-) => {
- const logger = input.logger
- const requestItemIds = new Set()
- const subscriptions = new Subscription()
- const requestsItemStore = new Map()
- const subjects = input.subjects || RequestItemSubjects()
- const requestItemStoreKey = `${storageKey}:requestItemStore`
-
- storageClient
- .getData>(requestItemStoreKey)
- .map((store) => {
- if (store) {
- Object.keys(store).forEach((key) => {
- requestItemIds.add(key)
- requestsItemStore.set(key, store[key])
- })
- }
- subjects.items.next(getItemsList())
- })
-
- const emitChange = (
- oldValue: RequestItem | undefined,
- newValue: RequestItem | undefined
- ) => subjects.onChange.next({ oldValue, newValue })
-
- const createItem = (type: RequestItem['type']): RequestItem => ({
- type,
- status: 'pending',
- timestamp: Date.now(),
- id: crypto.randomUUID(),
- showCancel: true,
- })
-
- const add = (type: RequestItem['type']) => {
- const item = createItem(type)
- requestsItemStore.set(item.id, item)
- requestItemIds.add(item.id)
- emitChange(undefined, item)
- logger?.trace(`addRequestItem`, {
- id: item.id,
- status: item.status,
- })
- return item
- }
-
- const remove = (id: string) => {
- if (requestsItemStore.has(id)) {
- const oldValue = requestsItemStore.get(id)!
- requestsItemStore.delete(id)
- requestItemIds.delete(id)
- emitChange(oldValue, undefined)
- logger?.trace(`removeRequestItem`, id)
- }
- }
-
- const patch = (id: string, partialValue: Partial) => {
- const item = requestsItemStore.get(id)
- if (item) {
- const updated = {
- ...item,
- ...partialValue,
- } as RequestItem
- requestsItemStore.set(id, updated)
- emitChange(item, updated)
- logger?.trace(`patchRequestItemStatus`, updated)
- }
- }
-
- const cancel = (id: string) => {
- if (requestsItemStore.has(id)) {
- patch(id, { status: 'fail', error: errorType.canceledByUser })
- logger?.trace(`cancelRequestItem`, id)
- }
- }
-
- const reset = () => {
- requestsItemStore.clear()
- requestItemIds.clear()
- emitChange(undefined, undefined)
- logger?.trace(`resetRequestItems`)
- }
-
- const updateStatus = ({
- id,
- status,
- error,
- transactionIntentHash,
- }: {
- id: string
- status: RequestStatusTypes
- error?: string
- transactionIntentHash?: string
- }) => {
- const item = requestsItemStore.get(id)
- if (item) {
- const updated = {
- ...item,
- status,
- } as RequestItem
- if (updated.status === 'fail') {
- updated.error = error!
- }
- if (updated.status === 'success' && updated.type === 'sendTransaction') {
- updated.transactionIntentHash = transactionIntentHash!
- }
- requestsItemStore.set(id, updated)
- emitChange(item, updated)
-
- logger?.trace(`updateRequestItemStatus`, updated)
- }
- }
-
- const getIds = () => [...requestItemIds].reverse()
-
- const getItemsList = () =>
- getIds()
- .map((id) => ({ id, ...requestsItemStore.get(id) }))
- .filter((item): item is RequestItem => !!item)
-
- subscriptions.add(
- subjects.onChange
- .pipe(
- map(() => getItemsList()),
- tap((items) => subjects.items.next(items)),
- tap(() => {
- const entries = Array.from(requestsItemStore.entries())
-
- storageClient.setData(
- requestItemStoreKey,
- Object.fromEntries(
- entries.filter(([, value]) => value.status !== 'pending')
- )
- )
- })
- )
- .subscribe()
- )
-
- return {
- add,
- remove,
- cancel,
- updateStatus,
- patch,
- reset,
- destroy: () => {
- subscriptions.unsubscribe()
- },
- items$: subjects.items,
- change$: subjects.onChange.asObservable(),
- }
-}
diff --git a/src/state/state.ts b/src/state/state.ts
deleted file mode 100644
index 84f63e8c..00000000
--- a/src/state/state.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { Logger } from 'tslog'
-import { StateSubjects } from './subjects'
-import { StorageProvider } from '../_types'
-import {
- Subscription,
- combineLatest,
- debounceTime,
- filter,
- first,
- map,
- skip,
- switchMap,
- tap,
-} from 'rxjs'
-import { ResultAsync } from 'neverthrow'
-import { RdtState, walletDataDefault } from './types'
-import { produce } from 'immer'
-import isEqual from 'lodash.isequal'
-
-export type StateClient = ReturnType
-
-export const StateClient = (
- key: string,
- storageClient: StorageProvider,
- options: Partial<{
- subjects: StateSubjects
- logger: Logger